Changes
9 changed files (+328/-28)
-
-
@@ -11,6 +11,8 @@import { ofmHighlightFromMarkdown } from "./obsidian_markdown/mdast_util_ofm_highlight.ts"; import { ofmHighlight } from "./obsidian_markdown/micromark_extension_ofm_highlight.ts"; import { ofmImageSize } from "./obsidian_markdown/mdast_util_ofm_image_size.ts"; import { ofmWikilink } from "./obsidian_markdown/micromark_extension_ofm_wikilink.ts"; import { ofmWikilinkFromMarkdown } from "./obsidian_markdown/mdast_util_ofm_wikilink.ts"; import { macanaMarkAssets } from "./obsidian_markdown/mdast_util_macana_mark_assets.ts"; import { macanaMarkDocumentToken } from "./obsidian_markdown/mdast_util_macana_mark_document_token.ts";
-
@@ -23,6 +25,7 @@ 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 { ofmWikilinkToHastHandlers } from "./obsidian_markdown/mdast_util_ofm_wikilink.ts"; function getFrontMatterValue( frontmatter: Record<string, unknown>,
-
@@ -74,8 +77,12 @@ "getAssetToken" | "getDocumentToken">, ): Promise<Mdast.Root> { const mdast = fromMarkdown(markdown, { extensions: [gfm(), ofmHighlight()], mdastExtensions: [gfmFromMarkdown(), ofmHighlightFromMarkdown()], extensions: [gfm(), ofmHighlight(), ofmWikilink()], mdastExtensions: [ gfmFromMarkdown(), ofmHighlightFromMarkdown(), ofmWikilinkFromMarkdown(), ], }); ofmImageSize(mdast);
-
-
-
@@ -11,12 +11,21 @@ import type * as Mdast from "../../deps/esm.sh/mdast/types.ts";import { fromMarkdown } from "../../deps/esm.sh/mdast-util-from-markdown/mod.ts"; import { macanaMarkAssets } from "./mdast_util_macana_mark_assets.ts"; import { ofmWikilink } from "./micromark_extension_ofm_wikilink.ts"; import { ofmWikilinkFromMarkdown } from "./mdast_util_ofm_wikilink.ts"; const getAssetToken = (path: readonly string[]) => `mxa_${path.join("/")}` as const; function toMdast(markdown: string) { return fromMarkdown(markdown, { extensions: [ofmWikilink()], mdastExtensions: [ofmWikilinkFromMarkdown()], }); } Deno.test("Should set Asset Token on image's data", async () => { const mdast = fromMarkdown(""); const mdast = toMdast(""); await macanaMarkAssets(mdast, getAssetToken);
-
@@ -40,7 +49,7 @@ });}); Deno.test("Should support Vaule root absolute path for image", async () => { const mdast = fromMarkdown(""); const mdast = toMdast(""); await macanaMarkAssets(mdast, getAssetToken);
-
@@ -64,9 +73,32 @@ });}); Deno.test("Should skip full image URL", async () => { const mdast = fromMarkdown(""); const mdast = toMdast(""); await macanaMarkAssets(mdast, getAssetToken); assert(!((mdast.children[0] as Mdast.Paragraph).children[0].data)); }); Deno.test("Should set Asset Token on wikilink embeds too", async () => { const mdast = toMdast("![[doge.mp4]]"); await macanaMarkAssets(mdast, getAssetToken); assertObjectMatch(mdast, { type: "root", children: [ { type: "paragraph", children: [ { type: "ofmWikilinkEmbed", data: { macanaAssetToken: "mxa_doge.mp4", }, }, ], }, ], }); });
-
-
-
@@ -6,28 +6,51 @@ 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"; import type { AssetToken } from "../../types.ts"; import type { OfmWikilinkEmbed } from "./mdast_util_ofm_wikilink.ts"; import type { ParseParameters } from "../interface.ts"; const SEPARATOR = "/"; const URL_REGEXP_PATTERN = /^[a-z0-9]+:\/\//i; function setToken(node: Mdast.Node) { return (token: AssetToken) => { node.data = { ...node.data, macanaAssetToken: token, }; }; } /** * Searches asset references mark those node by setting `macanaAssetToken` with Asset Token. * This function modifies the Mdast tree in place. */ export async function macanaMarkAssets( tree: Mdast.Nodes, tree: Mdast.Nodes | OfmWikilinkEmbed, getAssetToken: ParseParameters["getAssetToken"], ): Promise<void> { const promises: Promise<unknown>[] = []; const defs = definitions(tree); const defs = definitions(tree as Mdast.Nodes); visit( tree, (node) => node.type === "image" || node.type === "imageReference", (node) => node.type === "image" || node.type === "imageReference" || node.type === "ofmWikilinkEmbed", (node) => { switch (node.type) { case "ofmWikilinkEmbed": { const path = node.target.split(SEPARATOR); promises.push( Promise.resolve(getAssetToken(path)).then(setToken(node)), ); return SKIP; } case "image": { // Full URL if (URL_REGEXP_PATTERN.test(node.url)) {
-
@@ -37,11 +60,7 @@const path = node.url.split(SEPARATOR); promises.push( Promise.resolve(getAssetToken(path)).then((token) => { node.data ??= {}; // @ts-expect-error: incorrect library type definition. node.data.macanaAssetToken = token; }), Promise.resolve(getAssetToken(path)).then(setToken(node)), ); return SKIP; }
-
@@ -59,11 +78,7 @@const path = def.url.split(SEPARATOR); promises.push( Promise.resolve(getAssetToken(path)).then((token) => { def.data ??= {}; // @ts-expect-error: incorrect library type definition. def.data.macanaAssetToken = token; }), Promise.resolve(getAssetToken(path)).then(setToken(node)), ); return SKIP; }
-
-
-
@@ -11,12 +11,21 @@ import type * as Mdast from "../../deps/esm.sh/mdast/types.ts";import { fromMarkdown } from "../../deps/esm.sh/mdast-util-from-markdown/mod.ts"; import { macanaMarkDocumentToken } from "./mdast_util_macana_mark_document_token.ts"; import { ofmWikilinkFromMarkdown } from "./mdast_util_ofm_wikilink.ts"; import { ofmWikilink } from "./micromark_extension_ofm_wikilink.ts"; const getDocumentToken = (path: readonly string[]) => `mxt_${path.join("/")}` as const; function toMdast(markdown: string) { return fromMarkdown(markdown, { extensions: [ofmWikilink()], mdastExtensions: [ofmWikilinkFromMarkdown()], }); } Deno.test("Should set Document Token on link", async () => { const mdast = fromMarkdown("[Foo](./foo.md)"); const mdast = toMdast("[Foo](./foo.md)"); await macanaMarkDocumentToken(mdast, getDocumentToken);
-
@@ -39,7 +48,7 @@ });}); Deno.test("Should support absolute path", async () => { const mdast = fromMarkdown("[Foo](/foo.md)"); const mdast = toMdast("[Foo](/foo.md)"); await macanaMarkDocumentToken(mdast, getDocumentToken);
-
@@ -62,9 +71,32 @@ });}); Deno.test("Should skip full image URL", async () => { const mdast = fromMarkdown("[Foo](https://example.com/foo.md)"); const mdast = toMdast("[Foo](https://example.com/foo.md)"); await macanaMarkDocumentToken(mdast, getDocumentToken); assert(!((mdast.children[0] as Mdast.Paragraph).children[0].data)); }); Deno.test("Should set Document Token on wikilink", async () => { const mdast = toMdast("[[Foo]]"); await macanaMarkDocumentToken(mdast, getDocumentToken); assertObjectMatch(mdast, { type: "root", children: [ { type: "paragraph", children: [ { type: "ofmWikilink", data: { macanaDocumentToken: "mxt_Foo", }, }, ], }, ], }); });
-
-
-
@@ -9,6 +9,8 @@import type { ParseParameters } from "../interface.ts"; import type { DocumentToken } from "../../types.ts"; import type { OfmWikilink } from "./mdast_util_ofm_wikilink.ts"; const SEPARATOR = "/"; const IGNORE_REGEXP_PATTERN = /^([a-z0-9]+:\/\/|#)/i;
-
@@ -25,18 +27,35 @@ ** This function mutates the Mdast tree in place. */ export async function macanaMarkDocumentToken( tree: Mdast.Nodes, tree: Mdast.Nodes | OfmWikilink, getDocumentToken: ParseParameters["getDocumentToken"], ): Promise<void> { const promises: Promise<unknown>[] = []; const defs = definitions(tree); const defs = definitions(tree as Mdast.Nodes); visit( tree, (node) => node.type === "link" || node.type === "linkReference", (node) => node.type === "link" || node.type === "linkReference" || node.type === "ofmWikilink", (node) => { switch (node.type) { case "ofmWikilink": { const path = node.target.split(SEPARATOR); const token = getDocumentToken(path); if (token instanceof Promise) { promises.push(token.then((t) => { setDocumentToken(node, t); })); return SKIP; } setDocumentToken(node, token); return SKIP; } case "link": { if (IGNORE_REGEXP_PATTERN.test(node.url)) { return SKIP;
-
-
-
@@ -8,6 +8,8 @@ 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) ||
-
@@ -20,8 +22,15 @@return node.data.macanaAssetToken as AssetToken; } function replace(node: Mdast.Nodes, replacedPath: string): void { function replace( node: Mdast.Nodes | OfmWikilinkEmbed, replacedPath: string, ): void { switch (node.type) { case "ofmWikilinkEmbed": { node.target = replacedPath; return; } case "image": { node.url = replacedPath; return;
-
-
-
@@ -8,6 +8,8 @@ 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 } } {
-
@@ -23,8 +25,18 @@ */path: string; } function replace(node: Mdast.Nodes, { path }: ExchangeResult): void { 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;
-
-
-
@@ -0,0 +1,165 @@// 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 { Extension } from "../../deps/esm.sh/mdast-util-from-markdown/mod.ts"; import type { State } from "../../deps/esm.sh/mdast-util-to-hast/mod.ts"; import type * as Mdast from "../../deps/esm.sh/mdast/types.ts"; import type * as Hast from "../../deps/esm.sh/hast/types.ts"; export interface OfmWikilink extends Mdast.Node { type: "ofmWikilink"; target: string; label: string | null; } export interface OfmWikilinkEmbed extends Mdast.Node { type: "ofmWikilinkEmbed"; target: string; label: string | null; } export function ofmWikilinkFromMarkdown(): Extension { return { enter: { ofmWikilink(token) { this.enter({ // @ts-expect-error: unist-related libraries heavily relies on ambiend module declarations, // which Deno does not support. APIs also don't accept type parameters. type: "ofmWikilink", target: "", label: null, }, token); }, ofmWikilinkEmbed(token) { this.enter({ // @ts-expect-error: unist-related libraries heavily relies on ambiend module declarations, // which Deno does not support. APIs also don't accept type parameters. type: "ofmWikilinkEmbed", target: "", label: null, data: {}, }, token); }, ofmWikilinkTarget(token) { const context = this.stack[this.stack.length - 1] as | Mdast.Nodes | OfmWikilink | OfmWikilinkEmbed | undefined; switch (context?.type) { case "ofmWikilink": case "ofmWikilinkEmbed": { context.target = this.sliceSerialize(token); return; } default: { throw new Error(`Unexpected wikilink target in ${context?.type}`); } } }, ofmWikilinkLabel(token) { const context = this.stack[this.stack.length - 1] as | Mdast.Nodes | OfmWikilink | OfmWikilinkEmbed | undefined; switch (context?.type) { case "ofmWikilink": case "ofmWikilinkEmbed": { context.label = this.sliceSerialize(token); return; } default: { throw new Error(`Unexpected wikilink target in ${context?.type}`); } } }, }, exit: { ofmWikilink(token) { this.exit(token); }, ofmWikilinkEmbed(token) { this.exit(token); }, }, }; } export const ofmWikilinkToHastHandlers = { ofmWikilink(_state: State, node: OfmWikilink): Hast.Nodes { return { type: "element", tagName: "a", properties: { href: node.target, }, children: [{ type: "text", value: node.label ?? node.target }], }; }, ofmWikilinkEmbed(_state: State, node: OfmWikilinkEmbed): Hast.Nodes { switch (extname(node.target).toLowerCase()) { case ".jpg": case ".jpeg": case ".avif": case ".bmp": case ".png": case ".svg": case ".webp": { return { type: "element", tagName: "img", properties: { src: node.target, alt: node.label ?? undefined, }, children: [], }; } case ".flac": case ".m4a": case ".mp3": case ".ogg": case ".wav": case ".3gp": { return { type: "element", tagName: "audio", properties: { src: node.target, title: node.label ?? undefined, }, children: [], }; } case ".mkv": case ".mov": case ".mp4": case ".ogv": case ".webm": { return { type: "element", tagName: "video", properties: { src: node.target, title: node.label ?? undefined, }, children: [], }; } default: { return { type: "element", tagName: "iframe", properties: { src: node.target, title: node.label ?? undefined, }, children: [], }; } } }, };
-
-
-
@@ -15,7 +15,10 @@ import { toJsxRuntime } from "../../../deps/esm.sh/hast-util-to-jsx-runtime/mod.ts";import * as HastToJSXRuntime from "../../../deps/esm.sh/hast-util-to-jsx-runtime/mod.ts"; import type { Document, DocumentTree } from "../../../types.ts"; import type { ObsidianMarkdownDocument } from "../../../content_parser/obsidian_markdown.ts"; import { type ObsidianMarkdownDocument, ofmWikilinkToHastHandlers, } from "../../../content_parser/obsidian_markdown.ts"; import type { JSONCanvasDocument } from "../../../content_parser/json_canvas.ts"; import { usePathResolver } from "../contexts/path_resolver.tsx";
-
@@ -81,7 +84,13 @@function ObsidianMarkdownBody( { content, document, tree, copyright, assets }: ObsidianMarkdownBodyProps, ) { const hast = toHast(content.content); const hast = toHast(content.content, { // @ts-expect-error: unist-related libraries heavily relies on ambiend module declarations, // which Deno does not support. APIs also don't accept type parameters. handlers: { ...ofmWikilinkToHastHandlers, }, }); const toc = tocMut(hast).map((item) => mapTocItem(item, (item) => toNode({ type: "root", children: item }))
-