A design system in a box.
hip-ui.tngl.io/docs/introduction
1import { glob } from "glob";
2import { JSDOM } from "jsdom";
3import { NodeHtmlMarkdown } from "node-html-markdown";
4import { readFile, rm, writeFile } from "node:fs/promises";
5import path from "node:path";
6
7const distClientDir = path.resolve("dist/client");
8
9function getLanguage(node) {
10 const codeNode = node.querySelector("code[class*='language-']");
11 const className = codeNode?.getAttribute("class") ?? "";
12 const match = className.match(/language-([\w-]+)/);
13
14 return match?.[1] ?? "";
15}
16
17function escapeTableCellPipes(text) {
18 return text.replaceAll("|", String.raw`\|`);
19}
20
21function normalizeInlineWhitespace(text) {
22 return text.replaceAll(/\s+/g, " ").trim();
23}
24
25function normalizeCodeBlocks(markdownRoot) {
26 const codeBlocks = markdownRoot.querySelectorAll("pre");
27
28 for (const codeBlock of codeBlocks) {
29 const code = codeBlock.textContent?.replace(/\n$/, "") ?? "";
30 const language = getLanguage(codeBlock);
31 const normalizedCodeBlock = markdownRoot.ownerDocument.createElement("pre");
32 const normalizedCode = markdownRoot.ownerDocument.createElement("code");
33
34 if (language) {
35 normalizedCode.className = `language-${language}`;
36 }
37
38 normalizedCode.textContent = code;
39 normalizedCodeBlock.replaceChildren(normalizedCode);
40 codeBlock.replaceWith(normalizedCodeBlock);
41 }
42}
43
44function normalizeTables(markdownRoot) {
45 const cells = markdownRoot.querySelectorAll("th, td");
46
47 for (const cell of cells) {
48 const clone = cell.cloneNode(true);
49
50 for (const codeNode of clone.querySelectorAll("pre, code")) {
51 const codeText = normalizeInlineWhitespace(codeNode.textContent ?? "");
52 codeNode.replaceWith(`\`${escapeTableCellPipes(codeText)}\``);
53 }
54
55 cell.textContent = escapeTableCellPipes(
56 normalizeInlineWhitespace(clone.textContent ?? ""),
57 );
58 }
59}
60
61function normalizeMarkdown(markdown) {
62 return markdown
63 .replaceAll(/\n{3,}/g, "\n\n")
64 .trim()
65 .concat("\n");
66}
67
68const markdownConverter = new NodeHtmlMarkdown({
69 bulletMarker: "-",
70 codeBlockStyle: "fenced",
71});
72
73async function exportMarkdownFile(htmlFilePath) {
74 const html = await readFile(htmlFilePath, "utf8");
75 const dom = new JSDOM(html);
76 const markdownRoot = dom.window.document.querySelector(
77 "[data-markdown-export]",
78 );
79
80 if (!markdownRoot) {
81 throw new Error(`Missing [data-markdown-export] in ${htmlFilePath}`);
82 }
83
84 normalizeCodeBlocks(markdownRoot);
85 normalizeTables(markdownRoot);
86
87 const markdown = normalizeMarkdown(
88 markdownConverter.translate(markdownRoot.innerHTML),
89 );
90 const markdownDirectoryPath = path.dirname(htmlFilePath);
91
92 await rm(markdownDirectoryPath, { force: true, recursive: true });
93 await writeFile(markdownDirectoryPath, markdown, "utf8");
94}
95
96async function main() {
97 const htmlFilePaths = await glob("**/*.md/index.html", {
98 absolute: true,
99 cwd: distClientDir,
100 });
101
102 if (htmlFilePaths.length === 0) {
103 throw new Error(`No markdown HTML exports found in ${distClientDir}`);
104 }
105
106 await Promise.all(
107 htmlFilePaths.map((htmlFilePath) => exportMarkdownFile(htmlFilePath)),
108 );
109
110 console.log(`Exported ${htmlFilePaths.length} markdown files.`);
111}
112
113await main();