From 16e8326daca6ae594c8c3ec5392df57dc54fe15b Mon Sep 17 00:00:00 2001 From: Jonah Scheinerman Date: Sat, 18 May 2024 09:57:48 -0400 Subject: [PATCH] Compiled example docs + watch mode --- docs/examples/intro.ts | 17 ++++++ docs/examples/tsconfig.json | 9 ++++ docs/index.md | 15 +----- docsgen/emit.tsx | 102 ++++++++++++++++++++++++++++++++++++ docsgen/index.ts | 3 ++ docsgen/index.tsx | 100 ----------------------------------- docsgen/markdown.tsx | 36 ++++++++++++- docsgen/watch.tsx | 13 +++++ package.json | 8 ++- 9 files changed, 187 insertions(+), 116 deletions(-) create mode 100644 docs/examples/intro.ts create mode 100644 docs/examples/tsconfig.json create mode 100644 docsgen/emit.tsx create mode 100644 docsgen/index.ts delete mode 100644 docsgen/index.tsx create mode 100644 docsgen/watch.tsx diff --git a/docs/examples/intro.ts b/docs/examples/intro.ts new file mode 100644 index 0000000..b11b920 --- /dev/null +++ b/docs/examples/intro.ts @@ -0,0 +1,17 @@ +// START +import { Length, Measure, meters, seconds, Time, Velocity } from "safe-units"; + +const length: Length = Measure.of(30, meters); +const time: Time = Measure.of(15, seconds); +const velocity: Velocity = length.over(time); + +console.log(length.toString()); // 30 m +console.log(time.toString()); // 15 s +console.log(velocity.toString()); // 2 m * s^-1 + +// @ts-expect-error ERROR: A measure of m*s isn't assignable to a measure of m/s. +const error: Velocity = length.times(time); +// END + +// Ensure that error is used +console.log(error); diff --git a/docs/examples/tsconfig.json b/docs/examples/tsconfig.json new file mode 100644 index 0000000..ed48374 --- /dev/null +++ b/docs/examples/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "lib": ["es5"], + "baseUrl": ".", + "paths": { "safe-units": ["../../src"] } + } +} \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index e274fef..01631fb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,19 +4,8 @@ Safe Units is a type safe library for using units of measurement in TypeScript. Check it out on [github](https://github.com/jscheiny/safe-units). Safe Units provides an implementation of an SI based unit system but is flexible enough to allow users to create their own unit systems which can be independent or can interoperate with the built-in units. Users can also make unit systems for any numeric type they'd like not just the JavaScript `number` type. This library requires TypeScript 3.2 or higher. -```typescript -import { Length, Measure, meters, seconds, Time, Velocity } from "safe-units"; - -const length: Length = Measure.of(30, meters); -const time: Time = Measure.of(15, seconds); -const velocity: Velocity = length.over(time); - -console.log(length.toString()); // 30 m -console.log(time.toString()); // 15 s -console.log(velocity.toString()); // 2 m * s^-1 - -const error: Velocity = length.times(time); -// ERROR: A measure of m*s isn't assignable to a measure of m/s. +```example +intro.ts ``` **Features include:** diff --git a/docsgen/emit.tsx b/docsgen/emit.tsx new file mode 100644 index 0000000..3150eb9 --- /dev/null +++ b/docsgen/emit.tsx @@ -0,0 +1,102 @@ +import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs"; +import { basename, extname, join } from "path"; +import * as React from "react"; +import * as ReactDOMServer from "react-dom/server"; +import { getStyles } from "typestyle"; +import { Page } from "./page"; +import { PageModel } from "./pageModel"; + +const renderPageHtml = (title: string, body: string, inlineStyles: string, linkedStyles: string) => ` + + + + + ${title} + + + ${linkedStyles} + + + ${body} +`; + +export function emitDocs() { + const docsDir = "docs"; + const buildDir = join(docsDir, "build"); + if (!existsSync(buildDir)) { + mkdirSync(buildDir); + } + + const stylesDir = join(buildDir, "styles"); + if (!existsSync(stylesDir)) { + mkdirSync(stylesDir); + } + + const imagesContextPath = "images"; + const imagesInDir = join(docsDir, imagesContextPath); + const imagesOutDir = join(buildDir, imagesContextPath); + if (!existsSync(imagesOutDir)) { + mkdirSync(imagesOutDir); + } + + const pages = readdirSync("docs") + .filter(path => extname(path) === ".md") + .map(path => join("docs", path)) + .map(PageModel.from); + + const orderPath = join("docs", "order.txt"); + let order: string[] = []; + if (existsSync(orderPath) && !statSync(orderPath).isDirectory()) { + order = readFileSync(orderPath, "utf-8").split("\n"); + } + + function orderBy(order: string[]): (a: PageModel, b: PageModel) => number { + const getIndex = (page: PageModel) => { + if (page.name === "index") { + return -Infinity; + } + let index = order.indexOf(page.name); + if (index === -1) { + index = order.indexOf("*"); + } + return index === -1 ? Infinity : index; + }; + + return (a, b) => { + const aIndex = getIndex(a); + const bIndex = getIndex(b); + if (aIndex < bIndex) { + return -1; + } else if (aIndex > bIndex) { + return 1; + } else { + return a.title.toLowerCase() < b.title.toLowerCase() ? -1 : 1; + } + }; + } + + function buildLinkedStyles(inPath: string): string { + const name = basename(inPath); + const outPath = join(stylesDir, name); + copyFileSync(inPath, outPath); + return ``; + } + + const linkedStyles = ["docs/styles/highlight.css", "node_modules/normalize.css/normalize.css"] + .map(buildLinkedStyles) + .join("\n"); + + readdirSync(imagesInDir).forEach(path => { + const inPath = join(imagesInDir, path); + const outPath = join(imagesOutDir, path); + copyFileSync(inPath, outPath); + }); + + pages.sort(orderBy(order)); + pages.forEach((page, index) => { + const body = ReactDOMServer.renderToString(); + const title = page.name === "index" ? page.title : `${page.title} | Safe Units`; + const html = renderPageHtml(title, body, getStyles(), linkedStyles); + writeFileSync(join(buildDir, page.path), html); + }); +} diff --git a/docsgen/index.ts b/docsgen/index.ts new file mode 100644 index 0000000..01a6cbd --- /dev/null +++ b/docsgen/index.ts @@ -0,0 +1,3 @@ +import { emitDocs } from "./emit"; + +emitDocs(); diff --git a/docsgen/index.tsx b/docsgen/index.tsx deleted file mode 100644 index f547b74..0000000 --- a/docsgen/index.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs"; -import { basename, extname, join } from "path"; -import * as React from "react"; -import * as ReactDOMServer from "react-dom/server"; -import { getStyles } from "typestyle"; -import { Page } from "./page"; -import { PageModel } from "./pageModel"; - -const renderPageHtml = (title: string, body: string, inlineStyles: string, linkedStyles: string) => ` - - - - - ${title} - - - ${linkedStyles} - - - ${body} -`; - -const docsDir = "docs"; -const buildDir = join(docsDir, "build"); -if (!existsSync(buildDir)) { - mkdirSync(buildDir); -} - -const stylesDir = join(buildDir, "styles"); -if (!existsSync(stylesDir)) { - mkdirSync(stylesDir); -} - -const imagesContextPath = "images"; -const imagesInDir = join(docsDir, imagesContextPath); -const imagesOutDir = join(buildDir, imagesContextPath); -if (!existsSync(imagesOutDir)) { - mkdirSync(imagesOutDir); -} - -const pages = readdirSync("docs") - .filter(path => extname(path) === ".md") - .map(path => join("docs", path)) - .map(PageModel.from); - -const orderPath = join("docs", "order.txt"); -let order: string[] = []; -if (existsSync(orderPath) && !statSync(orderPath).isDirectory()) { - order = readFileSync(orderPath, "utf-8").split("\n"); -} - -function orderBy(order: string[]): (a: PageModel, b: PageModel) => number { - const getIndex = (page: PageModel) => { - if (page.name === "index") { - return -Infinity; - } - let index = order.indexOf(page.name); - if (index === -1) { - index = order.indexOf("*"); - } - return index === -1 ? Infinity : index; - }; - - return (a, b) => { - const aIndex = getIndex(a); - const bIndex = getIndex(b); - if (aIndex < bIndex) { - return -1; - } else if (aIndex > bIndex) { - return 1; - } else { - return a.title.toLowerCase() < b.title.toLowerCase() ? -1 : 1; - } - }; -} - -function buildLinkedStyles(inPath: string): string { - const name = basename(inPath); - const outPath = join(stylesDir, name); - copyFileSync(inPath, outPath); - return ``; -} - -const linkedStyles = ["docs/styles/highlight.css", "node_modules/normalize.css/normalize.css"] - .map(buildLinkedStyles) - .join("\n"); - -readdirSync(imagesInDir).forEach(path => { - const inPath = join(imagesInDir, path); - const outPath = join(imagesOutDir, path); - copyFileSync(inPath, outPath); -}); - -pages.sort(orderBy(order)); -pages.forEach((page, index) => { - const body = ReactDOMServer.renderToString(); - const title = page.name === "index" ? page.title : `${page.title} | Safe Units`; - const html = renderPageHtml(title, body, getStyles(), linkedStyles); - writeFileSync(join(buildDir, page.path), html); -}); diff --git a/docsgen/markdown.tsx b/docsgen/markdown.tsx index f1a2e65..cd577c1 100644 --- a/docsgen/markdown.tsx +++ b/docsgen/markdown.tsx @@ -3,6 +3,8 @@ import highlight from "highlight.js"; import * as React from "react"; import { createNodeId, getNodeText } from "./markdownUtils"; import { component } from "./style"; +import { join } from "path"; +import { existsSync, readFileSync } from "fs"; interface MarkdownProps { root: Node; @@ -44,7 +46,8 @@ export const Markdown: MarkdownComponent = ({ root }) => { case "thematic_break": return
; case "code_block": { - const highlightedCode = highlight.highlight(root.literal ?? "", { language: "typescript" }).value; + const text = getCodeBlockText(root); + const highlightedCode = highlight.highlight(text, { language: "typescript" }).value; return (
                     
@@ -140,6 +143,37 @@ function getHeadingTag(level: number): keyof JSX.IntrinsicElements {
     }
 }
 
+function getCodeBlockText(root: Node): string {
+    if (root.info !== "example") {
+        return root.literal ?? "";
+    }
+
+    if (root.literal == null) {
+        throw new Error("Expected example code block to have a reference to an example file.");
+    }
+
+    const examplePath = join("docs", "examples", root.literal.trim());
+    console.log(examplePath);
+    if (!existsSync(examplePath)) {
+        throw new Error(`Example file not found: ${examplePath}`);
+    }
+
+    const contents = readFileSync(examplePath, "utf-8");
+
+    const lines = contents.split("\n");
+    const startLine = lines.findIndex(line => EXAMPLE_START_REGEX.test(line));
+    const endLine = lines.findIndex(line => EXAMPLE_END_REGX.test(line));
+
+    if (startLine === -1 || endLine === -1) {
+        return contents;
+    }
+
+    return lines.slice(startLine + 1, endLine).join("\n");
+}
+
+const EXAMPLE_START_REGEX = /^\/\/\s+start\s*/gi;
+const EXAMPLE_END_REGX = /^\/\/\s+end\s*/gi;
+
 const CodeBlock = component("code-block", "code", {
     borderRadius: 3,
     $nest: {
diff --git a/docsgen/watch.tsx b/docsgen/watch.tsx
new file mode 100644
index 0000000..b875758
--- /dev/null
+++ b/docsgen/watch.tsx
@@ -0,0 +1,13 @@
+import { watch } from "fs";
+import { emitDocs } from "./emit";
+
+console.log("Generating initial docs...");
+emitDocs();
+console.log("Watching for docs changes...");
+watch("docs", {}, (_eventType, filename) => {
+    if (filename?.endsWith(".md")) {
+        console.log(`Change detected in ${filename}. Rebuilding docs...`);
+        emitDocs();
+        console.log("Done");
+    }
+});
diff --git a/package.json b/package.json
index 1d6048e..7d1b11e 100644
--- a/package.json
+++ b/package.json
@@ -23,13 +23,17 @@
         "build": "tsc -p src",
         "clean": "rimraf dist docs/build",
         "compile:docs": "tsc -p docsgen",
-        "docs": "npm-run-all -s compile:docs node:docs",
+        "compile:examples": "tsc -p docs/examples",
+        "docs": "npm-run-all -s compile:docs compile:examples node:docs",
+        "docs:watch": "npm-run-all -s compile:docs compile:examples node:docs:watch",
         "lint:docs": "eslint --config eslint.config.mjs docsgen",
+        "lint:examples": "eslint --config eslint.config.mjs docs/examples",
         "lint:src": "eslint --config eslint.config.mjs src",
         "lint:dist": "./scripts/check-typings.sh",
         "lint:test": "eslint --config eslint.config.mjs test",
-        "lint": "npm-run-all -p lint:docs lint:src lint:dist",
+        "lint": "npm-run-all -p lint:docs lint:examples lint:src lint:dist lint:test",
         "node:docs": "node dist/docsgen/index",
+        "node:docs:watch": "node dist/docsgen/watch",
         "test:src": "jest --config jest.config.ts",
         "test:types": "tsc -p test",
         "test": "npm-run-all -p test:src test:types",