Added new Argdown components for use with Obsidian markdown transformer plugin
This commit is contained in:
282
quartz/components/scripts/argdown.inline.ts
Normal file
282
quartz/components/scripts/argdown.inline.ts
Normal file
@@ -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<HTMLElement>
|
||||||
|
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<HTMLElement, string> = new WeakMap()
|
||||||
|
for (const node of nodes) {
|
||||||
|
textMapping.set(node, node.innerText)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderArgdown() {
|
||||||
|
// de-init any other diagrams
|
||||||
|
let requests: Array<IArgdownRequest> = []
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
133
quartz/components/styles/argdown.inline.scss
Normal file
133
quartz/components/styles/argdown.inline.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user