From c473ed54120182e81e738d68f4f9208222e11c91 Mon Sep 17 00:00:00 2001 From: pfych Date: Sat, 5 Apr 2025 17:38:22 +1100 Subject: [PATCH] Basic Markdown to HTML generation --- package.json | 14 +++--- pnpm-lock.yaml | 87 ++++++++++++++++++++++++++++++++++ src/blog/post.md | 8 ++++ src/templates/blog.module.scss | 3 ++ src/templates/blog.tsx | 15 ++++++ ssg/build-blog.tsx | 68 ++++++++++++++++++++++++++ ssg/build-page.tsx | 43 +++++++++++++++++ ssg/build.tsx | 50 +++---------------- ssg/utils.ts | 13 +++-- 9 files changed, 246 insertions(+), 55 deletions(-) create mode 100644 src/blog/post.md create mode 100644 src/templates/blog.module.scss create mode 100644 src/templates/blog.tsx create mode 100644 ssg/build-blog.tsx create mode 100644 ssg/build-page.tsx diff --git a/package.json b/package.json index 555a2a3..de83e55 100644 --- a/package.json +++ b/package.json @@ -14,18 +14,20 @@ "@kitajs/html": "4.2.7", "@kitajs/ts-html-plugin": "4.1.1", "@types/node": "22.14.0", + "@typescript-eslint/eslint-plugin": "7.0.2", + "@typescript-eslint/parser": "7.0.2", + "@typescript-eslint/typescript-estree": "8.22.0", "esbuild": "0.25.2", "esbuild-sass-plugin": "3.3.1", - "glob": "11.0.1", - "prettier": "3.5.3", - "typescript": "5.8.3", "eslint": "8.56.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "2.29.1", "eslint-plugin-prettier": "5.1.3", - "@typescript-eslint/typescript-estree": "8.22.0", - "@typescript-eslint/eslint-plugin": "7.0.2", - "@typescript-eslint/parser": "7.0.2" + "glob": "11.0.1", + "gray-matter": "4.0.3", + "marked": "15.0.7", + "prettier": "3.5.3", + "typescript": "5.8.3" }, "prettier": { "semi": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cac2b0..9fbf5b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,12 @@ importers: glob: specifier: 11.0.1 version: 11.0.1 + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 + marked: + specifier: ^15.0.7 + version: 15.0.7 prettier: specifier: 3.5.3 version: 3.5.3 @@ -469,6 +475,9 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -764,6 +773,11 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -780,6 +794,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -891,6 +909,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -976,6 +998,10 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1058,6 +1084,10 @@ packages: resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} engines: {node: 20 || >=22} + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1078,6 +1108,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1093,6 +1127,11 @@ packages: resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} engines: {node: 20 || >=22} + marked@15.0.7: + resolution: {integrity: sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1422,6 +1461,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1479,6 +1522,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1507,6 +1553,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -2010,6 +2060,10 @@ snapshots: ansi-styles@6.2.1: {} + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} array-buffer-byte-length@1.0.2: @@ -2439,6 +2493,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 3.4.3 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -2451,6 +2507,10 @@ snapshots: esutils@2.0.3: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -2590,6 +2650,13 @@ snapshots: graphemer@1.4.0: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -2676,6 +2743,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -2753,6 +2822,11 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -2771,6 +2845,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kind-of@6.0.3: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -2784,6 +2860,8 @@ snapshots: lru-cache@11.1.0: {} + marked@15.0.7: {} + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -3084,6 +3162,11 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.1 + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver@6.3.1: {} semver@7.7.1: {} @@ -3150,6 +3233,8 @@ snapshots: source-map-js@1.2.1: {} + sprintf-js@1.0.3: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -3193,6 +3278,8 @@ snapshots: dependencies: ansi-regex: 6.1.0 + strip-bom-string@1.0.0: {} + strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} diff --git a/src/blog/post.md b/src/blog/post.md new file mode 100644 index 0000000..4cb682e --- /dev/null +++ b/src/blog/post.md @@ -0,0 +1,8 @@ +--- +title: Hello World +template: blog +--- + +# Post + +Hello World! diff --git a/src/templates/blog.module.scss b/src/templates/blog.module.scss new file mode 100644 index 0000000..5e50151 --- /dev/null +++ b/src/templates/blog.module.scss @@ -0,0 +1,3 @@ +.blog { + background-color: red; +} diff --git a/src/templates/blog.tsx b/src/templates/blog.tsx new file mode 100644 index 0000000..d1c7a69 --- /dev/null +++ b/src/templates/blog.tsx @@ -0,0 +1,15 @@ +import styles from './blog.module.scss'; + +interface Props { + children: HTMLElement; +} + +export default function Blog(props: Props) { + const { children } = props; + return ( +
+

Blog Post

+
{children}
+
+ ); +} diff --git a/ssg/build-blog.tsx b/ssg/build-blog.tsx new file mode 100644 index 0000000..81255b0 --- /dev/null +++ b/ssg/build-blog.tsx @@ -0,0 +1,68 @@ +import { readFile } from 'node:fs/promises'; +import matter from 'gray-matter'; +import { parse } from 'marked'; +import { getPaths, getStylePath } from './utils'; +import { build } from 'esbuild'; +import { sassPlugin } from 'esbuild-sass-plugin'; +import { format } from 'prettier'; +import Root from './root'; +import { writeFileSync } from 'node:fs'; +import { resolve } from 'path'; +import crypto from 'node:crypto'; + +export const buildBlog = async (blog: string) => { + console.log(`Building ${blog}`); + const fileContent = await readFile(blog, 'utf-8'); + const content = matter(fileContent); + const postHtml = parse(content.content); + + const sha = crypto.createHash('sha1').update(fileContent).digest('hex'); + + const templateFile = content.data.template; + const templatePath = resolve(`./src/templates/${templateFile}.tsx`); + + const blogFileName = blog.replace('src/blog/', '').replace('.md', ''); + + const tmpJsPath = templatePath + .replace('src', '.tmp') + .replace(content.data.template, sha) + .replace('.tsx', '.js'); + + const blogOutputHtmlPath = templatePath + .replace('src/templates', '.out') + .replace(content.data.template, blogFileName) + .replace('.tsx', '.html'); + + await build({ + entryPoints: [templatePath], + bundle: true, + outfile: tmpJsPath, + jsxImportSource: '@kitajs/html', + minify: true, + platform: 'node', + external: ['esbuild'], + plugins: [ + sassPlugin({ + filter: /\.module\.scss$/, + type: 'local-css', + }), + sassPlugin({ + filter: /\.scss$/, + type: 'css', + }), + ], + }); + + const { default: Page } = await import(tmpJsPath); + const html = Page.default({ children: postHtml }); + let stylePaths: string[] = getStylePath(tmpJsPath, blogOutputHtmlPath); + + const formattedHtml = await format( + + {html} + , + { parser: 'html' }, + ); + + writeFileSync(blogOutputHtmlPath, formattedHtml); +}; diff --git a/ssg/build-page.tsx b/ssg/build-page.tsx new file mode 100644 index 0000000..a769c95 --- /dev/null +++ b/ssg/build-page.tsx @@ -0,0 +1,43 @@ +import { getPaths, getStylePath } from './utils'; +import { build } from 'esbuild'; +import { sassPlugin } from 'esbuild-sass-plugin'; +import { format } from 'prettier'; +import Root from './root'; +import { writeFileSync } from 'node:fs'; + +export const buildPage = async (page: string) => { + const { pagePath, tmpJsPath, pageOutputHtmlPath } = getPaths(page); + + await build({ + entryPoints: [pagePath], + bundle: true, + outfile: tmpJsPath, + jsxImportSource: '@kitajs/html', + minify: true, + platform: 'node', + external: ['esbuild'], + plugins: [ + sassPlugin({ + filter: /\.module\.scss$/, + type: 'local-css', + }), + sassPlugin({ + filter: /\.scss$/, + type: 'css', + }), + ], + }); + + const { default: Page } = await import(tmpJsPath); + const html = Page.default(); + let stylePaths: string[] = getStylePath(tmpJsPath, pageOutputHtmlPath); + + const formattedHtml = await format( + + {html} + , + { parser: 'html' }, + ); + + writeFileSync(pageOutputHtmlPath, formattedHtml); +}; diff --git a/ssg/build.tsx b/ssg/build.tsx index 3693d49..c2310b5 100644 --- a/ssg/build.tsx +++ b/ssg/build.tsx @@ -1,54 +1,16 @@ import { glob } from 'glob'; -import { build } from 'esbuild'; -import { mkdirSync, writeFileSync, rmSync } from 'node:fs'; -import { sassPlugin } from 'esbuild-sass-plugin'; -import Root from './root'; -import { getPaths, getStylePath, outDir } from './utils'; -import { format } from 'prettier'; +import { mkdirSync, rmSync } from 'node:fs'; +import { outDir } from './utils'; +import { buildPage } from './build-page'; +import { buildBlog } from './build-blog'; mkdirSync(outDir, { recursive: true }); void (async () => { const pages = await glob('src/pages/**/*.tsx'); + const blogs = await glob('src/blog/**/*.md'); - await Promise.all( - pages.map(async (page) => { - const { pagePath, tmpJsPath, outputHtmlPath } = getPaths(page); - - await build({ - entryPoints: [pagePath], - bundle: true, - outfile: tmpJsPath, - jsxImportSource: '@kitajs/html', - minify: true, - platform: 'node', - external: ['esbuild'], - plugins: [ - sassPlugin({ - filter: /\.module\.scss$/, - type: 'local-css', - }), - sassPlugin({ - filter: /\.scss$/, - type: 'css', - }), - ], - }); - - const { default: Page } = await import(tmpJsPath); - const html = Page.default(); - let stylePaths: string[] = getStylePath(tmpJsPath, outputHtmlPath); - - const formattedHtml = await format( - - {html} - , - { parser: 'html' }, - ); - - writeFileSync(outputHtmlPath, formattedHtml); - }), - ); + await Promise.all([...pages.map(buildPage), ...blogs.map(buildBlog)]); rmSync('.tmp', { recursive: true }); })(); diff --git a/ssg/utils.ts b/ssg/utils.ts index 9a59df1..a6c2431 100644 --- a/ssg/utils.ts +++ b/ssg/utils.ts @@ -3,17 +3,17 @@ import { resolve } from 'path'; export const outDir = resolve('.out'); -export const getStylePath = (tmpJsPath: string, outputHtmlPath: string) => { +export const getStylePath = (tmpJsPath: string, pageOutputHtmlPath: string) => { let stylePaths: string[] = []; if (existsSync(tmpJsPath.replace('.js', '.css'))) { cpSync( tmpJsPath.replace('.js', '.css'), - outputHtmlPath.replace('.html', '.css'), + pageOutputHtmlPath.replace('.html', '.css'), ); stylePaths.push( - outputHtmlPath.replace('.html', '.css').replace(outDir, '.'), + pageOutputHtmlPath.replace('.html', '.css').replace(outDir, ''), ); } @@ -23,9 +23,12 @@ export const getStylePath = (tmpJsPath: string, outputHtmlPath: string) => { export const getPaths = (rawPath: string) => { const pagePath = resolve(rawPath); const tmpJsPath = pagePath.replace('src', '.tmp').replace('.tsx', '.js'); - const outputHtmlPath = pagePath + const pageOutputHtmlPath = pagePath .replace('src/pages', '.out') .replace('.tsx', '.html'); + const blogOutputHtmlPath = pagePath + .replace('src/templates', '.out') + .replace('.tsx', '.html'); - return { pagePath, tmpJsPath, outputHtmlPath }; + return { pagePath, tmpJsPath, pageOutputHtmlPath, blogOutputHtmlPath }; };