diff --git a/README.md b/README.md index 5ea905d5f68264d8fac898859d36c14fe94e050b..af7172ea08bc7d9e9a895a9bf638f714044a2c78 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ leveraging the existing ecosystem. > - [Paragraph Detection](#paragraph-detection) > - [Static Statements](#static-statements) > - [Config](#config) +> - [Frontmatter Layout Configuration](#frontmatter-layout-configuration) Here's a glance at how things look. Complete example documents in <./examples> diff --git a/lib/jsr.json b/lib/jsr.json index b7be3b5673a4857b728d4ebca2f56488ced2a87f..04b30a33a98c0abfe45033d7c5ff23e062d82616 100644 --- a/lib/jsr.json +++ b/lib/jsr.json @@ -1,6 +1,6 @@ { "name": "@clo/markodown", - "version": "1.0.0-rc.6", + "version": "1.0.0-rc.9", "license": "ISC", "exports": { ".": "./mod.ts", diff --git a/lib/mod.ts b/lib/mod.ts index 83d9fcb8eaa00b227c22e4d518dee171c308d411..8cccadd82293b66e2f4a32a5878adb5ef06a1f5f 100644 --- a/lib/mod.ts +++ b/lib/mod.ts @@ -3,9 +3,63 @@ import bytes from "./bindgen/wasm_bytes.js"; wasm.initSync({ module: bytes }); -type Transformed = Success | Failure; +/** The result of a Markodown transform */ +export type Transformed = Success | Failure; +/** Convert Markodown (`.mdo`) source code into Marko source code (`.marko`) */ +export function transform(options: TransformOptions): Transformed { + const formats = options.format ?? ["marko"]; + let forceFormat = null; + if (formats.includes("html") && !formats.includes("marko")) { + forceFormat = wasm.OutputFormat.Html; + } else if (formats.includes("marko") && !formats.includes("html")) { + forceFormat = wasm.OutputFormat.Marko; + } else if (!formats.includes("html") && !formats.includes("marko")) { + throw new Error("No supported formats in " + JSON.stringify(formats)); + } + + return wasm.transform( + options.source, + forceFormat, + options?.layoutImport, + options?.componentImports, + options?.selfImport, + options?.markdownOnly, + options?.cloverExtensions, + ); +} + +/** Converts a flat document outline into a nested tree. */ +export function outlineToTree(outline: Heading[]): HeadingTree[] { + const root: HeadingTree[] = []; + const stack: HeadingTree[] = []; + + for (const heading of outline) { + if (heading.level === 1) continue; + + const node: HeadingTree = { ...heading, children: [] }; + + while ( + stack.length && stack[stack.length - 1].level >= heading.level + ) { + stack.pop(); + } + + if (stack.length) { + stack[stack.length - 1].children.push(node); + } else { + root.push(node); + } + + stack.push(node); + } + + return root; +} + +/** Options for {@linkcode transform}. */ export interface TransformOptions { + /** The Markodown source code to be transformed */ source: string; /** * Specify the allowed output formats. For simplicity, pass `marko`. By @@ -29,7 +83,8 @@ export interface TransformOptions { markdownOnly?: boolean; /** * These extensions are special-cased so that Clover can re-use this on - * her website without + * her website without maintaining a second markdown parser. I promise + * we are not wasting your bundle size on my features. */ cloverExtensions?: CloverQuestionExtensions; } @@ -40,7 +95,7 @@ export interface ComponentImports { heading?: string; /** Replace `pre > code` with this import. */ codeBlock?: string; - /** Replace markdown links with this import. */ + /** Replace links with this import. */ link?: string; /** Replace images with this import. */ image?: string; @@ -56,6 +111,8 @@ export interface ComponentImports { * emit custom HTML elements instead of imported components. * * Also includes `@html ` block syntax for raw HTML passthrough. + * + * @internal */ export interface CloverQuestionExtensions { /** @@ -85,61 +142,89 @@ export interface CloverQuestionExtensions { labelledRedaction: string; } +/** The transformer currently supports two output formats. */ export type OutputFormat = "marko" | "html"; +/** The transform is a success when `success: true` or there are no errors. */ export interface Success { + /** Easy boolean to discriminate {@linkcode TransformResult} */ success: true; + /** The transformed text. Format is determined by `format` */ text: string; - errors: []; + /** The resolved output format of `text` */ format: OutputFormat; + /** List of errors, if any */ + errors: []; } +/** The transform is a success when `success: false` or there is at least one error. */ export interface Failure { + /** Easy boolean to discriminate {@linkcode TransformResult} */ success: false; + /** The transformed text. Format is determined by `format` */ text: null; - errors: TransformError[]; - outline: Heading[] | null; + /** The resolved output format of `text` */ + format: null; + /** List of errors, if any */ + errors: [TransformError, ...TransformError[]]; } +/** This is passed to Markodown layouts */ +export interface LayoutInput { + /** + * Scanned from heading tags, the document outline is provided flat here. You + * can convert it into a nested tree with {@linkcode outlineToTree} + */ + outline: Heading[]; + /** + * A copy of the Module Namespace object of the page. Use this to reflect + * frontmatter or other customizable exports. Don't render `module.default` as + * a component, since that will recursively call this layout. + */ + module: Record; + /** The rendered document */ + // @ts-ignore fails if marko types not chilling + content: Marko.Body; +} + +/** + * Scanned from heading tags, this represents one heading in the document. You + * can convert it into a nested tree with {@linkcode outlineToTree} + */ export interface Heading { + /** Which header element this corresponds to. */ level: 1 | 2 | 3 | 4 | 5 | 6; + /** The link ID. Derived from the `id` attribute of the heading, or generated for you otherwise. */ id: string; + /** The rendered heading name */ // @ts-ignore fails if marko types not chilling content: Marko.Body; } +/** Generated by {@linkcode outlineToTree} */ +export interface HeadingTree extends Heading { + /** Sub-headings */ + children: HeadingTree[]; +} + export interface TransformError { + /** What went wrong? */ message: string; - labels: LabelledSpan[]; + /** Additional notes for the failure */ + notes: TransformNote[]; + /** One-based line */ line: number; + /** One-based column, byte offset */ column: number; } -export interface LabelledSpan { - message: string; +export interface TransformNote { + /** What this span is communicating */ + message?: string | null; + /** One-based line */ line: number; + /** One-based column, byte offset */ column: number; + /** Byte length */ width: number; } - -export function transform(options: TransformOptions): Transformed { - const formats = options.format ?? ["marko"]; - let forceFormat = null; - if (formats.includes("html") && !formats.includes("marko")) { - forceFormat = wasm.OutputFormat.Html; - } else if (formats.includes("marko") && !formats.includes("html")) { - forceFormat = wasm.OutputFormat.Marko; - } else if (!formats.includes("html") && !formats.includes("marko")) { - throw new Error("No supported formats in " + JSON.stringify(formats)); - } - - return wasm.transform( - options.source, - forceFormat, - options?.layoutImport, - options?.componentImports, - options?.selfImport, - options?.markdownOnly, - options?.cloverExtensions, - ); -} diff --git a/npm.sh b/npm.sh deleted file mode 100644 index b02dc80b2e4628ec2be7e113869d7f5182790fca..0000000000000000000000000000000000000000 --- a/npm.sh +++ /dev/null @@ -1,17 +0,0 @@ -set -e - -rm -rf lib/dist-npm -mkdir lib/dist-npm - -cd lib -VERSION="$(cat jsr.json | jq .version -r)" - -cd dist-npm -echo '{}' > package.json -npx jsr add "@clo/markodown@$VERSION" - -cd node_modules/@clo/markodown -rm jsr.json -sed "s/VERSION/$VERSION/g" ../../../../package.npm.json > package.json - -npm publish --tag rc diff --git a/publish.sh b/publish.sh new file mode 100644 index 0000000000000000000000000000000000000000..3f58604a676c57568524b013ec1f34598bdd57a0 --- /dev/null +++ b/publish.sh @@ -0,0 +1,24 @@ +set -e + +cargo test --all +bash wasm.sh + +cd lib +cp ../README.md README.md + +npx jsr publish --allow-dirty + +rm -rf dist-npm +mkdir dist-npm + +VERSION="$(cat jsr.json | jq .version -r)" + +cd dist-npm +echo '{}' > package.json +npx jsr add "@clo/markodown@$VERSION" + +cd node_modules/@clo/markodown +rm jsr.json +sed "s/VERSION/$VERSION/g" ../../../../package.npm.json > package.json + +npm publish --tag rc diff --git a/src/component_transforms.rs b/src/component_transforms.rs index 27c4e4727061ce90359d8cce8555fa1853136696..3f7d3d6cb2021514825e38ffa1f6c171407f1e86 100644 --- a/src/component_transforms.rs +++ b/src/component_transforms.rs @@ -150,7 +150,7 @@ pub fn generate_layout_boilerplate(used: &UsedElements) -> String { // Heading — always present when a layout is active out.push_str(concat!( "\n", - " <${'h' + level} ...attrs><${content}>\n", + " <${'h' + level} ...attrs><${content} />\n", "\n", "\n", )); diff --git a/src/wasm.rs b/src/wasm.rs index d3a4c4dc63799a8e08297317a24fb7a5c13498d5..124791fa05bfde93d99971ebef51b7005e2fdc2c 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -14,7 +14,7 @@ struct TransformResult { #[derive(Serialize)] struct WasmDiagnostic { message: String, - labels: Vec, + notes: Vec, line: u32, column: u32, } @@ -89,7 +89,7 @@ fn convert_diagnostic(src: &str, diag: &OxcDiagnostic) -> WasmDiagnostic { WasmDiagnostic { message: diag.message.to_string(), - labels: forward_labels, + notes: forward_labels, line, column, } diff --git a/tests/fixtures/23-outline-extracting.marko b/tests/fixtures/23-outline-extracting.marko index 8d815d7efec851621fe5d1f0e579daf29b4854a8..e0bf48430eadbee57cd65ec12ccb804c0f710928 100644 --- a/tests/fixtures/23-outline-extracting.marko +++ b/tests/fixtures/23-outline-extracting.marko @@ -1,7 +1,7 @@ import Layout__markodown__ from "./layout.marko"; import * as LayoutModule__markodown__ from "./layout.marko"; - <${'h' + level} ...attrs><${content}> + <${'h' + level} ...attrs><${content} /> diff --git a/tests/fixtures/27-frontmatter-layout.marko b/tests/fixtures/27-frontmatter-layout.marko index 6b3ba25886b7f52964dd800bfbf6372c01721675..395515393b30c0a0ca944e5dcacf65e01c9412cc 100644 --- a/tests/fixtures/27-frontmatter-layout.marko +++ b/tests/fixtures/27-frontmatter-layout.marko @@ -1,7 +1,7 @@ import Layout__markodown__ from "./page-layout.marko"; import * as LayoutModule__markodown__ from "./page-layout.marko"; - <${'h' + level} ...attrs><${content}> + <${'h' + level} ...attrs><${content} /> diff --git a/tests/fixtures/28-outline-self-import.marko b/tests/fixtures/28-outline-self-import.marko index be52fe1be55dce493aaa9134340b7a66bcb6a7d8..f4a97bf974dac17e76f79c7bf4331e1b7aeb6e58 100644 --- a/tests/fixtures/28-outline-self-import.marko +++ b/tests/fixtures/28-outline-self-import.marko @@ -2,7 +2,7 @@ import Layout__markodown__ from "./layout.marko"; import * as LayoutModule__markodown__ from "./layout.marko"; import * as self__markodown__ from "./self.marko"; - <${'h' + level} ...attrs><${content}> + <${'h' + level} ...attrs><${content} /> diff --git a/tests/fixtures/29-layout-all-components.marko b/tests/fixtures/29-layout-all-components.marko index e820151fa1d0a8ab0bc759022f91ec40ae780de4..018269f82c441d7f7bbff00f60d1a7201b76f8fe 100644 --- a/tests/fixtures/29-layout-all-components.marko +++ b/tests/fixtures/29-layout-all-components.marko @@ -1,7 +1,7 @@ import Layout__markodown__ from "./layout.marko"; import * as LayoutModule__markodown__ from "./layout.marko"; - <${'h' + level} ...attrs><${content}> + <${'h' + level} ...attrs><${content} /> diff --git a/tests/fixtures/30-layout-selective-components.marko b/tests/fixtures/30-layout-selective-components.marko index e2a380c902b695f20203117ccb6c662b5e99f4cb..4c34dbbe61f7551771aa956753c5f6dc15106345 100644 --- a/tests/fixtures/30-layout-selective-components.marko +++ b/tests/fixtures/30-layout-selective-components.marko @@ -1,7 +1,7 @@ import Layout__markodown__ from "./layout.marko"; import * as LayoutModule__markodown__ from "./layout.marko"; - <${'h' + level} ...attrs><${content}> + <${'h' + level} ...attrs><${content} />