Basic Markdown to HTML generation

This commit is contained in:
pfych 2025-04-05 17:38:22 +11:00
parent 6c0f9f5e7c
commit c473ed5412
9 changed files with 246 additions and 55 deletions

View File

@ -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,

View File

@ -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: {}

8
src/blog/post.md Normal file
View File

@ -0,0 +1,8 @@
---
title: Hello World
template: blog
---
# Post
Hello World!

View File

@ -0,0 +1,3 @@
.blog {
background-color: red;
}

15
src/templates/blog.tsx Normal file
View File

@ -0,0 +1,15 @@
import styles from './blog.module.scss';
interface Props {
children: HTMLElement;
}
export default function Blog(props: Props) {
const { children } = props;
return (
<div className={styles.blog}>
<h1>Blog Post</h1>
<article>{children}</article>
</div>
);
}

68
ssg/build-blog.tsx Normal file
View File

@ -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(
<Root title="My App" stylePaths={stylePaths} scriptPaths={[]}>
{html}
</Root>,
{ parser: 'html' },
);
writeFileSync(blogOutputHtmlPath, formattedHtml);
};

43
ssg/build-page.tsx Normal file
View File

@ -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(
<Root title="My App" stylePaths={stylePaths} scriptPaths={[]}>
{html}
</Root>,
{ parser: 'html' },
);
writeFileSync(pageOutputHtmlPath, formattedHtml);
};

View File

@ -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(
<Root title="My App" stylePaths={stylePaths} scriptPaths={[]}>
{html}
</Root>,
{ parser: 'html' },
);
writeFileSync(outputHtmlPath, formattedHtml);
}),
);
await Promise.all([...pages.map(buildPage), ...blogs.map(buildBlog)]);
rmSync('.tmp', { recursive: true });
})();

View File

@ -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 };
};