1 files added
13 files modified
| | |
| | | |
| | | // dom custom event |
| | | interface CustomEventMap { |
| | | "spa_nav": CustomEvent<{ url: string }>; |
| | | "nav": CustomEvent<{ url: string }>; |
| | | } |
| | | |
| | | declare global { |
| | |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | |
| | | function Body({ children }: QuartzComponentProps) { |
| | | return <article> |
| | | return <div id="quartz-body"> |
| | | {children} |
| | | </article> |
| | | </div> |
| | | } |
| | | |
| | | Body.afterDOMLoaded = clipboardScript |
| | |
| | | function Content({ tree }: QuartzComponentProps) { |
| | | // @ts-ignore (preact makes it angry) |
| | | const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) |
| | | return content |
| | | return <article>{content}</article> |
| | | } |
| | | |
| | | export default (() => Content) satisfies QuartzComponentConstructor |
| | |
| | | import legacyStyle from "./styles/legacyToc.scss" |
| | | import modernStyle from "./styles/toc.scss" |
| | | |
| | | // @ts-ignore |
| | | import script from "./scripts/toc.inline" |
| | | |
| | | interface Options { |
| | | layout: 'modern' | 'legacy' |
| | | } |
| | |
| | | layout: 'modern' |
| | | } |
| | | |
| | | export default ((opts?: Partial<Options>) => { |
| | | const layout = opts?.layout ?? defaultOptions.layout |
| | | function TableOfContents({ fileData }: QuartzComponentProps) { |
| | | if (!fileData.toc) { |
| | | return null |
| | | } |
| | | function TableOfContents({ fileData }: QuartzComponentProps) { |
| | | if (!fileData.toc) { |
| | | return null |
| | | } |
| | | |
| | | return <details class="toc" open> |
| | | <summary><h3>Table of Contents</h3></summary> |
| | | return <> |
| | | <button type="button" id="toc"> |
| | | <h3>Table of Contents</h3> |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold"> |
| | | <polyline points="6 9 12 15 18 9"></polyline> |
| | | </svg> |
| | | </button> |
| | | <div id="toc-content"> |
| | | <ul> |
| | | {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> |
| | | <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a> |
| | | </li>)} |
| | | </ul> |
| | | </details> |
| | | } |
| | | |
| | | TableOfContents.css = layout === "modern" ? modernStyle : legacyStyle |
| | | |
| | | if (layout === "modern") { |
| | | TableOfContents.afterDOMLoaded = ` |
| | | const bufferPx = 150 |
| | | const observer = new IntersectionObserver(entries => { |
| | | for (const entry of entries) { |
| | | const slug = entry.target.id |
| | | const tocEntryElement = document.querySelector(\`a[data-for="$\{slug\}"]\`) |
| | | const windowHeight = entry.rootBounds?.height |
| | | if (windowHeight && tocEntryElement) { |
| | | if (entry.boundingClientRect.y < windowHeight) { |
| | | tocEntryElement.classList.add("in-view") |
| | | } else { |
| | | tocEntryElement.classList.remove("in-view") |
| | | } |
| | | } |
| | | } |
| | | }) |
| | | |
| | | function init() { |
| | | const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]") |
| | | headers.forEach(header => observer.observe(header)) |
| | | </div> |
| | | </> |
| | | } |
| | | TableOfContents.css = modernStyle |
| | | TableOfContents.afterDOMLoaded = script |
| | | |
| | | init() |
| | | |
| | | document.addEventListener("spa_nav", (e) => { |
| | | observer.disconnect() |
| | | init() |
| | | }) |
| | | ` |
| | | function LegacyTableOfContents({ fileData }: QuartzComponentProps) { |
| | | if (!fileData.toc) { |
| | | return null |
| | | } |
| | | |
| | | return TableOfContents |
| | | return <details id="toc" open> |
| | | <summary> |
| | | <h3>Table of Contents</h3> |
| | | </summary> |
| | | <ul> |
| | | {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> |
| | | <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a> |
| | | </li>)} |
| | | </ul> |
| | | </details> |
| | | } |
| | | LegacyTableOfContents.css = legacyStyle |
| | | |
| | | export default ((opts?: Partial<Options>) => { |
| | | const layout = opts?.layout ?? defaultOptions.layout |
| | | return layout === "modern" ? TableOfContents : LegacyTableOfContents |
| | | }) satisfies QuartzComponentConstructor |
| | |
| | | const svgCheck = |
| | | '<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" fill="rgb(63, 185, 80)" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>' |
| | | |
| | | const els = document.getElementsByTagName("pre") |
| | | for (let i = 0; i < els.length; i++) { |
| | | const codeBlock = els[i].getElementsByTagName("code")[0] |
| | | const source = codeBlock.innerText.replace(/\n\n/g, "\n") |
| | | const button = document.createElement("button") |
| | | button.className = "clipboard-button" |
| | | button.type = "button" |
| | | button.innerHTML = svgCopy |
| | | button.ariaLabel = "Copy source" |
| | | button.addEventListener("click", () => { |
| | | navigator.clipboard.writeText(source).then( |
| | | () => { |
| | | button.blur() |
| | | button.innerHTML = svgCheck |
| | | setTimeout(() => { |
| | | button.innerHTML = svgCopy |
| | | button.style.borderColor = "" |
| | | }, 2000) |
| | | }, |
| | | (error) => console.error(error), |
| | | ) |
| | | }) |
| | | els[i].prepend(button) |
| | | } |
| | | document.addEventListener("nav", () => { |
| | | const els = document.getElementsByTagName("pre") |
| | | for (let i = 0; i < els.length; i++) { |
| | | const codeBlock = els[i].getElementsByTagName("code")[0] |
| | | const source = codeBlock.innerText.replace(/\n\n/g, "\n") |
| | | const button = document.createElement("button") |
| | | button.className = "clipboard-button" |
| | | button.type = "button" |
| | | button.innerHTML = svgCopy |
| | | button.ariaLabel = "Copy source" |
| | | button.addEventListener("click", () => { |
| | | navigator.clipboard.writeText(source).then( |
| | | () => { |
| | | button.blur() |
| | | button.innerHTML = svgCheck |
| | | setTimeout(() => { |
| | | button.innerHTML = svgCopy |
| | | button.style.borderColor = "" |
| | | }, 2000) |
| | | }, |
| | | (error) => console.error(error), |
| | | ) |
| | | }) |
| | | els[i].prepend(button) |
| | | } |
| | | }) |
| | |
| | | } |
| | | |
| | | function notifyNav(slug: string) { |
| | | const event = new CustomEvent("spa_nav", { detail: { slug } }) |
| | | const event = new CustomEvent("nav", { detail: { slug } }) |
| | | document.dispatchEvent(event) |
| | | } |
| | | |
| | |
| | | return |
| | | }) |
| | | } |
| | | |
| | | return new class Router { |
| | | go(pathname: string) { |
| | | const url = new URL(pathname, window.location.toString()) |
| | |
| | | } |
| | | |
| | | createRouter() |
| | | notifyNav(document.body.dataset.slug!) |
| | | |
| | | if (!customElements.get('route-announcer')) { |
| | | const attrs = { |
| New file |
| | |
| | | const bufferPx = 150 |
| | | const observer = new IntersectionObserver(entries => { |
| | | for (const entry of entries) { |
| | | const slug = entry.target.id |
| | | const tocEntryElement = document.querySelector(`a[data-for="${slug}"]`) |
| | | const windowHeight = entry.rootBounds?.height |
| | | if (windowHeight && tocEntryElement) { |
| | | if (entry.boundingClientRect.y < windowHeight) { |
| | | tocEntryElement.classList.add("in-view") |
| | | } else { |
| | | tocEntryElement.classList.remove("in-view") |
| | | } |
| | | } |
| | | } |
| | | }) |
| | | |
| | | function toggleCollapsible(this: HTMLElement) { |
| | | this.classList.toggle("collapsed") |
| | | const content = this.nextElementSibling as HTMLElement |
| | | content.classList.toggle("collapsed") |
| | | content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px" |
| | | } |
| | | |
| | | document.addEventListener("nav", () => { |
| | | const toc = document.getElementById("toc")! |
| | | const content = toc.nextElementSibling as HTMLElement |
| | | content.style.maxHeight = content.scrollHeight + "px" |
| | | toc.removeEventListener("click", toggleCollapsible) |
| | | toc.addEventListener("click", toggleCollapsible) |
| | | |
| | | // update toc entry highlighting |
| | | observer.disconnect() |
| | | const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]") |
| | | headers.forEach(header => observer.observe(header)) |
| | | }) |
| | |
| | | details.toc { |
| | | details#toc { |
| | | & summary { |
| | | cursor: pointer; |
| | | |
| | |
| | | details.toc { |
| | | & summary { |
| | | cursor: pointer; |
| | | button#toc { |
| | | background-color: transparent; |
| | | border: none; |
| | | text-align: left; |
| | | cursor: pointer; |
| | | padding: 0; |
| | | color: var(--dark); |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | list-style: none; |
| | | &::marker, &::-webkit-details-marker { |
| | | display: none; |
| | | } |
| | | |
| | | & > * { |
| | | display: inline-block; |
| | | margin: 0; |
| | | } |
| | | |
| | | & > h3 { |
| | | font-size: 1rem; |
| | | } |
| | | & h3 { |
| | | font-size: 1rem; |
| | | display: inline-block; |
| | | margin: 0; |
| | | } |
| | | |
| | | |
| | | & .fold { |
| | | margin-left: 0.5rem; |
| | | transition: transform 0.3s ease; |
| | | opacity: 0.8; |
| | | } |
| | | |
| | | &.collapsed .fold { |
| | | transform: rotateZ(-90deg) |
| | | } |
| | | } |
| | | |
| | | #toc-content { |
| | | list-style: none; |
| | | overflow: hidden; |
| | | max-height: none; |
| | | transition: max-height 0.3s ease; |
| | | |
| | | & ul { |
| | | list-style: none; |
| | | margin: 0.5rem 0; |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | return { |
| | | name: "ContentPage", |
| | | getQuartzComponents() { |
| | | return [opts.head, Header, ...opts.header, ...opts.body] |
| | | return [opts.head, Header, Body, ...opts.header, ...opts.body, ...opts.left, ...opts.right, ...opts.footer] |
| | | }, |
| | | async emit(_contentDir, cfg, content, resources, emit): Promise<string[]> { |
| | | const fps: string[] = [] |
| | |
| | | afterDOMLoaded: [] |
| | | } |
| | | |
| | | if (cfg.enableSPA) { |
| | | componentResources.afterDOMLoaded.push(spaRouterScript) |
| | | } |
| | | |
| | | for (const component of allComponents) { |
| | | const { css, beforeDOMLoaded, afterDOMLoaded } = component |
| | | if (css) { |
| | |
| | | } |
| | | } |
| | | |
| | | if (cfg.enableSPA) { |
| | | componentResources.afterDOMLoaded.push(spaRouterScript) |
| | | } else { |
| | | componentResources.afterDOMLoaded.push(` |
| | | const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } }) |
| | | document.dispatchEvent(event)` |
| | | ) |
| | | } |
| | | |
| | | emit({ |
| | | slug: "index", |
| | | ext: ".css", |
| | |
| | | }] |
| | | ] |
| | | }, |
| | | externalResources: { |
| | | css: [ |
| | | // base css |
| | | "https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css", |
| | | ], |
| | | js: [ |
| | | { |
| | | // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md |
| | | src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js", |
| | | loadTime: "afterDOMReady", |
| | | contentType: 'external' |
| | | } |
| | | ] |
| | | externalResources() { |
| | | return { |
| | | css: [ |
| | | // base css |
| | | "https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css", |
| | | ], |
| | | js: [ |
| | | { |
| | | // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md |
| | | src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js", |
| | | loadTime: "afterDOMReady", |
| | | contentType: 'external' |
| | | } |
| | | ] |
| | | } |
| | | } |
| | | }) |
| | |
| | | import rehypeRaw from "rehype-raw" |
| | | import { visit } from "unist-util-visit" |
| | | import path from "path" |
| | | import { JSResource } from "../../resources" |
| | | |
| | | export interface Options { |
| | | highlight: boolean |
| | |
| | | node.children.splice(0, 1, ...blockquoteContent) |
| | | |
| | | // add properties to base blockquote |
| | | // TODO: add the js to actually support collapsing callout |
| | | node.data = { |
| | | hProperties: { |
| | | ...(node.data?.hProperties ?? {}), |
| | |
| | | htmlPlugins() { |
| | | return [rehypeRaw] |
| | | }, |
| | | externalResources: { |
| | | js: [{ |
| | | externalResources() { |
| | | const mermaidScript: JSResource = { |
| | | script: ` |
| | | import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs'; |
| | | mermaid.initialize({ startOnLoad: true }); |
| | | `, |
| | | import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs'; |
| | | mermaid.initialize({ startOnLoad: true }); |
| | | `, |
| | | loadTime: 'afterDOMReady', |
| | | moduleType: 'module', |
| | | contentType: 'inline' |
| | | }] |
| | | } |
| | | return { |
| | | js: opts.mermaid ? [mermaidScript] : [] |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | name: string |
| | | markdownPlugins(): PluggableList |
| | | htmlPlugins(): PluggableList |
| | | externalResources?: Partial<StaticResources> |
| | | externalResources?(): Partial<StaticResources> |
| | | } |
| | | |
| | | export type QuartzFilterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzFilterPluginInstance |