Changes
7 changed files (+551/-76)
-
-
@@ -258,6 +258,43 @@ function defaultTitleId(count: number): string {return `_ofm_callout__${count}`; } export function parseOfmCalloutNode( state: State, node: OfmCallout, ): { title: Hast.ElementContent[]; body: Hast.ElementContent[]; type: CalloutType; } { const titleTextTitleCased = node.calloutType.slice(0, 1).toUpperCase() + node.calloutType.slice(1).toLowerCase(); const type = normalizeType(node.calloutType); let mdastTitle: OfmCalloutTitle | null = null; const mdastBody: (Mdast.BlockContent | Mdast.DefinitionContent)[] = []; for (const child of node.children) { if (child.type === "ofmCalloutTitle") { mdastTitle = child; } else { mdastBody.push(child); } } const title: Hast.ElementContent[] = mdastTitle ? state.all({ type: "paragraph", children: mdastTitle.children, }) : [{ type: "text", value: titleTextTitleCased, }]; // @ts-expect-error: unist-related libraries heavily relies on ambient module declarations, // which Deno does not support. APIs also don't accept type parameters. return { title, body: state.all({ ...node, children: mdastBody }), type }; } export function ofmCalloutToHastHandlers( { generateTitleId = defaultTitleId, generateIcon }: OfmCalloutToHastHandlersOptions = {},
-
-
-
@@ -6,7 +6,11 @@ /** @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 type * as Hast from "../../deps/esm.sh/hast/types.ts"; import { type State, toHast, } from "../../deps/esm.sh/mdast-util-to-hast/mod.ts"; import { logger } from "../../logger.ts";
-
@@ -18,6 +22,10 @@ type ObsidianMarkdownDocument,ofmHtml, ofmToHastHandlers, } from "../../content_parser/obsidian_markdown.ts"; import { type OfmCallout, parseOfmCalloutNode, } from "../../content_parser/obsidian_markdown/mdast_util_ofm_callout.ts"; import type { JSONCanvasDocument } from "../../content_parser/json_canvas.ts"; import * as jsonCanvas from "../../content_parser/json_canvas/utils.ts"; import type {
-
@@ -69,8 +77,6 @@ }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: {
-
@@ -86,6 +92,35 @@ };}, }, }), // @ts-expect-error: unist-related libraries heavily relies on ambient module declarations, // which Deno does not support. APIs also don't accept type parameters. ofmCallout(state: State, node: OfmCallout): Hast.Nodes { const { title, body, type } = parseOfmCalloutNode(state, node); return { type: "element", tagName: "macana-ofm-callout", properties: { type: type, foldable: node.isFoldable, defaultExpanded: node.defaultExpanded, }, children: [ { type: "element", tagName: "macana-ofm-callout-title", properties: {}, children: title, }, { type: "element", tagName: "macana-ofm-callout-body", properties: {}, children: body, }, ], }; }, ...syntaxHighlightingHandlers(), }, allowDangerousHtml: true,
-
-
-
@@ -5,20 +5,15 @@/** @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 { join as joinCss } from "../../css.ts"; import * as callout from "../from-hast/callout.tsx"; import * as LucideIcons from "../lucide_icons.tsx"; export const styles = css``; export const styles = joinCss(callout.styles); function nanoifyProps(props: HastToJSXRuntime.Props): HastToJSXRuntime.Props { const ret: HastToJSXRuntime.Props = {};
-
@@ -45,48 +40,9 @@export function render(hast: Hast.Nodes) { return toJsxRuntime(hast, { components: { "macana-ofm-callout-icon"({ 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" />; } }, "macana-ofm-callout": callout.MacanaOfmCallout, "macana-ofm-callout-title": callout.MacanaOfmCalloutTitle, "macana-ofm-callout-body": callout.MacanaOfmCalloutBody, }, Fragment: jsxRuntime.Fragment, jsx(type, props, key) {
-
-
-
@@ -0,0 +1,368 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 /** @jsx h */ import { createContext, h, useContext, } from "../../../../deps/deno.land/x/nano_jsx/mod.ts"; import { type CalloutType } from "../../../../content_parser/obsidian_markdown.ts"; import { css } from "../../css.ts"; import * as LucideIcons from "../lucide_icons.tsx"; const enum C { Root = "fh-co--root", Title = "fh-co--title", TitleText = "fh-co--tt", Body = "fh-co--b", Icon = "fh-co--i", Bg = "fg-co--g", Chevron = "fg-co--c", } export const styles = css` .${C.Root} { --_macana-callout-overlay: hsl(0deg 0% 0% / 0.03); --_macana-callout-color: var(--callout-color-info, var(--obsidian-color-fallback)); position: relative; margin: 0; margin-top: calc(var(--baseline) * 1rem); padding: 0; line-height: calc(var(--baseline) * 1rem); max-width: 100%; font-size: 1rem; border: 1px solid var(--_macana-callout-color); border-radius: 4px; } .${C.Root}[data-type="todo"] { --_macana-callout-color: var(--callout-color-todo); } .${C.Root}[data-type="tip"] { --_macana-callout-color: var(--callout-color-tip); } .${C.Root}[data-type="success"] { --_macana-callout-color: var(--callout-color-success); } .${C.Root}[data-type="question"] { --_macana-callout-color: var(--callout-color-question); } .${C.Root}[data-type="warning"] { --_macana-callout-color: var(--callout-color-warning); } .${C.Root}[data-type="failure"] { --_macana-callout-color: var(--callout-color-failure); } .${C.Root}[data-type="danger"] { --_macana-callout-color: var(--callout-color-danger); } .${C.Root}[data-type="bug"] { --_macana-callout-color: var(--callout-color-bug); } .${C.Root}[data-type="example"] { --_macana-callout-color: var(--callout-color-example); } .${C.Root}[data-type="quote"] { --_macana-callout-color: var(--callout-color-quote); } .${C.Bg} { position: absolute; inset: 0; background-color: var(--_macana-callout-color); pointer-events: none; opacity: 0.02; } @media (prefers-color-scheme: dark) { .${C.Root} { --_macana-callout-overlay: hsl(0deg 0% 100% / 0.1); } .${C.Bg} { opacity: 0.05; } } .${C.Title} { font-size: 1.1rem; display: flex; justify-content: flex-start; align-items: center; gap: 0.25em; padding: calc(var(--baseline) * 0.5rem) 8px; border-bottom: 1px solid var(--_macana-callout-overlay); margin-top: 0; font-weight: 700; } summary.${C.Title} { cursor: pointer; } summary.${C.Title}:hover { background-color: var(--_macana-callout-overlay); } details:not([open]) > summary.${C.Title} { border-bottom-color: transparent; } .${C.Icon} { color: var(--_macana-callout-color); } .${C.TitleText} { line-height: calc(var(--baseline) * 1rem); } .${C.Chevron} { transition: transform 0.15s ease-out; } details:not([open]) .${C.Chevron} { transform: rotate(-90deg); } .${C.Body} { font-size: 1rem; padding: calc(var(--baseline) * 0.5rem) 12px; } .${C.Body} > :first-child { margin-block-start: 0; } `; interface CalloutContextValue { foldable: boolean; defaultExpanded: boolean; type: CalloutType; titleId: string; } const CalloutContext = createContext( { foldable: false, defaultExpanded: false, type: "note", titleId: "", } satisfies CalloutContextValue, ); export interface ViewProps { type?: CalloutType; foldable?: "" | false; defaultExpanded?: "" | false; children: JSX.ElementChildrenAttribute["children"]; } let counter = 0; export function MacanaOfmCallout( { type = "note", foldable = false, defaultExpanded = false, children }: ViewProps, ) { const titleId = "__macana_callout__" + (counter++).toString(16); const isFoldable = typeof foldable === "string"; const isDefaultExpanded = typeof defaultExpanded === "string"; const contextValue: CalloutContextValue = { foldable: isFoldable, defaultExpanded: isDefaultExpanded, type, titleId, }; if (!isFoldable) { return ( <aside className={C.Root} aria-labelledby={titleId} data-type={type} > <div className={C.Bg} /> <CalloutContext.Provider value={contextValue}> {children} </CalloutContext.Provider> </aside> ); } return ( <aside className={C.Root} aria-labelledby={titleId} data-type={type} > <div className={C.Bg} /> <details open={isDefaultExpanded ? "" : undefined}> <CalloutContext.Provider value={contextValue}> {children} </CalloutContext.Provider> </details> </aside> ); } interface IconProps { className?: string; type: CalloutType; } function Icon({ className, type }: IconProps) { switch (type) { case "abstract": return ( <LucideIcons.ClipboardList className={className} role="img" aria-label="Clipboard icon" /> ); case "info": return ( <LucideIcons.Info className={className} role="img" aria-label="Info icon" /> ); case "todo": return ( <LucideIcons.CircleCheck className={className} role="img" aria-label="Check icon" /> ); case "tip": return ( <LucideIcons.Flame className={className} role="img" aria-label="Flame icon" /> ); case "success": return ( <LucideIcons.Check className={className} role="img" aria-label="Check icon" /> ); case "question": return ( <LucideIcons.CircleHelp className={className} role="img" aria-label="Question icon" /> ); case "warning": return ( <LucideIcons.TriangleAlert className={className} role="img" aria-label="Warning icon" /> ); case "failure": return ( <LucideIcons.X className={className} role="img" aria-label="Cross icon" /> ); case "danger": return ( <LucideIcons.Zap className={className} role="img" aria-label="Lightning icon" /> ); case "bug": return ( <LucideIcons.Bug className={className} role="img" aria-label="Bug icon" /> ); case "example": return ( <LucideIcons.List className={className} role="img" aria-label="List icon" /> ); case "quote": return ( <LucideIcons.Quote className={className} role="img" aria-label="Quote icon" /> ); case "note": default: return ( <LucideIcons.Pencil className={className} role="img" aria-label="Pencil icon" /> ); } } export interface MacanaOfmCalloutTitleProps { children: JSX.ElementChildrenAttribute["children"]; } export function MacanaOfmCalloutTitle( { children }: MacanaOfmCalloutTitleProps, ) { const { titleId, type, foldable }: CalloutContextValue = useContext( CalloutContext, ); if (foldable) { return ( <summary className={C.Title} id={titleId}> <Icon className={C.Icon} type={type} /> <span className={C.TitleText}>{children}</span> <LucideIcons.ChevronDown className={C.Chevron} aria-hidden="true" /> </summary> ); } return ( <p className={C.Title} id={titleId}> <Icon className={C.Icon} type={type} /> <span className={C.TitleText}>{children}</span> </p> ); } export interface MacanaOfmCalloutBodyProps { children: JSX.ElementChildrenAttribute["children"]; } export function MacanaOfmCalloutBody({ children }: MacanaOfmCalloutBodyProps) { return <div className={C.Body}>{children}</div>; }
-
-
-
@@ -23,13 +23,34 @@ --color-border: #b4af9d;--color-subtle-overlay: hsl(0deg 0% 0% / 0.035); color: var(--color-fg); --canvas-color-red: #e03131; --canvas-color-orange: #f76707; --canvas-color-yellow: #fcc419; --canvas-color-green: #2f9e44; --canvas-color-cyan: #22b8cf; --canvas-color-purple: #ae3ec9; --canvas-color-fallback: #adb5bd; --obsidian-color-red: #e03131; --obsidian-color-blue: #4c6ef5; --obsidian-color-orange: #e03131; --obsidian-color-yellow: #fcc419; --obsidian-color-green: #2f9e44; --obsidian-color-cyan: #22b8cf; --obsidian-color-purple: #ae3ec9; --obsidian-color-fallback: #adb5bd; --canvas-color-red: var(--obsidian-color-red); --canvas-color-orange: var(--obisidian-color-orange); --canvas-color-yellow: var(--obisidian-color-yellow); --canvas-color-green: var(--obisidian-color-green); --canvas-color-cyan: var(--obisidian-color-cyan); --canvas-color-purple: var(--obisidian-color-purple); --canvas-color-fallback: var(--obisidian-color-fallback); --callout-color-info: var(--obsidian-color-blue); --callout-color-todo: var(--obsidian-color-blue); --callout-color-tip: var(--obsidian-color-cyan); --callout-color-success: var(--obsidian-color-green); --callout-color-question: var(--obsidian-color-yellow); --callout-color-warning: var(--obsidian-color-yellow); --callout-color-failure: var(--obsidian-color-red); --callout-color-danger: var(--obsidian-color-red); --callout-color-bug: var(--obsidian-color-red); --callout-color-example: var(--obsidian-color-purple); --callout-color-quote: var(--obsidian-color-fallback); --canvas-node-bg-opacity: 0.05; --canvas-node-stroke-width: 2px;
-
@@ -256,22 +277,6 @@hr { margin: 0; margin-top: calc(var(--baseline) * 1rem); } aside[data-ofm-callout-type] { margin: 0; margin-top: calc(var(--baseline) * 1rem); padding: calc(var(--baseline) * 0.5rem) 1em; line-height: calc(var(--baseline) * 1rem); max-width: 100%; font-size: 1rem; border: 1px solid var(--color-fg-light); border-radius: 4px; } aside[data-ofm-callout-type] > p:first-child, aside[data-ofm-callout-type] > details > summary { margin-top: 0; font-weight: 700; } /* Syntax highlight */
-
-
-
@@ -297,3 +297,21 @@ <path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z" /></svg> ); } export function ChevronDown({ className, ...rest }: LucideIconProps) { return ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" {...rest} className={cls(C.Icon, className)} > <path d="m6 9 6 6 6-6" /> </svg> ); }
-
-
-
@@ -0,0 +1,56 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 /** @jsx h */ import { h } from "../../../../deps/deno.land/x/nano_jsx/mod.ts"; import { type CalloutType } from "../../../../content_parser/obsidian_markdown.ts"; import { css } from "../../css.ts"; const enum C { Root = "m-co--root", } export const styles = css` border-radius: 4px; `; export interface ViewProps { title: JSX.ElementChildrenAttribute["children"]; type?: CalloutType; foldable?: boolean; defaultExpanded?: boolean; children: JSX.ElementChildrenAttribute["children"]; } let counter = 0; export function View( { title, type = "note", foldable = false, defaultExpanded = false, children }: ViewProps, ) { const Body = foldable ? "details" : "div"; const Title = foldable ? "summary" : "p"; const bodyProps = foldable ? { open: defaultExpanded ? "" : undefined, } : {}; const titleId = "__macana_callout__" + (counter++).toString(16); return ( <aside {...bodyProps} className={C.Root} aria-labelledby={titleId}> <Body data-ofm-callout-type={type}> <Title>{title}</Title> <div>{children}</div> </Body> </aside> ); }
-