From 4dfd1cd69690ada4d302fc68d3227969b3627d5e Mon Sep 17 00:00:00 2001 From: Alex Tavarez Date: Sun, 23 Nov 2025 12:35:11 -0500 Subject: [PATCH] Added new Argdown components for use with Obsidian markdown transformer plugin --- quartz/components/scripts/argdown.inline.ts | 282 +++++++++++++++++++ quartz/components/styles/argdown.inline.scss | 133 +++++++++ 2 files changed, 415 insertions(+) create mode 100644 quartz/components/scripts/argdown.inline.ts create mode 100644 quartz/components/styles/argdown.inline.scss diff --git a/quartz/components/scripts/argdown.inline.ts b/quartz/components/scripts/argdown.inline.ts new file mode 100644 index 0000000..31cab0f --- /dev/null +++ b/quartz/components/scripts/argdown.inline.ts @@ -0,0 +1,282 @@ +import { registerEscapeHandler, removeAllChildren } from "./util" +import { + argdown, + IArgdownRequest, + ParserPlugin, + ModelPlugin, + ColorPlugin, + HtmlExportPlugin +} from "@argdown/node" + +interface Position { + x: number + y: number +} + +class DiagramPanZoom { + private isDragging = false + private startPan: Position = { x: 0, y: 0 } + private currentPan: Position = { x: 0, y: 0 } + private scale = 1 + private readonly MIN_SCALE = 0.5 + private readonly MAX_SCALE = 3 + + cleanups: (() => void)[] = [] + + constructor( + private container: HTMLElement, + private content: HTMLElement, + ) { + this.setupEventListeners() + this.setupNavigationControls() + this.resetTransform() + } + + private setupEventListeners() { + // Mouse drag events + const mouseDownHandler = this.onMouseDown.bind(this) + const mouseMoveHandler = this.onMouseMove.bind(this) + const mouseUpHandler = this.onMouseUp.bind(this) + const resizeHandler = this.resetTransform.bind(this) + + this.container.addEventListener("mousedown", mouseDownHandler) + document.addEventListener("mousemove", mouseMoveHandler) + document.addEventListener("mouseup", mouseUpHandler) + window.addEventListener("resize", resizeHandler) + + this.cleanups.push( + () => this.container.removeEventListener("mousedown", mouseDownHandler), + () => document.removeEventListener("mousemove", mouseMoveHandler), + () => document.removeEventListener("mouseup", mouseUpHandler), + () => window.removeEventListener("resize", resizeHandler), + ) + } + + cleanup() { + for (const cleanup of this.cleanups) { + cleanup() + } + } + + private setupNavigationControls() { + const controls = document.createElement("div") + controls.className = "argdown-controls" + + // Zoom controls + const zoomIn = this.createButton("+", () => this.zoom(0.1)) + const zoomOut = this.createButton("-", () => this.zoom(-0.1)) + const resetBtn = this.createButton("Reset", () => this.resetTransform()) + + controls.appendChild(zoomOut) + controls.appendChild(resetBtn) + controls.appendChild(zoomIn) + + this.container.appendChild(controls) + } + + private createButton(text: string, onClick: () => void): HTMLButtonElement { + const button = document.createElement("button") + button.textContent = text + button.className = "argdown-control-button" + button.addEventListener("click", onClick) + window.addCleanup(() => button.removeEventListener("click", onClick)) + return button + } + + private onMouseDown(e: MouseEvent) { + if (e.button !== 0) return // Only handle left click + this.isDragging = true + this.startPan = { x: e.clientX - this.currentPan.x, y: e.clientY - this.currentPan.y } + this.container.style.cursor = "grabbing" + } + + private onMouseMove(e: MouseEvent) { + if (!this.isDragging) return + e.preventDefault() + + this.currentPan = { + x: e.clientX - this.startPan.x, + y: e.clientY - this.startPan.y, + } + + this.updateTransform() + } + + private onMouseUp() { + this.isDragging = false + this.container.style.cursor = "grab" + } + + private zoom(delta: number) { + const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE) + + // Zoom around center + const rect = this.content.getBoundingClientRect() + const centerX = rect.width / 2 + const centerY = rect.height / 2 + + const scaleDiff = newScale - this.scale + this.currentPan.x -= centerX * scaleDiff + this.currentPan.y -= centerY * scaleDiff + + this.scale = newScale + this.updateTransform() + } + + private updateTransform() { + this.content.style.transform = `translate(${this.currentPan.x}px, ${this.currentPan.y}px) scale(${this.scale})` + } + + private resetTransform() { + this.scale = 1 + const svg = this.content.querySelector("svg")! + this.currentPan = { + x: svg.getBoundingClientRect().width / 2, + y: svg.getBoundingClientRect().height / 2, + } + this.updateTransform() + } +} + +// const cssVars = [ +// "--secondary", +// "--tertiary", +// "--gray", +// "--light", +// "--lightgray", +// "--highlight", +// "--dark", +// "--darkgray", +// "--codeFont", +// ] as const + +const argdownParser = new ParserPlugin() +argdown.addPlugin(argdownParser, "parse-input") +const argdownModeler = new ModelPlugin() +argdown.addPlugin(argdownModeler, "model-input") +const argdownPainter = new ColorPlugin() +argdown.addPlugin(argdownPainter, "paint-input") +const argdownHTML = new HtmlExportPlugin() +argdown.addPlugin(argdownHTML, "html") + +// let argdownImport = undefined +document.addEventListener("nav", async () => { + const center = document.querySelector(".center") as HTMLElement + const nodes = center.querySelectorAll("code.argdown") as NodeListOf + if (nodes.length === 0) return + + // argdownImport ||= await import( + // // @ts-ignore + // "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/argdown.esm.min.mjs" + // ) + // const mermaid = argdownImport.default + + const textMapping: WeakMap = new WeakMap() + for (const node of nodes) { + textMapping.set(node, node.innerText) + } + + async function renderArgdown() { + // de-init any other diagrams + let requests: Array = [] + for (const node of nodes) { + node.removeAttribute("data-processed") + const oldText = textMapping.get(node) + if (oldText) { + node.innerHTML = oldText + requests.push( + { + node, + process: ["parse-input", "model-input", "paint-input", "html"] + } + ) + } + } + + // const computedStyleMap = cssVars.reduce( + // (acc, key) => { + // acc[key] = window.getComputedStyle(document.documentElement).getPropertyValue(key) + // return acc + // }, + // {} as Record<(typeof cssVars)[number], string>, + // ) + + // const darkMode = document.documentElement.getAttribute("saved-theme") === "dark" + // argdown.initialize({ + // startOnLoad: false, + // securityLevel: "loose", + // theme: darkMode ? "dark" : "base", + // themeVariables: { + // fontFamily: computedStyleMap["--codeFont"], + // primaryColor: computedStyleMap["--light"], + // primaryTextColor: computedStyleMap["--darkgray"], + // primaryBorderColor: computedStyleMap["--tertiary"], + // lineColor: computedStyleMap["--darkgray"], + // secondaryColor: computedStyleMap["--secondary"], + // tertiaryColor: computedStyleMap["--tertiary"], + // clusterBkg: computedStyleMap["--light"], + // edgeLabelBackground: computedStyleMap["--highlight"], + // }, + // }) + + await argdown.run({ requests }) + } + + await renderArgdown() + document.addEventListener("themechange", renderArgdown) + window.addCleanup(() => document.removeEventListener("themechange", renderArgdown)) + + for (let i = 0; i < nodes.length; i++) { + const codeBlock = nodes[i] as HTMLElement + const pre = codeBlock.parentElement as HTMLPreElement + const clipboardBtn = pre.querySelector(".clipboard-button") as HTMLButtonElement + const expandBtn = pre.querySelector(".expand-button") as HTMLButtonElement + + const clipboardStyle = window.getComputedStyle(clipboardBtn) + const clipboardWidth = + clipboardBtn.offsetWidth + + parseFloat(clipboardStyle.marginLeft || "0") + + parseFloat(clipboardStyle.marginRight || "0") + + // Set expand button position + expandBtn.style.right = `calc(${clipboardWidth}px + 0.3rem)` + pre.prepend(expandBtn) + + // query popup container + const popupContainer = pre.querySelector("#argdown-container") as HTMLElement + if (!popupContainer) return + + let panZoom: DiagramPanZoom | null = null + function showArgdown() { + const container = popupContainer.querySelector("#argdown-space") as HTMLElement + const content = popupContainer.querySelector(".argdown-content") as HTMLElement + if (!content) return + removeAllChildren(content) + + // Clone the mermaid content + const argdownContent = codeBlock.querySelector("svg")!.cloneNode(true) as SVGElement + content.appendChild(argdownContent) + + // Show container + popupContainer.classList.add("active") + container.style.cursor = "grab" + + // Initialize pan-zoom after showing the popup + panZoom = new DiagramPanZoom(container, content) + } + + function hideArgdown() { + popupContainer.classList.remove("active") + panZoom?.cleanup() + panZoom = null + } + + expandBtn.addEventListener("click", showArgdown) + registerEscapeHandler(popupContainer, hideArgdown) + + window.addCleanup(() => { + panZoom?.cleanup() + expandBtn.removeEventListener("click", showArgdown) + }) + } +}) diff --git a/quartz/components/styles/argdown.inline.scss b/quartz/components/styles/argdown.inline.scss new file mode 100644 index 0000000..9f171cb --- /dev/null +++ b/quartz/components/styles/argdown.inline.scss @@ -0,0 +1,133 @@ +.expand-button { + position: absolute; + display: flex; + float: right; + padding: 0.4rem; + margin: 0.3rem; + right: 0; // NOTE: right will be set in mermaid.inline.ts + color: var(--gray); + border-color: var(--dark); + background-color: var(--light); + border: 1px solid; + border-radius: 5px; + opacity: 0; + transition: 0.2s; + + & > svg { + fill: var(--light); + filter: contrast(0.3); + } + + &:hover { + cursor: pointer; + border-color: var(--secondary); + } + + &:focus { + outline: 0; + } +} + +pre { + &:hover > .expand-button { + opacity: 1; + transition: 0.2s; + } +} + +#argdown-container { + position: fixed; + contain: layout; + z-index: 999; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + overflow: hidden; + display: none; + backdrop-filter: blur(4px); + background: rgba(0, 0, 0, 0.5); + + &.active { + display: inline-block; + } + + & > #argdown-space { + border: 1px solid var(--lightgray); + background-color: var(--light); + border-radius: 5px; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + height: 80vh; + width: 80vw; + overflow: hidden; + + & > .argdown-content { + padding: 2rem; + position: relative; + transform-origin: 0 0; + transition: transform 0.1s ease; + overflow: visible; + min-height: 200px; + min-width: 200px; + + pre { + margin: 0; + border: none; + } + + svg { + max-width: none; + height: auto; + } + } + + & > .argdown-controls { + position: absolute; + bottom: 20px; + right: 20px; + display: flex; + gap: 8px; + padding: 8px; + background: var(--light); + border: 1px solid var(--lightgray); + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + z-index: 2; + + .argdown-control-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--lightgray); + background: var(--light); + color: var(--dark); + border-radius: 4px; + cursor: pointer; + font-size: 16px; + font-family: var(--bodyFont); + transition: all 0.2s ease; + + &:hover { + background: var(--lightgray); + } + + &:active { + transform: translateY(1px); + } + + // Style the reset button differently + &:nth-child(2) { + width: auto; + padding: 0 12px; + font-size: 14px; + } + } + } + } +}