From c041f77b881fab6bf34c76360f126d0114f4c215 Mon Sep 17 00:00:00 2001 From: jetstream0 <49297268+jetstream0@users.noreply.github.com> Date: Mon, 31 Jul 2023 14:28:26 -0700 Subject: [PATCH] working + many improvements ryuji new features, saki changes, write build code, bump makoto version, add preview --- README.md | 4 +- index.ts | 82 ++++++++++++++++++++++++++++++-- package-lock.json | 8 ++-- package.json | 5 +- posts/_metadata.json | 2 +- posts/example.md | 6 +-- preview.ts | 56 ++++++++++++++++++++++ ryuji.ts | 77 ++++++++++++++++++++++++++---- saki.ts | 69 ++++----------------------- templates/components/return.html | 2 +- templates/index.html | 1 + templates/post.html | 10 +++- templates/tags.html | 21 ++++++++ tests.ts | 57 ++++++++++++++++++++++ 14 files changed, 312 insertions(+), 88 deletions(-) create mode 100644 preview.ts create mode 100644 templates/tags.html diff --git a/README.md b/README.md index ba982d9..1c9fdcd 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/index.ts b/index.ts index 29a920d..7e46103 100644 --- a/index.ts +++ b/index.ts @@ -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); diff --git a/package-lock.json b/package-lock.json index e09f438..da57d9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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==" } } } diff --git a/package.json b/package.json index 73cd6ee..306857c 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/posts/_metadata.json b/posts/_metadata.json index df5cf46..f4d4d05 100644 --- a/posts/_metadata.json +++ b/posts/_metadata.json @@ -5,6 +5,6 @@ "filename": "example", "date": "30/12/1999", "author": "Prussia", - "tags": [] + "tags": ["example", "fake", "please remember to remove"] } } \ No newline at end of file diff --git a/posts/example.md b/posts/example.md index 8c8913c..a9fcbf8 100644 --- a/posts/example.md +++ b/posts/example.md @@ -12,8 +12,8 @@ console.log("code block!!!"); ``` > ## woah a blockquote -> Yeehaw ^[1]^ +> Yeehaw ^\[1]^ -=== +---- -- [1]: Source? Me. +- \[1]: Source? Me. diff --git a/preview.ts b/preview.ts new file mode 100644 index 0000000..8a0b1d4 --- /dev/null +++ b/preview.ts @@ -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}`); diff --git a/ryuji.ts b/ryuji.ts index 504d88d..0fee5d6 100644 --- a/ryuji.ts +++ b/ryuji.ts @@ -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; diff --git a/saki.ts b/saki.ts index 30d7bce..c323b5a 100644 --- a/saki.ts +++ b/saki.ts @@ -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]); } } } diff --git a/templates/components/return.html b/templates/components/return.html index 1179c04..1872b30 100644 --- a/templates/components/return.html +++ b/templates/components/return.html @@ -1 +1 @@ -<- Get me outta here! \ No newline at end of file +<- Get me outta here! \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 58d4c74..83d47ef 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,6 +4,7 @@ prussia fan club +
diff --git a/templates/post.html b/templates/post.html index bdea0d1..59bf581 100644 --- a/templates/post.html +++ b/templates/post.html @@ -4,6 +4,7 @@ [[ post.title ]] + [[ component:return ]] @@ -11,7 +12,7 @@

[[ post.title ]]

- By [[ post.author ]] | [[ post.date ]] + By [[ post.author ]] | [[ post.date ]] [[ if:post.tags_exist ]]| [[ for:post.tags:tag:index:max ]][[ tag ]][[ if:index:!max ]], [[ endif ]][[ endfor ]][[ endif ]]
@@ -19,7 +20,12 @@ [[ html:post.html ]]
- [[ post.md ]] + [[ for:post.md_lines:line ]] + [[ line ]] + [[ if:line ]] +
+ [[ endif ]] + [[ endfor ]]
diff --git a/templates/tags.html b/templates/tags.html new file mode 100644 index 0000000..8076ad4 --- /dev/null +++ b/templates/tags.html @@ -0,0 +1,21 @@ + + + + + + "[[ tag ]]" posts + + + +
+

Search for posts with tag "[[ tag ]]"

+
+
+ +
+ + \ No newline at end of file diff --git a/tests.ts b/tests.ts index d4705f7..0075fff 100644 --- a/tests.ts +++ b/tests.ts @@ -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 ]][[ index_var ]][[ endif ]][[ endfor ]]", + { + trees: [ + "mango", + "oak", + "redwood", + "palm", + ], + } + ), + "123", + "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();