working + many improvements

ryuji new features, saki changes, write build code, bump makoto version, add preview
This commit is contained in:
jetstream0
2023-07-31 14:28:26 -07:00
parent 17baac0640
commit c041f77b88
14 changed files with 312 additions and 88 deletions

View File

@@ -18,9 +18,9 @@ Makoto is the markdown-to-html parser, made with no dependencies. It was made ar
It also has a very cool warnings feature, that isn't used in this project, but can be seen in action if you use the [Makoto Web Editor](https://makoto.prussia.dev).
## Ryuji
Ryuji is a simple templating system. It's Jinja/Nunjucks inspired but has less features. On the upside, Ryuji is less than 200 lines of code and supports if statements, for loops, components and inserting variables.
Ryuji is a simple templating system. It's Jinja/Nunjucks inspired but has less features. On the upside, Ryuji is around 200 lines of code and supports if statements, for loops, components and inserting variables.
I didn't write any docs for it, but you can see the syntax if you look in the `templates` directory or look in `tests.ts`.
## Saki
Saki is the build system that puts it all together and outputs the blog's static html.
Saki is the build system that puts it all together and outputs the blog's static html. Even more simple than Ryuji, it is just around 70 lines of code.

View File

@@ -1,18 +1,92 @@
import * as path from 'path';
import { readFileSync } from 'fs';
import { parse_md_to_html } from 'makoto';
import { Renderer } from './ryuji.js';
import { BlogBuilder, PostMetadata } from './saki.js';
import { Builder } from './saki.js';
import _posts_metadata from './posts/_metadata.json';
export interface PostMetadata {
title: string,
slug: string,
filename: string,
date: string,
author: string,
tags: string[],
}
export interface Post extends PostMetadata {
md_lines: string[],
html: string,
tags_exist: boolean,
}
let renderer: Renderer = new Renderer("templates", "components");
let builder: BlogBuilder = new BlogBuilder(renderer);
let builder: Builder = new Builder();
let posts_metadata: PostMetadata[] = Object.values(_posts_metadata);
builder.serve_static_folder("static");
//home page
builder.serve_template("/", "index.html", {
builder.serve_template(renderer, "/", "index.html", {
posts: posts_metadata,
});
//blog posts
builder.serve_markdowns("/posts", "/posts", "post.html", posts_metadata, true);
//if two tags reduce down to the same slug, oh well, not my problem
function slugify(tag: string) {
let allowed_chars: string[] = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "_"];
return tag.replaceAll(" ", "_").split("").filter((char) => allowed_chars.includes(char.toLowerCase())).join("");
}
//slugify all the tags
posts_metadata.forEach((post_metadata) => post_metadata.tags = post_metadata.tags.map((tag) => slugify(tag)));
let posts_serve_paths: string[] = [];
let posts_vars: any[] = [];
let tags: string[] = []; //also get all the tags since we are iterating through all the posts
for (let i=0; i < posts_metadata.length; i++) {
let post_metadata: PostMetadata = posts_metadata[i];
posts_serve_paths.push(`/posts/${post_metadata.slug}`);
let post_md_path: string = path.join(__dirname, `/posts/${post_metadata.slug}.md`);
let md: string = readFileSync(post_md_path, "utf-8").replaceAll("\r", "");
let html: string = parse_md_to_html(md);
for (let j=0; j < post_metadata.tags.length; j++) {
let tag: string = post_metadata.tags[j];
if (!tags.includes(tag)) {
tags.push(post_metadata.tags[j])
}
}
let post: Post = {
...post_metadata,
md_lines: md.split("\n"),
html,
tags_exist: post_metadata.tags.length !== 0,
}
posts_vars.push(
{
post,
author_expected: post.author.toLowerCase().startsWith("jetstream0") || post.author.toLowerCase().startsWith("prussia"),
}
);
}
builder.serve_templates(renderer, posts_serve_paths, "post.html", posts_vars);
//tags
let tags_serve_paths: string[] = [];
let tags_vars: any[] = [];
for (let i=0; i < tags.length; i++) {
let tag: string = tags[i];
tags_serve_paths.push(`/tags/${slugify(tag)}`);
tags_vars.push({
tag,
posts: posts_metadata.filter((post) => post.tags.includes(tag)),
});
}
builder.serve_templates(renderer, tags_serve_paths, "tags.html", tags_vars);

8
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.1",
"license": "AGPL-3.0",
"dependencies": {
"makoto": "^1.0.0"
"makoto": "^1.0.1"
},
"devDependencies": {
"@types/node": "^20.4.5"
@@ -22,9 +22,9 @@
"dev": true
},
"node_modules/makoto": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/makoto/-/makoto-1.0.0.tgz",
"integrity": "sha512-Zl2GWYNrPqpSXZajf2rBcgOD6rtwi3gT/sc0pbjGJSPvad9uoJ0z6iMTZowfohbYHhTLeR2x3E20CQ//myyrqg=="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/makoto/-/makoto-1.0.1.tgz",
"integrity": "sha512-SV/HW0lAy/D0tuYlX00Hg1g+5uo4ak95tcObw3RcY5U/fWMlc3aJuOPzbvqaWNKtDgPafCclU1eTDuZ6ZARSvA=="
}
}
}

View File

@@ -5,7 +5,8 @@
"main": "index.js",
"scripts": {
"build": "tsc -p . && node index.js",
"test": "tsc -p . && node tests.js"
"test": "tsc -p . && node tests.js",
"preview": "npm run build && node preview.js"
},
"repository": {
"type": "git",
@@ -18,7 +19,7 @@
},
"homepage": "https://github.com/jetstream0/hedgeblog#readme",
"dependencies": {
"makoto": "^1.0.0"
"makoto": "^1.0.1"
},
"devDependencies": {
"@types/node": "^20.4.5"

View File

@@ -5,6 +5,6 @@
"filename": "example",
"date": "30/12/1999",
"author": "Prussia",
"tags": []
"tags": ["example", "fake", "please remember to remove"]
}
}

View File

@@ -12,8 +12,8 @@ console.log("code block!!!");
```
> ## woah a blockquote
> Yeehaw ^[1]^
> Yeehaw ^\[1]^
===
----
- [1]: Source? Me.
- \[1]: Source? Me.

56
preview.ts Normal file
View File

@@ -0,0 +1,56 @@
import { createServer } from 'http';
import * as path from 'path';
import { existsSync, readFileSync } from 'fs';
const port: number = 8042;
createServer((req, res) => {
let req_path: string;
if (!req.url.includes(".")) {
req_path = path.join(__dirname, "build", req.url, "index.html");
} else {
req_path = path.join(__dirname, "build", req.url);
}
if (!existsSync(req_path)) {
res.writeHead(404);
//write file
res.write("404");
return res.end();
}
//set content type
let non_utf8_content_types: string[] = ["image/png", "image/gif"];
let content_type: string;
switch (req_path.split(".")[1]) {
case "html":
content_type = "text/html; charset=utf-8";
break;
case "css":
content_type = "text/css; charset=utf-8";
break;
case "js":
content_type = "text/javascript";
break;
case "png":
case "ico":
content_type = "image/png";
break;
case "gif":
content_type = "image/gif";
break;
default:
content_type = "text/plain";
}
res.writeHead(200, {
'Content-Type': content_type,
});
//write file
if (non_utf8_content_types.includes(content_type)) {
res.write(readFileSync(req_path));
} else {
res.write(readFileSync(req_path, "utf-8"));
}
//end response
res.end();
}).listen(port);
console.log(`Preview on port ${port}`);

View File

@@ -1,6 +1,6 @@
import { readFileSync } from 'fs';
export const SYNTAX_REGEX = /\[\[ [a-zA-Z0-9.:\-_]+ \]\]/g;
export const SYNTAX_REGEX = /\[\[ [a-zA-Z0-9.:\-_!]+ \]\]/g;
export type file_extension = `.${string}`;
@@ -10,6 +10,7 @@ export interface ForLoopInfo {
current: number,
var_value: any, //value we are looping over
iter_var_name?: string,
index_var_name?: string,
}
export class Renderer {
@@ -104,6 +105,18 @@ export class Renderer {
Renderer.check_var_name_legality(iter_var_name, false);
vars[iter_var_name] = var_value[0];
}
if (typeof exp_parts[3] === "string") {
//set index count
let index_var_name: string = exp_parts[3];
Renderer.check_var_name_legality(index_var_name, false);
vars[index_var_name] = 0;
}
if (typeof exp_parts[4] === "string") {
//set max count
let max_var_name: string = exp_parts[4];
Renderer.check_var_name_legality(max_var_name, false);
vars[max_var_name] = var_value.length-1;
}
//add to for loops
for_loops.push({
index,
@@ -111,6 +124,7 @@ export class Renderer {
current: 0,
var_value,
iter_var_name: exp_parts[2],
index_var_name: exp_parts[3],
});
//make sure thing we are iterating over isn't empty
if (var_value.length === 0) {
@@ -122,16 +136,16 @@ export class Renderer {
continue;*/
let sliced = matches.slice(index+1, matches.length);
let new_index: number;
let extra_forss: number = 0;
let extra_fors: number = 0;
for (let i=0; i < sliced.length; i++) {
if (sliced[i][0].startsWith("[[ for:")) {
extra_forss++;
extra_fors++;
} else if (sliced[i][0] === "[[ endfor ]]") {
if (extra_forss === 0) {
if (extra_fors === 0) {
new_index = i;
break;
}
extra_forss--;
extra_fors--;
}
}
if (typeof new_index === "undefined") throw Error("if statement missing an `[[ endif ]]`");
@@ -151,6 +165,9 @@ export class Renderer {
if (current_loop.iter_var_name) {
vars[current_loop.iter_var_name] = current_loop.var_value[current_loop.current];
}
if (current_loop.index_var_name) {
vars[current_loop.index_var_name] = current_loop.current;
}
//go back to start of for loop index
index = current_loop.index;
continue;
@@ -159,9 +176,40 @@ export class Renderer {
if (typeof exp_parts[1] !== "string") throw Error("`if:` statement missing variable name afterwards");
let var_name: string = exp_parts[1];
let var_value = Renderer.get_var(var_name, vars);
if (var_value) {
//yup, nothing here
let condition_pass: boolean;
if (typeof exp_parts[2] !== "string") {
//make sure var is truthy
if (var_value) {
condition_pass = true;
} else {
condition_pass = false;
}
} else {
//compare with second var
let var_name2: string = exp_parts[2];
let if_not: boolean = false;
if (var_name2.startsWith("!")) {
var_name2 = var_name2.slice(1, var_name2.length);
if_not = true;
}
let var_value2 = Renderer.get_var(var_name2, vars);
if (if_not) {
//make sure the two compared variables are NOT equal
if (var_value !== var_value2) {
condition_pass = true;
} else {
condition_pass = false;
}
} else {
//regular comparison statement
if (var_value === var_value2) {
condition_pass = true;
} else {
condition_pass = false;
}
}
}
if (!condition_pass) { //failed condition
//skip to the endif
let sliced = matches.slice(index+1, matches.length);
let new_index: number;
@@ -192,7 +240,20 @@ export class Renderer {
} else {
var_name = exp_parts[0];
}
let var_value = Renderer.get_var(var_name, vars);
//convert to string
let var_value: string = String(Renderer.get_var(var_name, vars));
//add indentation
let current_lines: string[] = rendered.split("\n")
let current_last: string = current_lines[current_lines.length-1];
let indentation: number = 0;
for (let i=0; i < current_last.length; i++) {
if (current_last[i] !== " ") break;
indentation++;
}
let var_lines: string[] = var_value.split("\n");
let var_first: string = var_lines.shift();
//append spaces
var_value = var_lines.length === 0 ? var_first : var_first+"\n"+var_lines.map((var_line) => " ".repeat(indentation)+var_line).join("\n");
if (exp_parts[0] === "html") {
//variable but not sanitized
rendered += var_value;

69
saki.ts
View File

@@ -1,7 +1,6 @@
import * as path from 'path';
import { copyFileSync, existsSync, readdirSync, rmSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import type { Renderer } from './ryuji.js';
import { parse_md_to_html } from 'makoto';
export class Builder {
build_dir: string;
@@ -44,9 +43,11 @@ export class Builder {
let dest_path: string = path.join(this.build_dir, serve_path);
if (!serve_path.includes(".")) {
//serve as index.html in serve_path directory
//will not make a new directory if `serve_path` is "/", since the build directory already exists
if (dest_path !== this.build_dir && dest_path !== path.join(this.build_dir, "/")) {
//will not make a new directory if `serve_path` is "/", since the build directory already exists
mkdirSync(dest_path);
mkdirSync(dest_path, {
recursive: true,
});
}
writeFileSync(path.join(dest_path, "index.html"), content);
} else {
@@ -60,68 +61,14 @@ export class Builder {
this.serve_content(file_content, serve_path);
}
_serve_template(renderer: Renderer, serve_path: string, template_name: string, vars: any) {
serve_template(renderer: Renderer, serve_path: string, template_name: string, vars: any) {
let content: string = renderer.render_template(template_name, vars);
this.serve_content(content, serve_path);
}
}
//this code is more or less specific to my blog
export interface PostMetadata {
title: string,
slug: string,
filename: string,
date: string,
author: string,
tags: string[],
}
export interface Post extends PostMetadata {
md: string,
html: string,
}
export class BlogBuilder extends Builder {
renderer: Renderer;
constructor(renderer: Renderer, build_dir: string="/build") {
super(build_dir);
this.renderer = renderer;
}
serve_template(serve_path: string, template_name: string, vars: any) {
super._serve_template(this.renderer, serve_path, template_name, vars);
}
serve_markdown(serve_path: string, template_name: string, markdown_post: Post, additional_vars: any={}) {
additional_vars.post = markdown_post;
this.serve_template(serve_path, template_name, {
post: markdown_post,
author_expected: markdown_post.author.toLowerCase().startsWith("jetstream0") || markdown_post.author.toLowerCase().startsWith("prussia"),
});
}
serve_markdowns(serve_path: string, posts_path: string, template_name: string, posts_metadata: PostMetadata[], own_dir: boolean=true) {
let posts_dir_path: string = path.join(this.build_dir, posts_path);
if (!existsSync(posts_dir_path)) {
mkdirSync(posts_dir_path);
}
for (let i=0; i < posts_metadata.length; i++) {
let post_metadata: PostMetadata = posts_metadata[i];
let post_md_path: string = path.join(__dirname, posts_path, `${post_metadata.slug}.md`);
let post_md: string = readFileSync(post_md_path, "utf-8");
let post_html: string = parse_md_to_html(post_md);
let post: Post = {
...post_metadata,
md: post_md,
html: post_html,
};
if (own_dir) {
this.serve_markdown(path.join(serve_path, post.slug), template_name, post);
} else {
this.serve_markdown(path.join(serve_path, `${post.slug}.html`), template_name, post);
}
serve_templates(renderer: Renderer, serve_paths: string[], template_name: string, vars_array: any[]) {
for (let i=0; i < serve_paths.length; i++) {
this.serve_template(renderer, serve_paths[i], template_name, vars_array[i]);
}
}
}

View File

@@ -1 +1 @@
<a style="position: absolute; left: 10px; top: 10px;"><- Get me outta here!</a>
<a href="/" style="position: absolute; left: 10px; top: 10px;"><- Get me outta here!</a>

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>prussia fan club</title>
<link rel="icon" type="image/png" href="/favicon.ico">
</head>
<body>
<div>

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>[[ post.title ]]</title>
<link rel="icon" type="image/png" href="/favicon.ico">
</head>
<body>
[[ component:return ]]
@@ -11,7 +12,7 @@
<div id="post-info">
<h1>[[ post.title ]]</h1>
<div>
<span><span [[ if:author_expected ]]title="(obviously)"[[ endif ]]>By [[ post.author ]]</span> | <span>[[ post.date ]]</span></span>
<span><span [[ if:author_expected ]]title="(obviously)"[[ endif ]]>By [[ post.author ]]</span> | <span>[[ post.date ]]</span> [[ if:post.tags_exist ]]| <span>[[ for:post.tags:tag:index:max ]]<a href="/tags/[[ tag ]]">[[ tag ]]</a>[[ if:index:!max ]], [[ endif ]][[ endfor ]]</span>[[ endif ]]</span>
</div>
</div>
<input id="show-md" type="checkbox"/><label for="html">Show MD</label>
@@ -19,7 +20,12 @@
[[ html:post.html ]]
</div>
<div id="post-md">
[[ post.md ]]
[[ for:post.md_lines:line ]]
[[ line ]]
[[ if:line ]]
<br>
[[ endif ]]
[[ endfor ]]
</div>
</div>
</body>

21
templates/tags.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>"[[ tag ]]" posts</title>
<link rel="icon" type="image/png" href="/favicon.ico">
</head>
<body>
<div>
<h2>Search for posts with tag "[[ tag ]]"</h2>
</div>
<div>
<ul>
[[ for:posts:post ]]
[[ component:post-listing ]]
[[ endfor ]]
</ul>
</div>
</body>
</html>

View File

@@ -116,4 +116,61 @@ test_assert_equal(
"for loop with template test"
);
//[[ if index_var ]] will be false when index_var is 0 btw
test_assert_equal(
renderer.render(
"[[ for:trees:_tree:index_var ]][[ if:index_var ]]<b>[[ index_var ]]</b>[[ endif ]][[ endfor ]]",
{
trees: [
"mango",
"oak",
"redwood",
"palm",
],
}
),
"<b>1</b><b>2</b><b>3</b>",
"for loop with index test",
);
test_assert_equal(
renderer.render(
"[[ for:trees:_tree:index_var:max_var ]][[ index_var ]]/[[ max_var ]][[ if:index_var:!max_var ]] [[ endif ]][[ endfor ]]",
{
trees: [
"mango",
"oak",
"redwood",
"palm",
],
}
),
"0/3 1/3 2/3 3/3",
"for loop with index, max and if statement test",
);
test_assert_equal(
renderer.render(
"[[ if:nutritious:delicious ]]meets both[[ endif ]]",
{
nutritious: "yes",
delicious: "yes",
}
),
"meets both",
"if statement with comparisons test",
);
test_assert_equal(
renderer.render(
"[[ if:nutritious:!delicious ]]fails both[[ endif ]][[ if:nutritious:!yes ]]this never displays[[ endif ]]",
{
nutritious: "yes",
delicious: "no",
yes: "yes",
}
),
"fails both",
"if not statement with comparisons test",
);
log_test_results();