Changes
10 changed files (+194/-35)
-
-
@@ -27,6 +27,8 @@ path: readonly string[],): DocumentToken | Promise<DocumentToken>; } export interface ContentParser { parse(params: ParseParameters): Promise<ContentParseResult>; export interface ContentParser< Content extends DocumentContent = DocumentContent, > { parse(params: ParseParameters): Promise<ContentParseResult<Content>>; }
-
-
-
@@ -2,10 +2,14 @@ // 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 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 { parseMarkdown } from "./obsidian_markdown.ts"; export class JSONCanvasParseError extends Error {}
-
@@ -25,13 +29,16 @@ this.cause = cause;} } export type JSONCanvasDocument = DocumentContent< export type JSONCanvasDocument<T> = DocumentContent< "json_canvas", JSONCanvas JSONCanvas<T> >; export class JSONCanvasParser implements ContentParser { async parse({ fileReader }: ParseParameters): Promise<JSONCanvasDocument> { export class JSONCanvasParser implements ContentParser<JSONCanvasDocument<Mdast.Nodes>> { async parse( { fileReader, getDocumentToken, getAssetToken }: ParseParameters, ): Promise<JSONCanvasDocument<Mdast.Nodes>> { const text = new TextDecoder().decode(await fileReader.read()); let json: unknown;
-
@@ -47,7 +54,12 @@ }return { kind: "json_canvas", content: json, content: await mapText(json, async (node) => { return parseMarkdown(node.text, { getAssetToken, getDocumentToken, }); }), }; } }
-
-
-
@@ -137,7 +137,11 @@return true; } export interface TextNode extends GenericNode { /** * You can change `Markdown` type parameter to other types. * For example, AST if it needs to be displayed as HTML. */ export interface TextNode<Markdown = string> extends GenericNode { type: "text"; /**
-
@@ -146,7 +150,7 @@ * NOTE: Although the spec does not define what "Markdown" is, Obsidian seems to* using/parsing their Obsidian Flavored Markdown. Parsing and displaying * of this property is UB. */ text: string; text: Markdown; } export function isTextNode(x: GenericNode): x is TextNode {
-
@@ -269,7 +273,11 @@return true; } export type Node = TextNode | FileNode | LinkNode | GroupNode; export type Node<Markdown = string> = | TextNode<Markdown> | FileNode | LinkNode | GroupNode; export function isNode(x: unknown): x is Node { if (!isGenericNode(x)) {
-
@@ -398,8 +406,8 @@return true; } export interface JSONCanvas { nodes?: Node[]; export interface JSONCanvas<Markdown = string> { nodes?: Node<Markdown>[]; edges?: Edge[]; }
-
-
-
@@ -0,0 +1,59 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import type { JSONCanvas, Node, TextNode } from "./types.ts"; export function mapTextSync<A, B>( 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>, ): 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>); } 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 { ...tree, nodes, }; }
-
-
-
@@ -96,7 +96,7 @@ */frontmatter?: boolean; } async function parseMarkdown( export async function parseMarkdown( markdown: string | Uint8Array, { getAssetToken, getDocumentToken }: Pick< ParseParameters,
-
@@ -122,7 +122,8 @@return mdast; } export class ObsidianMarkdownParser implements ContentParser { export class ObsidianMarkdownParser implements ContentParser<ObsidianMarkdownDocument> { #frontmatter: boolean; constructor({ frontmatter = false }: ObsidianMarkdownParserOptions = {}) {
-
-
-
@@ -0,0 +1,15 @@{ "nodes":[ {"id":"a2a6b20bb3129a7a","x":-125,"y":-30,"width":250,"height":450,"type":"text","text":"# Heading1\n## Heading2\n### Heading3\n\n> [!warning]-\n> Yay\n\n- List Item\n- List Item\n\t- Nested List Item\n\n```ts\nconst foo = \"bar\"\n```"}, {"id":"f64025c69470260f","x":318,"y":32,"width":400,"height":400,"type":"file","file":"en/Roadmap.md"}, {"id":"e42673f255295e01","x":-200,"y":-720,"width":400,"height":400,"type":"file","file":"Assets/dog.jpg"}, {"id":"21bafbf34e0554ed","x":500,"y":-720,"width":400,"height":400,"type":"link","url":"https://wikipedia.org"} ], "edges":[ {"id":"4a196cf19d0b3085","fromNode":"a2a6b20bb3129a7a","fromSide":"right","toNode":"f64025c69470260f","toSide":"left"}, {"id":"8b9aeb669e298cc6","fromNode":"a2a6b20bb3129a7a","fromSide":"top","toNode":"e42673f255295e01","toSide":"bottom"}, {"id":"822080eccc4237a9","fromNode":"e42673f255295e01","fromSide":"right","toNode":"f64025c69470260f","toSide":"top"}, {"id":"4741e419a45256ae","fromNode":"e42673f255295e01","fromSide":"bottom","toNode":"f64025c69470260f","toSide":"top"}, {"id":"19dc23365e8229c5","fromNode":"21bafbf34e0554ed","fromSide":"left","toNode":"a2a6b20bb3129a7a","toSide":"top"} ] }
-
-
-
@@ -48,7 +48,7 @@ - [ ] JSONCanvas- [ ] Node rendering - [x] Basic shapes - [x] Colors - [ ] Text node - [x] Text node - [ ] Group node - [ ] Link node - [ ] File node
-
-
-
@@ -5,6 +5,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 { logger } from "../../logger.ts";
-
@@ -15,6 +16,7 @@ macanaReplaceDocumentToken,type ObsidianMarkdownDocument, } 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 * as css from "./css.ts";
-
@@ -29,7 +31,9 @@ ): x is Document<ObsidianMarkdownDocument> {return x.content.kind === "obsidian_markdown"; } function isJSONCanvas(x: Document): x is Document<JSONCanvasDocument> { function isJSONCanvas( x: Document, ): x is Document<JSONCanvasDocument<Mdast.Nodes>> { return x.content.kind === "json_canvas"; }
-
@@ -188,6 +192,35 @@if ("file" in item) { if (isObsidianMarkdown(item) || isJSONCanvas(item)) { const assetWrites: Promise<unknown>[] = []; if (item.content.kind === "json_canvas") { await jsonCanvas.mapText(item.content.content, async (node) => { await macanaReplaceAssetTokens( node.text, async (token) => { const file = tree.exchangeToken(token); assetWrites.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), }; }, ); }); } if (item.content.kind === "obsidian_markdown") { await macanaReplaceAssetTokens(
-
-
-
@@ -8,6 +8,7 @@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";
-
@@ -24,6 +25,7 @@ ofmCommentToHastHandlers,ofmWikilinkToHastHandlers, } 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 { usePathResolver } from "../contexts/path_resolver.tsx"; import * as css from "../css.ts";
-
@@ -174,14 +176,8 @@ </>); } interface ObsidianMarkdownBodyProps extends ViewProps { content: ObsidianMarkdownDocument; } function ObsidianMarkdownBody( { content, document, tree, copyright, assets }: ObsidianMarkdownBodyProps, ) { const hast = toHast(content.content, { function mdastToHast(input: Mdast.Nodes) { return 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: {
-
@@ -202,6 +198,16 @@ ...ofmWikilinkToHastHandlers,...syntaxHighlightingHandlers(), }, }); } 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 }))
-
@@ -230,12 +236,16 @@ );} interface JSONCanvasBodyProps extends ViewProps { content: JSONCanvasDocument; 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={
-
@@ -250,13 +260,15 @@ defaultDocument={tree.defaultDocument}> <h1>{document.metadata.title}</h1> <MetadataDates metadata={document.metadata} /> <JSONCanvasRenderer.View data={content.content} /> <JSONCanvasRenderer.View data={canvas} /> </SiteLayout.View> ); } export interface ViewProps { document: Document<ObsidianMarkdownDocument | JSONCanvasDocument>; document: Document< ObsidianMarkdownDocument | JSONCanvasDocument<Mdast.Nodes> >; /** * Root document tree, for navigations and links.
-
-
-
@@ -19,6 +19,7 @@ } from "../../../content_parser/json_canvas/types.ts";const enum C { Wrapper = "jcr--wr", Embed = "jcr--em", } export const styles = css`
-
@@ -32,6 +33,17 @@ border: 1px solid var(--color-border);border-radius: calc(1rem / 4); padding: 4px; } .${C.Embed} { padding: 4px 8px; overflow: auto; } .${C.Embed} > p { margin-block-start: calc(var(--baseline) * 0.5rem); } .${C.Embed} > :first-child { margin-block-start: 0; } `; interface Rect {
-
@@ -42,7 +54,10 @@ height: number;} // TODO: Automatically calculate padding based on stroke-width, edges and shadows. function getBoundingBox(canvas: JSONCanvas, padding: number = 20): Rect { function getBoundingBox( canvas: JSONCanvas<unknown>, padding: number = 20, ): Rect { let minX: number = 0; let minY: number = 0; let maxX: number = 0;
-
@@ -65,7 +80,7 @@ }type Point2D = readonly [number, number]; function getConnectionPoint(node: Node, side: NodeSide): Point2D { function getConnectionPoint(node: Node<unknown>, side: NodeSide): Point2D { switch (side) { case "top": return [node.x + node.width * 0.5, node.y];
-
@@ -93,7 +108,7 @@ * Which node side faces the point?* We need this function because `fromSide` and `toSide` property is optional... * but Obsidian does not render Canvas if either of one is missing. wtf */ function getNearestSideToPoint(node: Node, p: Point2D): NodeSide { function getNearestSideToPoint(node: Node<unknown>, p: Point2D): NodeSide { const center: Point2D = [ node.x + node.width * 0.5, node.y + node.height * 0.5,
-
@@ -142,9 +157,9 @@ return "top";} function getClosestSides( a: Node, a: Node<unknown>, aSide: NodeSide | undefined, b: Node, b: Node<unknown>, bSide: NodeSide | undefined, ): readonly [NodeSide, NodeSide] { if (aSide && bSide) {
-
@@ -252,7 +267,8 @@ );} export interface JSONCanvasRendererProps { data: JSONCanvas; // nano-jsx does not ship working typings, thus "unknown" (no JSX.Element). data: JSONCanvas<unknown>; radius?: number;
-
@@ -275,7 +291,7 @@ /*** Edges refer nodes by ID. This map helps and optimizes its retrieving operation. * Without using `Map`, lookup takes `O(N)`. */ const nodes = new Map<string, Node>( const nodes = new Map<string, Node<unknown>>( data.nodes?.map((node) => [node.id, node]), );
-
@@ -332,7 +348,8 @@ height={node.height}> <div xmlns="http://www.w3.org/1999/xhtml" style="padding: 1em;white-space: pre-wrap;" style={`width: ${node.width}px;height: ${node.height}px;`} className={C.Embed} > {node.text} </div>
-