Changes
21 changed files (+531/-399)
-
-
@@ -9,7 +9,7 @@ import type { ContentParser, ParseParameters } from "./interface.ts";import type { DocumentContent } from "../types.ts"; import { isJSONCanvas, type JSONCanvas } from "./json_canvas/types.ts"; import { mapNode } from "./json_canvas/utils.ts"; import { mapNodeAsync } from "./json_canvas/utils.ts"; import { parseMarkdown } from "./obsidian_markdown.ts"; const PROBABLY_URL_PATTERN = /^[a-z0-9]+:/i;
-
@@ -58,7 +58,7 @@ }return { kind: "json_canvas", content: await mapNode(json, async (node) => { content: await mapNodeAsync(json, async (node) => { switch (node.type) { case "text": { return {
-
-
-
@@ -4,20 +4,37 @@ // SPDX-License-Identifier: Apache-2.0import type { JSONCanvas, Node } from "./types.ts"; export async function mapNode<A, B = A>( export function mapNode<A, B = A>( tree: JSONCanvas<A>, f: (node: Node<A>) => Node<B>, ): JSONCanvas<B> { if (!tree.nodes?.map?.length) { // No nodes = no text nodes = Markdown type does not matter. return tree as unknown as JSONCanvas<B>; } const nodes = tree.nodes.map<Node<B>>((node) => { return f(node); }); return { ...tree, nodes, }; } export async function mapNodeAsync<A, B = A>( tree: JSONCanvas<A>, f: (node: Node<A>) => Node<B> | Promise<Node<B>>, f: (node: Node<A>) => Promise<Node<B>>, ): Promise<JSONCanvas<B>> { if (!tree.nodes?.map?.length) { // No nodes = no text nodes = Markdown type does not matter. return Promise.resolve(tree as unknown as JSONCanvas<B>); return tree as unknown as JSONCanvas<B>; } const nodes = await Promise.all( tree.nodes.map<Promise<Node<B>>>(async (node) => { return await f(node); }), ); const nodes = await Promise.all(tree.nodes.map<Promise<Node<B>>>((node) => { return f(node); })); return { ...tree,
-
-
-
@@ -28,8 +28,6 @@ ParseParameters,} from "./interface.ts"; import type { DocumentContent } from "../types.ts"; export { macanaReplaceAssetTokens } from "./obsidian_markdown/mdast_util_macana_replace_asset_tokens.ts"; export { macanaReplaceDocumentToken } from "./obsidian_markdown/mdast_util_macana_replace_document_tokens.ts"; export { ofmToHastHandlers } from "./obsidian_markdown/mdast_util_ofm.ts"; export { ofmHtml } from "./obsidian_markdown/hast_util_ofm_html.ts"; export type { CalloutType } from "./obsidian_markdown/mdast_util_ofm_callout.ts";
-
@@ -111,8 +109,8 @@ autoHeadingIdFromMarkdown(),], }); await macanaMarkAssets(mdast, getAssetToken); await macanaMarkDocumentToken(mdast, getDocumentToken); await macanaMarkAssets(mdast, getAssetToken); return mdast; }
-
-
-
@@ -43,6 +43,11 @@ node.type === "ofmWikilinkEmbed",(node) => { switch (node.type) { case "ofmWikilinkEmbed": { // This node is document embed. if (node.data && "macanaDocumentToken" in node.data) { return SKIP; } const path = node.target.split(SEPARATOR); promises.push(
-
-
-
@@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>// // SPDX-License-Identifier: Apache-2.0 import { extname } from "../../deps/deno.land/std/path/mod.ts"; import type * as Mdast from "../../deps/esm.sh/mdast/types.ts"; import { SKIP, visit } from "../../deps/esm.sh/unist-util-visit/mod.ts"; import { definitions } from "../../deps/esm.sh/mdast-util-definitions/mod.ts";
-
@@ -9,7 +10,10 @@import type { ParseParameters } from "../interface.ts"; import type { DocumentToken } from "../../types.ts"; import type { OfmWikilink } from "./mdast_util_ofm_wikilink.ts"; import type { OfmWikilink, OfmWikilinkEmbed, } from "./mdast_util_ofm_wikilink.ts"; const SEPARATOR = "/"; const FRAGMENT_PREFIX = "#";
-
@@ -37,9 +41,12 @@ * Searches document references and Marks thoese node by setting `macanaDocumentToken`* with Document Token. * * This function mutates the Mdast tree in place. * * In order to correctly mark document embeds, run this function before * `macanaMarkAssets`. */ export async function macanaMarkDocumentToken( tree: Mdast.Nodes | OfmWikilink, tree: Mdast.Nodes | OfmWikilink | OfmWikilinkEmbed, getDocumentToken: ParseParameters["getDocumentToken"], ): Promise<void> { const promises: Promise<unknown>[] = [];
-
@@ -50,9 +57,29 @@ visit(tree, (node) => node.type === "link" || node.type === "linkReference" || node.type === "ofmWikilink", node.type === "ofmWikilink" || node.type === "ofmWikilinkEmbed", (node) => { switch (node.type) { case "ofmWikilinkEmbed": { const { path, fragments } = parseInternalLink(node.target); if (extname(path[path.length - 1])) { // File embeds return SKIP; } const token = getDocumentToken(path, fragments); if (token instanceof Promise) { promises.push(token.then((t) => { setDocumentToken(node, t); })); return SKIP; } setDocumentToken(node, token); return SKIP; } case "ofmWikilink": { const { path, fragments } = parseInternalLink(node.target);
-
-
-
@@ -1,42 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import { assertObjectMatch } from "../../deps/deno.land/std/assert/mod.ts"; import { fromMarkdown } from "../../deps/esm.sh/mdast-util-from-markdown/mod.ts"; import { macanaMarkAssets } from "./mdast_util_macana_mark_assets.ts"; import { macanaReplaceAssetTokens } from "./mdast_util_macana_replace_asset_tokens.ts"; Deno.test("Should replace Asset Token on images", async () => { const mdast = fromMarkdown(""); await macanaMarkAssets(mdast, () => { return "mxa_0"; }); await macanaReplaceAssetTokens(mdast, (token) => { if (token !== "mxa_0") { throw new Error("Unexpected token"); } return "../FOO.PNG"; }); assertObjectMatch(mdast, { type: "root", children: [ { type: "paragraph", children: [ { type: "image", alt: "Foo", url: "../FOO.PNG", }, ], }, ], }); });
-
-
-
@@ -1,81 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import type * as Mdast from "../../deps/esm.sh/mdast/types.ts"; import { visit } from "../../deps/esm.sh/unist-util-visit/mod.ts"; import type { AssetToken } from "../../types.ts"; import type { OfmWikilinkEmbed } from "./mdast_util_ofm_wikilink.ts"; function extractToken(node: Mdast.Node): AssetToken { if ( !node.data || !("macanaAssetToken" in node.data) || typeof node.data.macanaAssetToken !== "string" || !node.data.macanaAssetToken.startsWith("mxa_") ) { throw new Error(`Asset Token not found on the node: ${node.type}`); } return node.data.macanaAssetToken as AssetToken; } function replace( node: Mdast.Nodes | OfmWikilinkEmbed, replacedPath: string, ): void { switch (node.type) { case "ofmWikilinkEmbed": { node.target = replacedPath; return; } case "image": { node.url = replacedPath; return; } case "definition": { node.url = replacedPath; return; } } } /** * Modifies the given Mdast tree by searching nodes having `macanaAssetToken` * property then replacing node properties. * This function modifies Mdast tree in place. * * @param tree - Mdast tree to modify. * @param replacer - A function that takes Asset Token and returns path *string* for the asset. */ export async function macanaReplaceAssetTokens( tree: Mdast.Nodes, replacer: (token: AssetToken) => string | Promise<string>, ): Promise<void> { const promises: Promise<unknown>[] = []; visit( tree, (node) => node.data && "macanaAssetToken" in node.data && typeof node.data.macanaAssetToken === "string", (node) => { const token = extractToken(node); const replaced = replacer(token); if (replaced instanceof Promise) { promises.push(replaced.then((str) => { replace(node, str); })); return; } replace(node, replaced); }, ); await Promise.all(promises); }
-
-
-
@@ -1,43 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import { assertObjectMatch } from "../../deps/deno.land/std/assert/mod.ts"; import { fromMarkdown } from "../../deps/esm.sh/mdast-util-from-markdown/mod.ts"; import { macanaMarkDocumentToken } from "./mdast_util_macana_mark_document_token.ts"; import { macanaReplaceDocumentToken } from "./mdast_util_macana_replace_document_tokens.ts"; Deno.test("Should replace Document Token on links", async () => { const mdast = fromMarkdown("[Foo](./foo.png)"); await macanaMarkDocumentToken(mdast, () => { return "mxt_0"; }); await macanaReplaceDocumentToken(mdast, (token) => { if (token !== "mxt_0") { throw new Error("Unexpected token"); } return { path: "../FOO.MD", }; }); assertObjectMatch(mdast, { type: "root", children: [ { type: "paragraph", children: [ { type: "link", url: "../FOO.MD", }, ], }, ], }); });
-
-
-
@@ -1,89 +0,0 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import type * as Mdast from "../../deps/esm.sh/mdast/types.ts"; import { visit } from "../../deps/esm.sh/unist-util-visit/mod.ts"; import type { DocumentToken } from "../../types.ts"; import type { OfmWikilink } from "./mdast_util_ofm_wikilink.ts"; function hasDocumentToken( node: Mdast.Node, ): node is typeof node & { data: { macanaDocumentToken: DocumentToken } } { return !!(node.data && ("macanaDocumentToken" in node.data) && typeof node.data.macanaDocumentToken === "string" && node.data.macanaDocumentToken.startsWith("mxt_")); } export interface ExchangeResult { /** * Path string appears on the final markup. */ path: string; } function replace( node: Mdast.Nodes | OfmWikilink, { path }: ExchangeResult, ): void { switch (node.type) { case "ofmWikilink": { // Do not use resolved path as a fallback label node.label ??= node.target; node.target = path; return; } case "link": { node.url = path; return; } case "definition": { node.url = path; return; } } } /** * Modifies the given Mdast tree by searching nodes having `macanaDocumentToken` * property then replacing node properties. * This function modifies Mdast tree in place. * * @param tree - Mdast tree to modify. * @param exchange - A function that takes Document Token and returns properties required for constructing markup. */ export function macanaReplaceDocumentToken( tree: Mdast.Nodes, exchange: (token: DocumentToken) => ExchangeResult | Promise<ExchangeResult>, ): Promise<void> | void { const promises: Promise<unknown>[] = []; visit( tree, (node) => hasDocumentToken(node), (node) => { if (!hasDocumentToken(node)) { return; } const exchanged = exchange(node.data.macanaDocumentToken); if (exchanged instanceof Promise) { promises.push(exchanged.then((payload) => { replace(node, payload); })); return; } replace(node, exchanged); }, ); if (promises.length > 0) { return Promise.all(promises).then(() => {}); } }
-
-
-
@@ -90,6 +90,23 @@ ```![[dog.jpg|128x128]] #### Another documents Put an exclamation symbol `!` before internal wikilink to make it a document embeds. - [Embed files - Obsidian Help](https://help.obsidian.md/Linking+notes+and+files/Embed+files#Embed+a+note+in+another+note) As you can see in the below examples, Macana embeds the target document directly inside body without any modifications. The embedded section affects embedder's document outline. Place the embed inside blockquote if you want to it to be shown as quote. ```markdown ![[GitHub Flavored Markdown#Strikethrough]] > ![[GitHub Flavored Markdown#Strikethrough]] ``` ![[GitHub Flavored Markdown#Strikethrough]] > ![[GitHub Flavored Markdown#Strikethrough]] ### Highlights Surround text with `==` to make the text highlighted.
-
-
-
@@ -0,0 +1,11 @@## Images  ## Thematic breaks *** --- ___
-
-
-
@@ -1,6 +1,6 @@--- createdAt: 2024-04-15T23:00:00+09:00 updatedAt: 2024-04-23T23:00:00+09:00 updatedAt: 2024-05-03T15:00:00+09:00 --- ## v1.0
-
@@ -37,7 +37,7 @@## v0.1 - [x] Using Vault as a site structure - [ ] Markdown Parsing - [x] Markdown Parsing - [x] CommonMark - [x] GitHub Flavored Markdown - [x] Strikethrough
-
@@ -47,7 +47,7 @@ - [x] Autolinks- [x] Tables - [x] Math ... ~No support if MathJax can't generate MathML at build time.~ Using Temml instead. - [x] Syntax Highlighting - [ ] Obsidian Extensions - [x] Obsidian Extensions - [x] Internal Link path resolution - [x] Absolute path in vault - [x] Absolute path in vault (extension-less)
-
@@ -60,11 +60,11 @@ - [x] Heading- [x] Block reference - [x] Defining a block - [x] Image size annotation - [ ] Embeddings - [x] Embeddings - [x] Image file - [x] Audio file - [x] PDF file - [ ] List from another file - [x] Another document - [x] ==Highlight== - [x] Callouts - [x] Comments %% You can check this item once I'm no longer visible %%
-
-
-
@@ -1,4 +1,4 @@import type { Document, DocumentTree } from "../../types.ts"; import type { Document, DocumentTree, FileReader } from "../../types.ts"; export interface Assets { globalCss: readonly string[];
-
@@ -39,4 +39,9 @@ * Resolves the given path as an absolute path from the document root to* relative path from the current document. */ resolvePath(to: readonly string[]): readonly string[]; /** * Copy file to the output directory. */ copyFile(file: FileReader): void; }
-
-
-
@@ -0,0 +1,137 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 /** @jsx h */ import { extname } from "../../../deps/deno.land/std/path/mod.ts"; import type * as Hast from "../../../deps/esm.sh/hast/types.ts"; import type * as Mdast from "../../../deps/esm.sh/mdast/types.ts"; import { h } from "../../../deps/esm.sh/hastscript/mod.ts"; import { type State } from "../../../deps/esm.sh/mdast-util-to-hast/mod.ts"; import { type OfmWikilinkEmbed } from "../../../content_parser/obsidian_markdown/mdast_util_ofm_wikilink.ts"; import type { Document } from "../../../types.ts"; import type { BuildContext } from "../context.ts"; import { hasAssetToken, hasDocumentToken } from "./utils.ts"; function sizeProperties(node: Mdast.Node): { width?: number; height?: number } { return { width: node.data && "width" in node.data && typeof node.data.width === "number" ? node.data.width : undefined, height: node.data && "height" in node.data && typeof node.data.height === "number" ? node.data.height : undefined, }; } function getUrl(url: string, node: Mdast.Node, context: BuildContext): string { if (!hasAssetToken(node)) { return url; } const file = context.documentTree.exchangeToken(node.data.macanaAssetToken); context.copyFile(file); return context.resolvePath(file.path).join("/"); } export interface EmbedHandlersParameters { context: BuildContext; buildDocumentContent( document: Document, fragments?: readonly string[], ): Hast.Nodes; } export function embedHandlers( { context, buildDocumentContent }: EmbedHandlersParameters, ) { return { image(_state: State, node: Mdast.Image) { return h("img", { ...sizeProperties(node), src: getUrl(node.url, node, context), alt: node.alt, }, []); }, imageReference(state: State, node: Mdast.ImageReference) { const def = state.definitionById.get(node.identifier); if (!def) { throw new Error(`Orphaned image reference: id=${node.identifier}`); } return h("img", { ...sizeProperties(node), src: getUrl(def.url, node, context), alt: node.alt, }, []); }, ofmWikilinkEmbed(_state: State, node: OfmWikilinkEmbed) { // Document embed if (hasDocumentToken(node)) { const { document, fragments } = context.documentTree.exchangeToken( node.data.macanaDocumentToken, ); const hast = buildDocumentContent(document, fragments); return h("div", {}, [hast]); } const path = getUrl(node.target, node, context); switch (extname(node.target).toLowerCase()) { case ".jpg": case ".jpeg": case ".avif": case ".bmp": case ".png": case ".svg": case ".webp": { return h("img", { ...sizeProperties(node), src: path, alt: node.label ?? undefined, }, []); } case ".flac": case ".m4a": case ".mp3": case ".ogg": case ".wav": case ".3gp": { return h("audio", { src: path, title: node.label ?? undefined, }, []); } case ".mkv": case ".mov": case ".mp4": case ".ogv": case ".webm": { return h("video", { ...sizeProperties(node), src: path, title: node.label ?? undefined, }, []); } default: { return h("iframe", { ...sizeProperties(node), src: path, title: node.label ?? undefined, }, []); } } }, }; }
-
-
-
@@ -16,6 +16,9 @@ import { type OfmWikilink } from "../../../content_parser/obsidian_markdown/mdast_util_ofm_wikilink.ts";import { buildClasses, css, join } from "../css.ts"; import * as lucide from "../icons/lucide.tsx"; import type { BuildContext } from "../context.ts"; import { hasDocumentToken } from "./utils.ts"; function isExternal(urlOrPath: string): boolean { try {
-
@@ -56,6 +59,8 @@ * Whether to set `target="_blank"` to anchor links pointing to* external location. */ openExternalLinkInBlank?: boolean; context: BuildContext; } function link(
-
@@ -81,12 +86,33 @@ lucide.externalLink({ className: c.externalIcon, "aria-hidden": "true" }),]); } function getUrl(url: string, node: Mdast.Node, context: BuildContext): string { if (!hasDocumentToken(node)) { return url; } const { document, fragments } = context.documentTree.exchangeToken( node.data.macanaDocumentToken, ); const hash = fragments.length > 0 ? "#" + document.content.getHash(fragments) : ""; const path = context.resolvePath([...document.path, ""]); return path.join("/") + hash; } export function linkHandlers( { openExternalLinkInBlank = true }: LinkHandlersOptions = {}, { openExternalLinkInBlank = true, context }: LinkHandlersOptions, ): Handlers { return { link(state, node: Mdast.Link) { return link(node.url, state.all(node), { openExternalLinkInBlank }); return link(getUrl(node.url, node, context), state.all(node), { openExternalLinkInBlank, context, }); }, linkReference(state, node: Mdast.LinkReference) { const def = state.definitionById.get(node.identifier);
-
@@ -94,15 +120,18 @@ if (!def) {throw new Error(`Orphaned link reference: id=${node.identifier}`); } return link(def.url, state.all(node), { openExternalLinkInBlank }); return link(getUrl(def.url, node, context), state.all(node), { openExternalLinkInBlank, context, }); }, // @ts-expect-error: unist-related libraries heavily relies on ambient module declarations, // which Deno does not support. APIs also don't accept type parameters. ofmWikilink(_state: State, node: OfmWikilink) { return link(node.target, [{ return link(getUrl(node.target, node, context), [{ type: "text", value: node.label ?? node.target, }], { openExternalLinkInBlank }); }], { openExternalLinkInBlank, context }); }, }; }
-
-
-
@@ -13,8 +13,10 @@ ofmToHastHandlers,} from "../../../content_parser/obsidian_markdown.ts"; import { buildClasses, css, join as joinCss } from "../css.ts"; import { type BuildContext } from "../context.ts"; import { calloutHandlers, calloutStyles } from "./callout.tsx"; import { embedHandlers, type EmbedHandlersParameters } from "./embed.tsx"; import { listHandlers, listStyles } from "./list.tsx"; import { mathHandlers } from "./math.ts"; import { codeHandlers, codeStyles } from "./code.tsx";
-
@@ -143,7 +145,15 @@ linkStyles,quoteStyles, ); export function fromMdast(mdast: Mdast.Nodes): Hast.Nodes { export interface FromMdastParameters extends Pick<EmbedHandlersParameters, "buildDocumentContent"> { context: BuildContext; } export function fromMdast( mdast: Mdast.Nodes, { context, buildDocumentContent }: FromMdastParameters, ): Hast.Nodes { return ofmHtml(toHast(mdast, { handlers: { ...ofmToHastHandlers(),
-
@@ -151,9 +161,10 @@ ...calloutHandlers(),...listHandlers(), ...mathHandlers(), ...codeHandlers(), ...linkHandlers(), ...linkHandlers({ context }), ...quoteHandlers(), ...paragraphHandlers(), ...embedHandlers({ context, buildDocumentContent }), }, allowDangerousHtml: true, }));
-
-
-
@@ -4,6 +4,8 @@ // SPDX-License-Identifier: Apache-2.0import type * as Mdast from "../../../deps/esm.sh/mdast/types.ts"; import type { AssetToken, DocumentToken } from "../../../types.ts"; export function getRequestedId<Node extends Mdast.Node>( node: Node, ): string | null {
-
@@ -23,3 +25,19 @@ }return node.data.hProperties.id; } export function hasDocumentToken( node: Mdast.Node, ): node is typeof node & { data: { macanaDocumentToken: DocumentToken } } { return !!(node.data && ("macanaDocumentToken" in node.data) && typeof node.data.macanaDocumentToken === "string" && node.data.macanaDocumentToken.startsWith("mxt_")); } export function hasAssetToken( node: Mdast.Node, ): node is typeof node & { data: { macanaAssetToken: AssetToken } } { return !!(node.data && ("macanaAssetToken" in node.data) && typeof node.data.macanaAssetToken === "string" && node.data.macanaAssetToken.startsWith("mxa_")); }
-
-
-
@@ -121,3 +121,22 @@ level: 1,}, ]); }); Deno.test("Should not set duplicate IDs", () => { const toc = tocMut(h(null, [ h("h1", [{ type: "text", value: "Foo" }]), h("h1", [{ type: "text", value: "Foo" }]), ])); // @ts-expect-error: Deno ships broken type definition for assert functions assertObjectMatch(toc, [ { level: 1, id: "Foo", }, { level: 1, id: "Foo__1", }, ]); });
-
-
-
@@ -34,6 +34,8 @@ ): readonly TocItem<Hast.ElementContent[]>[] {const items: TocItem[] = []; const stack: TocItem[] = []; const counts = new Map<string, number>(); const pop = () => { const popped = stack.pop(); if (!popped) {
-
@@ -85,6 +87,15 @@ if (typeof id !== "string") {id = fastUslug(toString(node), { lower: false, }); node.properties.id = id; } const count = counts.get(id) ?? 0; counts.set(id, count + 1); if (count > 0) { id = id + "__" + count; node.properties.id = id; }
-
-
-
@@ -0,0 +1,31 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import type * as Mdast from "../../../deps/esm.sh/mdast/types.ts"; import { visit } from "../../../deps/esm.sh/unist-util-visit/mod.ts"; /** * This mutates given tree. */ export function deleteId<T extends Mdast.Node>(tree: T): void { visit(tree, (node) => { if (!node.data) { return; } if ( !("hProperties" in node.data) || typeof node.data.hProperties !== "object" || !node.data.hProperties ) { return; } node.data = { hProperties: { ...node.data.hProperties, id: undefined, }, }; }); }
-
-
-
@@ -2,15 +2,15 @@ // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>// // SPDX-License-Identifier: Apache-2.0 import type * as Hast from "../../deps/esm.sh/hast/types.ts"; import type * as Mdast from "../../deps/esm.sh/mdast/types.ts"; import { headingRange } from "../../deps/esm.sh/mdast-util-heading-range/mod.ts"; import { toHtml } from "../../deps/esm.sh/hast-util-to-html/mod.ts"; import { logger } from "../../logger.ts"; import type { BuildParameters, PageBuilder } from "../interface.ts"; import { macanaReplaceAssetTokens, macanaReplaceDocumentToken, type ObsidianMarkdownDocument, } from "../../content_parser/obsidian_markdown.ts"; import type { JSONCanvasDocument } from "../../content_parser/json_canvas.ts";
-
@@ -28,23 +28,20 @@ import { globalStyles } from "./global_styles.ts";import type { Assets, BuildContext } from "./context.ts"; import { tocMut } from "./hast/hast_util_toc_mut.ts"; import { deleteId } from "./mdast/mdast_util_delete_id.ts"; import { fromMdast, fromMdastStyles, style as styleMarkdownContent, } from "./from_mdast/mod.ts"; import { jsonCanvas as jsonCanvasRenderer, jsonCanvasStyles as jsonCanvasRendererStyles, } from "./json_canvas/mod.tsx"; import { indexRedirect } from "./pages/index_redirect.tsx"; import { markdownEmbed, markdownPage, markdownPageStyles, } from "./pages/markdown.tsx"; import { jsonCanvasEmbed, jsonCanvasPage, jsonCanvasPageStyles, } from "./pages/json_canvas.tsx"; import { markdownPage, markdownPageStyles } from "./pages/markdown.tsx"; import { jsonCanvasPage, jsonCanvasPageStyles } from "./pages/json_canvas.tsx"; export type { BuildContext } from "./context.ts";
-
@@ -162,6 +159,7 @@ globalStyles,fromMdastStyles, markdownPageStyles, jsonCanvasPageStyles, jsonCanvasRendererStyles, ), );
-
@@ -224,6 +222,127 @@ duration,}); } #buildEmbed( context: BuildContext, target: Document, fragments: readonly string[] = [], ): Hast.Nodes { if (isJSONCanvas(target)) { if (fragments.length > 0) { logger().warn( "JSONCanvas embeds does not support partial embed using hash", { embedder: context.document.path.join(" > "), target: target.path.join(" > "), fragments, }, ); } const content = jsonCanvas.mapNode( target.content.content, (node) => { switch (node.type) { case "text": { const nodes = structuredClone(node.text); deleteId(nodes); return { ...node, text: fromMdast(nodes, { context, buildDocumentContent: (document, fragments) => { return this.#buildEmbed( context, document, fragments, ); }, }), }; } case "file": { if (isAssetToken(node.file)) { const file = context.documentTree.exchangeToken(node.file); context.copyFile(file); return { ...node, file: toRelativePathString(file.path, context.document.path), }; } if (isDocumentToken(node.file)) { const { document, fragments } = context.documentTree .exchangeToken( node.file, ); return { ...node, type: "text", text: this.#buildEmbed( context, document, fragments, ), }; } return node; } default: { return node; } } }, ); return jsonCanvasRenderer({ data: content, }); } if (isObsidianMarkdown(target)) { let nodes: Mdast.Nodes = structuredClone(target.content.content); if (fragments.length > 0) { const id = target.content.getHash(fragments); headingRange( nodes as Mdast.Root, id, (start, content) => { nodes = { type: "root", children: [ start, ...content, ], }; }, ); } deleteId(nodes); return fromMdast(nodes, { context, buildDocumentContent: (document, fragments) => { return this.#buildEmbed(context, document, fragments); }, }); } throw new Error( `Can't embed document "${ target.path.join("/") }": unknown content kind (${target.content.kind})`, ); } async #build( { item, tree, parentLanguage, pathPrefix = [], buildParameters, assets }: InnerBuildParameters,
-
@@ -231,6 +350,8 @@ ): Promise<void> {const { fileSystemWriter } = buildParameters; if ("file" in item) { const writeTasks: Promise<unknown>[] = []; const context: BuildContext = { document: item, documentTree: tree,
-
@@ -242,6 +363,13 @@ resolvePath(to) {// This page builder transforms path to "Foo/Bar.md" to "Foo/Bar/(index.html)" return toRelativePath(to, item.path); }, copyFile(file) { writeTasks.push( file.read().then((bytes) => { fileSystemWriter.write(file.path, bytes); }), ); }, }; if (isObsidianMarkdown(item) || isJSONCanvas(item)) {
-
@@ -252,52 +380,25 @@ ...pathPrefix,item.metadata.name, ]; const writeTasks: Promise<unknown>[] = []; switch (item.content.kind) { case "json_canvas": { const content = await jsonCanvas.mapNode( const content = jsonCanvas.mapNode( item.content.content, async (node) => { (node) => { switch (node.type) { case "text": { await macanaReplaceAssetTokens( node.text, async (token) => { const file = tree.exchangeToken(token); writeTasks.push(fileSystemWriter.write( file.path, await file.read(), )); return toRelativePathString(file.path, item.path); }, ); await macanaReplaceDocumentToken( node.text, async (token) => { const { document, fragments } = tree.exchangeToken( token, ); const hash = fragments.length > 0 ? "#" + document.content.getHash(fragments) : ""; return { path: toRelativePathString( [...document.path, ""], item.path, ) + hash, }; }, ); return { ...node, text: fromMdast(node.text), text: fromMdast(node.text, { context, buildDocumentContent: (document, fragments) => { return this.#buildEmbed( context, document, fragments, ); }, }), }; } case "file": {
-
@@ -305,7 +406,9 @@ if (isAssetToken(node.file)) {const file = tree.exchangeToken(node.file); writeTasks.push( fileSystemWriter.write(file.path, await file.read()), file.read().then((bytes) => fileSystemWriter.write(file.path, bytes) ), ); return {
-
@@ -318,17 +421,11 @@ if (isDocumentToken(node.file)) {const { document, fragments } = tree.exchangeToken( node.file, ); const hash = fragments.length > 0 ? "#" + document.content.getHash(fragments) : ""; return { ...node, file: toRelativePathString( [...document.path, "embed.html"], item.path, ) + hash, type: "text", text: this.#buildEmbed(context, document, fragments), }; }
-
@@ -351,56 +448,22 @@ fileSystemWriter.write([...basePath, "index.html", ], enc.encode(html)), ); const embed = toHtml(jsonCanvasEmbed({ context, content, })); writeTasks.push( fileSystemWriter.write([ ...basePath, "embed.html", ], enc.encode(embed)), ); await Promise.all(writeTasks); return; } case "obsidian_markdown": { await macanaReplaceAssetTokens( item.content.content, async (token) => { const file = tree.exchangeToken(token); writeTasks.push(fileSystemWriter.write( file.path, await file.read(), )); return toRelativePathString(file.path, item.path); }, ); await macanaReplaceDocumentToken( item.content.content, async (token) => { const { document, fragments } = tree.exchangeToken(token); const hash = fragments.length > 0 ? "#" + document.content.getHash(fragments) : ""; return { path: toRelativePathString([...document.path, ""], item.path) + hash, }; const hast = fromMdast(item.content.content, { context, buildDocumentContent: (document, fragments) => { return this.#buildEmbed( context, document, fragments, ); }, ); const hast = fromMdast(item.content.content); }); const toc = tocMut(hast); const html = toHtml(markdownPage({
-
@@ -414,18 +477,6 @@ fileSystemWriter.write([...basePath, "index.html", ], enc.encode(html)), ); const embed = toHtml(markdownEmbed({ context, content: styleMarkdownContent(hast), })); writeTasks.push( fileSystemWriter.write([ ...basePath, "embed.html", ], enc.encode(embed)), ); await Promise.all(writeTasks);
-