Changes
7 changed files (+621/-330)
-
-
@@ -2,14 +2,18 @@ // 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 type { ContentParser, ParseParameters } from "./interface.ts"; import type { DocumentContent } from "../types.ts"; import { isJSONCanvas, type JSONCanvas } from "./json_canvas/types.ts"; import { mapText } from "./json_canvas/utils.ts"; import { mapNode } from "./json_canvas/utils.ts"; import { parseMarkdown } from "./obsidian_markdown.ts"; const PROBABLY_URL_PATTERN = /^[a-z0-9]+:/i; const SEPARATOR = "/"; export class JSONCanvasParseError extends Error {}
-
@@ -54,11 +58,38 @@ }return { kind: "json_canvas", content: await mapText(json, async (node) => { return parseMarkdown(node.text, { getAssetToken, getDocumentToken, }); content: await mapNode(json, async (node) => { switch (node.type) { case "text": { return { ...node, text: await parseMarkdown(node.text, { getAssetToken, getDocumentToken, }), }; } case "file": { if (PROBABLY_URL_PATTERN.test(node.file)) { return node; } const path = node.file.split(SEPARATOR); const ext = extname(node.file); const token = (!ext || /^\.md$/i.test(ext)) ? await getDocumentToken(path) : await getAssetToken(path); return { ...node, file: token, }; } default: { return node; } } }), }; }
-
-
-
@@ -2,37 +2,11 @@ // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>// // SPDX-License-Identifier: Apache-2.0 import type { JSONCanvas, Node, TextNode } from "./types.ts"; import type { JSONCanvas, Node } from "./types.ts"; export function mapTextSync<A, B>( export async function mapNode<A, B = A>( tree: JSONCanvas<A>, f: (node: TextNode<A>) => 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) => { if (node.type !== "text") { return node; } return { ...node, text: f(node), }; }); return { ...tree, nodes, }; } export async function mapText<A, B>( tree: JSONCanvas<A>, f: (node: TextNode<A>) => B | Promise<B>, f: (node: Node<A>) => Node<B> | Promise<Node<B>>, ): Promise<JSONCanvas<B>> { if (!tree.nodes?.map?.length) { // No nodes = no text nodes = Markdown type does not matter.
-
@@ -41,14 +15,7 @@ }const nodes = await Promise.all( tree.nodes.map<Promise<Node<B>>>(async (node) => { if (node.type !== "text") { return node; } return { ...node, text: await f(node), }; return await f(node); }), );
-
-
-
@@ -43,14 +43,14 @@ - [x] `<title>` <title>Foo</title>- [x] `<style>` <style>* { display: none; }</style> - [x] Keep Raw HTML (Unified libraries tends to ignore spec by default, needs to opt-out) - [x] <span style="color: red; background-color: yellow;">Colored text</span> - [ ] JSONCanvas - [ ] Node rendering - [x] JSONCanvas - [x] Node rendering - [x] Basic shapes - [x] Colors - [x] Text node - [x] Group node - [x] Link node - [ ] File node - [x] File node - [x] Edge rendering - [x] Basic shape - [x] Arrow
-
-
-
@@ -6,6 +6,7 @@ /** @jsx h */import { h, renderSSR } from "../../deps/deno.land/x/nano_jsx/mod.ts"; import type * as Mdast from "../../deps/esm.sh/mdast/types.ts"; import { toHast } from "../../deps/esm.sh/mdast-util-to-hast/mod.ts"; import { logger } from "../../logger.ts";
-
@@ -14,16 +15,37 @@ import {macanaReplaceAssetTokens, macanaReplaceDocumentToken, type ObsidianMarkdownDocument, ofmHtml, ofmToHastHandlers, } from "../../content_parser/obsidian_markdown.ts"; import type { JSONCanvasDocument } from "../../content_parser/json_canvas.ts"; import * as jsonCanvas from "../../content_parser/json_canvas/utils.ts"; import type { Document, DocumentDirectory, DocumentTree } from "../../types.ts"; import type { AssetToken, Document, DocumentDirectory, DocumentToken, DocumentTree, } from "../../types.ts"; import * as css from "./css.ts"; import * as Html from "./components/html.tsx"; import * as HastRenderer from "./components/atoms/hast_renderer.tsx"; import { syntaxHighlightingHandlers } from "./mdast/syntax_highlighting_handlers.ts"; import { mapTocItem, tocMut } from "./hast/hast_util_toc_mut.ts"; import { PathResolverProvider } from "./contexts/path_resolver.tsx"; const DOCTYPE = "<!DOCTYPE html>"; function isAssetToken(token: unknown): token is AssetToken { return typeof token === "string" && token.startsWith("mxa_"); } function isDocumentToken(token: unknown): token is DocumentToken { return typeof token === "string" && token.startsWith("mxt_"); } function isObsidianMarkdown( x: Document,
-
@@ -43,6 +65,31 @@ from: readonly string[],): string { return Array.from({ length: from.length }, () => "../").join("") + path.join("/"); } function mdastToHast(input: Mdast.Nodes) { return ofmHtml(toHast(input, { // @ts-expect-error: unist-related libraries heavily relies on ambient module declarations, // which Deno does not support. APIs also don't accept type parameters. handlers: { ...ofmToHastHandlers({ callout: { generateIcon(type) { return { type: "element", tagName: "MacanaOfmCalloutIcon", properties: { type, }, children: [], }; }, }, }), ...syntaxHighlightingHandlers(), }, allowDangerousHtml: true, })); } export interface Assets {
-
@@ -191,16 +238,145 @@ const { fileSystemWriter } = buildParameters;if ("file" in item) { if (isObsidianMarkdown(item) || isJSONCanvas(item)) { const assetWrites: Promise<unknown>[] = []; const enc = new TextEncoder(); const basePath = [ ...pathPrefix, item.metadata.name, ]; const writeTasks: Promise<unknown>[] = []; switch (item.content.kind) { case "json_canvas": { const content = await jsonCanvas.mapNode( item.content.content, async (node) => { switch (node.type) { case "text": { await macanaReplaceAssetTokens( node.text, async (token) => { const file = tree.exchangeToken(token); if (item.content.kind === "json_canvas") { await jsonCanvas.mapText(item.content.content, async (node) => { writeTasks.push(fileSystemWriter.write( file.path, await file.read(), )); return toRelativePath(file.path, item.path); }, ); await macanaReplaceDocumentToken( node.text, async (token) => { const document = tree.exchangeToken(token); return { path: toRelativePath( [...document.path, ""], item.path, ), }; }, ); return { ...node, text: <HastRenderer.View node={mdastToHast(node.text)} />, }; } case "file": { if (isAssetToken(node.file)) { const file = tree.exchangeToken(node.file); writeTasks.push( fileSystemWriter.write(file.path, await file.read()), ); return { ...node, file: toRelativePath(file.path, item.path), }; } if (isDocumentToken(node.file)) { const doc = tree.exchangeToken(node.file); return { ...node, file: toRelativePath( [...doc.path, "embed.html"], item.path, ), }; } return node; } default: { return node; } } }, ); const document = item as Document<JSONCanvasDocument<Mdast.Nodes>>; const html = DOCTYPE + renderSSR( () => ( // Adds 1 to depth due to `<name>/index.html` conversion. <PathResolverProvider depth={pathPrefix.length + 1}> <Html.JSONCanvasView tree={tree} copyright={this.#copyright} content={content} document={item} language={item.metadata.language || parentLanguage} assets={assets} /> </PathResolverProvider> ), ); writeTasks.push( fileSystemWriter.write([ ...basePath, "index.html", ], enc.encode(html)), ); const embed = DOCTYPE + renderSSR( () => ( <PathResolverProvider depth={pathPrefix.length + 1}> <Html.JSONCanvasEmbed document={document} language={item.metadata.language || parentLanguage} content={content} assets={assets} /> </PathResolverProvider> ), ); writeTasks.push( fileSystemWriter.write([ ...basePath, "embed.html", ], enc.encode(embed)), ); await Promise.all(writeTasks); return; } case "obsidian_markdown": { await macanaReplaceAssetTokens( node.text, item.content.content, async (token) => { const file = tree.exchangeToken(token); assetWrites.push(fileSystemWriter.write( writeTasks.push(fileSystemWriter.write( file.path, await file.read(), ));
-
@@ -210,7 +386,7 @@ },); await macanaReplaceDocumentToken( node.text, item.content.content, async (token) => { const document = tree.exchangeToken(token);
-
@@ -219,62 +395,68 @@ path: toRelativePath([...document.path, ""], item.path),}; }, ); }); } if (item.content.kind === "obsidian_markdown") { await macanaReplaceAssetTokens( item.content.content, async (token) => { const file = tree.exchangeToken(token); const document = item as Document<ObsidianMarkdownDocument>; const hast = mdastToHast(item.content.content); const renderedNode = <HastRenderer.View node={hast} />; assetWrites.push(fileSystemWriter.write( file.path, await file.read(), )); return toRelativePath(file.path, item.path); }, ); await macanaReplaceDocumentToken( item.content.content, async (token) => { const document = tree.exchangeToken(token); const html = DOCTYPE + renderSSR( () => ( // Adds 1 to depth due to `<name>/index.html` conversion. <PathResolverProvider depth={pathPrefix.length + 1}> <Html.ObsidianMarkdownView document={item} language={item.metadata.language || parentLanguage} assets={assets} content={renderedNode} tree={tree} copyright={this.#copyright} toc={tocMut(hast).map((node) => { return mapTocItem( node, (item) => ( <HastRenderer.View node={{ type: "root", children: item }} /> ), ); })} /> </PathResolverProvider> ), ); return { path: toRelativePath([...document.path, ""], item.path), }; }, ); } writeTasks.push( fileSystemWriter.write([ ...basePath, "index.html", ], enc.encode(html)), ); const html = "<!DOCTYPE html>" + renderSSR( () => ( // Adds 1 to depth due to `<name>/index.html` conversion. <PathResolverProvider depth={pathPrefix.length + 1}> <Html.View tree={tree} document={item} language={item.metadata.language || parentLanguage} copyright={this.#copyright} assets={assets} /> </PathResolverProvider> ), ); const embed = DOCTYPE + renderSSR( () => ( <PathResolverProvider depth={pathPrefix.length + 1}> <Html.ObsidianMarkdownEmbed document={document} language={item.metadata.language || parentLanguage} content={hast} assets={assets} /> </PathResolverProvider> ), ); const enc = new TextEncoder(); writeTasks.push( fileSystemWriter.write([ ...basePath, "embed.html", ], enc.encode(embed)), ); await Promise.all([ ...assetWrites, fileSystemWriter.write([ ...pathPrefix, item.metadata.name, "index.html", ], enc.encode(html)), ]); return; await Promise.all(writeTasks); return; } } } throw new Error(`Unsupported content type: ${item.content.kind}`);
-
-
-
@@ -0,0 +1,107 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 /** @jsx h */ /** @jsxFrag Fragment */ import { h } from "../../../../deps/deno.land/x/nano_jsx/mod.ts"; import type * as Hast from "../../../../deps/esm.sh/hast/types.ts"; import * as jsxRuntime from "../../../../deps/deno.land/x/nano_jsx/jsx-runtime/index.ts"; 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 { css } from "../../css.ts"; import { type CalloutType, } from "../../../../content_parser/obsidian_markdown.ts"; import * as LucideIcons from "../lucide_icons.tsx"; export const styles = css``; function nanoifyProps(props: HastToJSXRuntime.Props): HastToJSXRuntime.Props { const ret: HastToJSXRuntime.Props = {}; for (const key in props) { switch (props[key]) { // nanojsx cannot handle falsy attribute correctly case false: case null: break; // ideal `true` for boolean attribute is empty string, but nanojsx emits `"true"`. case true: ret[key] = ""; break; default: ret[key] = props[key]; break; } } return ret; } export function render(hast: Hast.Nodes) { return toJsxRuntime(hast, { components: { MacanaOfmCalloutIcon({ type }: { type: CalloutType }) { switch (type) { case "abstract": return ( <LucideIcons.ClipboardList role="img" aria-label="Clipboard icon" /> ); case "info": return <LucideIcons.Info role="img" aria-label="Info icon" />; case "todo": return ( <LucideIcons.CircleCheck role="img" aria-label="Check icon" /> ); case "tip": return <LucideIcons.Flame role="img" aria-label="Flame icon" />; case "success": return <LucideIcons.Check role="img" aria-label="Check icon" />; case "question": return ( <LucideIcons.CircleHelp role="img" aria-label="Question icon" /> ); case "warning": return ( <LucideIcons.TriangleAlert role="img" aria-label="Warning icon" /> ); case "failure": return <LucideIcons.X role="img" aria-label="Cross icon" />; case "danger": return <LucideIcons.Zap role="img" aria-label="Lightning icon" />; case "bug": return <LucideIcons.Bug role="img" aria-label="Bug icon" />; case "example": return <LucideIcons.List role="img" aria-label="List icon" />; case "quote": return <LucideIcons.Quote role="img" aria-label="Quote icon" />; case "note": default: return <LucideIcons.Pencil role="img" aria-label="Pencil icon" />; } }, }, Fragment: jsxRuntime.Fragment, jsx(type, props, key) { return jsxRuntime.jsx(type, nanoifyProps(props), key || ""); }, jsxs(type, props, key) { return jsxRuntime.jsxs(type, nanoifyProps(props), key || ""); }, }); } export interface ViewProps { node: Hast.Nodes; } export function View({ node }: ViewProps) { return render(node); }
-
-
-
@@ -5,6 +5,7 @@/** @jsx h */ /** @jsxFrag Fragment */ import { extname } from "../../../../deps/deno.land/std/path/mod.ts"; import { Fragment, h } from "../../../../deps/deno.land/x/nano_jsx/mod.ts"; import { logger } from "../../../../logger.ts";
-
@@ -166,20 +167,90 @@ node: FileNode;} function FileNodeRenderer({ node }: FileNodeRendererProps) { return ( <foreignObject x={node.x} y={node.y} width={node.width} height={node.height} > <iframe xmlns="http://www.w3.org/1999/xhtml" style={`width: ${node.width}px;height: ${node.height}px;`} src={node.file} /> </foreignObject> ); const ext = extname(node.file); switch (ext) { case ".jpg": case ".jpeg": case ".avif": case ".bmp": case ".png": case ".svg": case ".webp": { return ( <foreignObject x={node.x} y={node.y} width={node.width} height={node.height} > <img xmlns="http://www.w3.org/1999/xhtml" style="max-width:100%;max-height:100%;object-fit:contain;" src={node.file} /> </foreignObject> ); } case ".mkv": case ".mov": case ".mp4": case ".ogv": case ".webm": { return ( <foreignObject x={node.x} y={node.y} width={node.width} height={node.height} > <video xmlns="http://www.w3.org/1999/xhtml" style="max-width:100%;max-height:100%;object-fit:contain;" src={node.file} /> </foreignObject> ); } case ".flac": case ".m4a": case ".mp3": case ".ogg": case ".wav": case ".3gp": { return ( <foreignObject x={node.x} y={node.y} width={node.width} height={node.height} > <div xmlns="http://www.w3.org/1999/xhtml" style="width:100%;height:100%;display:grid;place-items:center;" > <audio src={node.file} /> </div> </foreignObject> ); } default: { return ( <foreignObject x={node.x} y={node.y} width={node.width} height={node.height} > <iframe xmlns="http://www.w3.org/1999/xhtml" style={`width: ${node.width}px;height: ${node.height}px;`} src={node.file} /> </foreignObject> ); } } } type VerticalAlign = "top" | "center" | "bottom";
-
-
-
@@ -6,120 +6,31 @@ /** @jsx h *//** @jsxFrag Fragment */ import { Fragment, h } from "../../../deps/deno.land/x/nano_jsx/mod.ts"; import * as jsxRuntime from "../../../deps/deno.land/x/nano_jsx/jsx-runtime/index.ts"; import { toHast } from "../../../deps/esm.sh/mdast-util-to-hast/mod.ts"; import type * as Mdast from "../../../deps/esm.sh/mdast/types.ts"; 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 * as Hast from "../../../deps/esm.sh/hast/types.ts"; import type { Document, DocumentMetadata, DocumentTree, } from "../../../types.ts"; import { type CalloutType, type ObsidianMarkdownDocument, ofmHtml, ofmToHastHandlers, } from "../../../content_parser/obsidian_markdown.ts"; import type { JSONCanvasDocument } from "../../../content_parser/json_canvas.ts"; import * as jsonCanvas from "../../../content_parser/json_canvas/utils.ts"; import type { JSONCanvas } from "../../../content_parser/json_canvas/types.ts"; import { usePathResolver } from "../contexts/path_resolver.tsx"; import * as css from "../css.ts"; import { globalStyles } from "./global_styles.ts"; import { mapTocItem, tocMut } from "../hast/hast_util_toc_mut.ts"; import { syntaxHighlightingHandlers } from "../mdast/syntax_highlighting_handlers.ts"; import { TocItem } from "../hast/hast_util_toc_mut.ts"; import type { Assets } from "../builder.tsx"; import * as LucideIcons from "./lucide_icons.tsx"; import * as HastRenderer from "./atoms/hast_renderer.tsx"; import * as JSONCanvasRenderer from "./atoms/json_canvas_renderer.tsx"; import * as DocumentTreeUI from "./organisms/document_tree.tsx"; import * as Footer from "./organisms/footer.tsx"; import * as Toc from "./organisms/toc.tsx"; import * as SiteLayout from "./templates/site_layout.tsx"; function nanoifyProps(props: HastToJSXRuntime.Props): HastToJSXRuntime.Props { const ret: HastToJSXRuntime.Props = {}; for (const key in props) { switch (props[key]) { // nanojsx cannot handle falsy attribute correctly case false: case null: break; // ideal `true` for boolean attribute is empty string, but nanojsx emits `"true"`. case true: ret[key] = ""; break; default: ret[key] = props[key]; break; } } return ret; } function toNode(hast: ReturnType<typeof toHast>) { return toJsxRuntime(hast, { components: { MacanaOfmCalloutIcon({ type }: { type: CalloutType }) { switch (type) { case "abstract": return ( <LucideIcons.ClipboardList role="img" aria-label="Clipboard icon" /> ); case "info": return <LucideIcons.Info role="img" aria-label="Info icon" />; case "todo": return ( <LucideIcons.CircleCheck role="img" aria-label="Check icon" /> ); case "tip": return <LucideIcons.Flame role="img" aria-label="Flame icon" />; case "success": return <LucideIcons.Check role="img" aria-label="Check icon" />; case "question": return ( <LucideIcons.CircleHelp role="img" aria-label="Question icon" /> ); case "warning": return ( <LucideIcons.TriangleAlert role="img" aria-label="Warning icon" /> ); case "failure": return <LucideIcons.X role="img" aria-label="Cross icon" />; case "danger": return <LucideIcons.Zap role="img" aria-label="Lightning icon" />; case "bug": return <LucideIcons.Bug role="img" aria-label="Bug icon" />; case "example": return <LucideIcons.List role="img" aria-label="List icon" />; case "quote": return <LucideIcons.Quote role="img" aria-label="Quote icon" />; case "note": default: return <LucideIcons.Pencil role="img" aria-label="Pencil icon" />; } }, }, Fragment: jsxRuntime.Fragment, jsx(type, props, key) { return jsxRuntime.jsx(type, nanoifyProps(props), key || ""); }, jsxs(type, props, key) { return jsxRuntime.jsxs(type, nanoifyProps(props), key || ""); }, }); } export const styles = css.join( globalStyles, LucideIcons.styles,
-
@@ -128,6 +39,7 @@ Footer.styles,SiteLayout.styles, Toc.styles, JSONCanvasRenderer.styles, HastRenderer.styles, ); interface DatetimeTextProps {
-
@@ -175,118 +87,19 @@ </>); } function mdastToHast(input: Mdast.Nodes) { return ofmHtml(toHast(input, { // @ts-expect-error: unist-related libraries heavily relies on ambient module declarations, // which Deno does not support. APIs also don't accept type parameters. handlers: { ...ofmToHastHandlers({ callout: { generateIcon(type) { return { type: "element", tagName: "MacanaOfmCalloutIcon", properties: { type, }, children: [], }; }, }, }), ...syntaxHighlightingHandlers(), }, allowDangerousHtml: true, })); } interface ObsidianMarkdownBodyProps extends ViewProps { content: ObsidianMarkdownDocument; } function ObsidianMarkdownBody( { content, document, tree, copyright, assets }: ObsidianMarkdownBodyProps, ) { const hast = mdastToHast(content.content); const toc = tocMut(hast).map((item) => mapTocItem(item, (item) => toNode({ type: "root", children: item })) ); const contentNodes = toNode(hast); return ( <SiteLayout.View aside={toc.length > 0 && <Toc.View toc={toc} />} nav={ <DocumentTreeUI.View tree={tree} currentPath={document.path} /> } footer={<Footer.View copyright={copyright} />} logoImage={assets.siteLogo} defaultDocument={tree.defaultDocument} > <h1>{document.metadata.title}</h1> <MetadataDates metadata={document.metadata} /> {contentNodes} </SiteLayout.View> ); } interface JSONCanvasBodyProps extends ViewProps { content: JSONCanvasDocument<Mdast.Nodes>; } function JSONCanvasBody( { content, document, copyright, tree, assets }: JSONCanvasBodyProps, ) { const canvas = jsonCanvas.mapTextSync(content.content, (node) => { return toNode(mdastToHast(node.text)); }); return ( <SiteLayout.View nav={ <DocumentTreeUI.View tree={tree} currentPath={document.path} /> } footer={<Footer.View copyright={copyright} />} logoImage={assets.siteLogo} defaultDocument={tree.defaultDocument} > <h1>{document.metadata.title}</h1> <MetadataDates metadata={document.metadata} /> <JSONCanvasRenderer.View data={canvas} /> </SiteLayout.View> ); } export interface ViewProps { document: Document< ObsidianMarkdownDocument | JSONCanvasDocument<Mdast.Nodes> >; /** * Root document tree, for navigations and links. */ tree: DocumentTree; interface ViewProps { document: Document; language: string; copyright: string; assets: Assets; assets: Assets; children: JSX.ElementChildrenAttribute["children"]; } export function View( { assets, ...props }: ViewProps, function View( { assets, language, document, children }: ViewProps, ) { const { document, language } = props; const path = usePathResolver(); return (
-
@@ -315,21 +128,7 @@ />)} </head> <body> {document.content.kind === "json_canvas" ? ( <JSONCanvasBody content={document.content} assets={assets} {...props} /> ) : ( <ObsidianMarkdownBody content={document.content} assets={assets} {...props} /> )} {children} <script> {`document.querySelectorAll("[data-macana-datetime]").forEach(el => { const datetime = new Date(el.dataset.macanaDatetime);
-
@@ -341,3 +140,137 @@ </body></html> ); } export interface ObsidianMarkdownViewProps extends Omit<ViewProps, "children"> { tree: DocumentTree; copyright: string; // nano-jsx does not ship any usable type definition content: unknown; // nano-jsx does not ship any usable type definition toc: readonly TocItem<unknown>[]; } export function ObsidianMarkdownView( { copyright, tree, content, toc, ...props }: ObsidianMarkdownViewProps, ) { const { document, assets } = props; return ( <View {...props}> <SiteLayout.View aside={toc.length > 0 && <Toc.View toc={toc} />} nav={ <DocumentTreeUI.View tree={tree} currentPath={document.path} /> } footer={<Footer.View copyright={copyright} />} logoImage={assets.siteLogo} defaultDocument={tree.defaultDocument} > <h1>{document.metadata.title}</h1> <MetadataDates metadata={document.metadata} /> {content} </SiteLayout.View> </View> ); } export interface JSONCanvasViewProps extends Omit<ViewProps, "children"> { tree: DocumentTree; copyright: string; // nano-jsx does not ship usable type definition content: JSONCanvas<unknown>; } export function JSONCanvasView( { tree, copyright, content, ...props }: JSONCanvasViewProps, ) { const { document, assets } = props; return ( <View {...props}> <SiteLayout.View nav={ <DocumentTreeUI.View tree={tree} currentPath={document.path} /> } footer={<Footer.View copyright={copyright} />} logoImage={assets.siteLogo} defaultDocument={tree.defaultDocument} > <h1>{document.metadata.title}</h1> <MetadataDates metadata={document.metadata} /> <JSONCanvasRenderer.View data={content} /> </SiteLayout.View> </View> ); } interface BaseEmbedProps { document: Document; language: string; assets: Assets; children: JSX.ElementChildrenAttribute["children"]; } function BaseEmbed({ document, language, assets, children }: BaseEmbedProps) { const path = usePathResolver(); return ( <html lang={language}> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="robot" content="noindex" /> <title>{document.metadata.title}</title> <link rel="stylesheet" href={path.resolve(assets.globalCss)} /> </head> <body> {children} </body> </html> ); } export interface ObsidianMarkdownEmbedProps extends Omit<BaseEmbedProps, "children"> { content: Hast.Nodes; } export function ObsidianMarkdownEmbed( { content, ...rest }: ObsidianMarkdownEmbedProps, ) { return ( <BaseEmbed {...rest}> <HastRenderer.View node={content} /> </BaseEmbed> ); } export interface JSONCanvasEmbedProps extends Omit<BaseEmbedProps, "children"> { // nano-jsx does not ship usable TypeScript definition. content: JSONCanvas<unknown>; } export function JSONCanvasEmbed({ content, ...rest }: JSONCanvasEmbedProps) { return ( <BaseEmbed {...rest}> <JSONCanvasRenderer.View data={content} /> </BaseEmbed> ); }
-