1 files added
29 files modified
| | |
| | | |
| | | const sharedPageComponents = { |
| | | head: Component.Head(), |
| | | header: [ |
| | | Component.PageTitle(), |
| | | Component.Spacer(), |
| | | Component.Search(), |
| | | Component.Darkmode() |
| | | ], |
| | | header: [], |
| | | footer: Component.Footer({ |
| | | authorName: "Jacky", |
| | | links: { |
| | |
| | | Component.ReadingTime(), |
| | | Component.TagList(), |
| | | ], |
| | | left: [], |
| | | left: [ |
| | | Component.PageTitle(), |
| | | Component.Search(), |
| | | Component.TableOfContents(), |
| | | Component.Darkmode() |
| | | ], |
| | | right: [ |
| | | Component.Graph(), |
| | | Component.TableOfContents(), |
| | | Component.Backlinks() |
| | | Component.Backlinks(), |
| | | ], |
| | | } |
| | | |
| | |
| | | beforeBody: [ |
| | | Component.ArticleTitle() |
| | | ], |
| | | left: [], |
| | | left: [ |
| | | Component.PageTitle(), |
| | | Component.Search(), |
| | | Component.Darkmode() |
| | | ], |
| | | right: [], |
| | | } |
| | | |
| | |
| | | pageTitle: "🪴 Quartz 4.0", |
| | | enableSPA: true, |
| | | enablePopovers: true, |
| | | analytics: { |
| | | provider: 'plausible', |
| | | }, |
| | | canonicalUrl: "quartz.jzhao.xyz", |
| | | ignorePatterns: ["private", "templates"], |
| | | theme: { |
| | |
| | | ...contentPageLayout, |
| | | pageBody: Component.Content(), |
| | | }), |
| | | Plugin.TagPage({ |
| | | ...sharedPageComponents, |
| | | ...listPageLayout, |
| | | pageBody: Component.TagContent(), |
| | | }), |
| | | Plugin.FolderPage({ |
| | | ...sharedPageComponents, |
| | | ...listPageLayout, |
| | | pageBody: Component.FolderContent(), |
| | | }), |
| | | Plugin.TagPage({ |
| | | ...sharedPageComponents, |
| | | ...listPageLayout, |
| | | pageBody: Component.TagContent(), |
| | | }), |
| | | Plugin.ContentIndex({ |
| | | enableSiteMap: true, |
| | | enableRSS: true, |
| | |
| | | packages: "external", |
| | | plugins: [ |
| | | sassPlugin({ |
| | | type: 'css-text' |
| | | type: 'css-text', |
| | | }), |
| | | { |
| | | name: 'inline-script-loader', |
| | |
| | | import { PluginTypes } from "./plugins/types" |
| | | import { Theme } from "./theme" |
| | | |
| | | export type Analytics = null |
| | | | { |
| | | provider: 'plausible' |
| | | } |
| | | | { |
| | | provider: 'google', |
| | | tagId: string |
| | | } |
| | | |
| | | export interface GlobalConfiguration { |
| | | pageTitle: string, |
| | | /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ |
| | | enableSPA: boolean, |
| | | /** Whether to display Wikipedia-style popovers when hovering over links */ |
| | | enablePopovers: boolean, |
| | | /** Analytics mode */ |
| | | analytics: Analytics |
| | | /** Glob patterns to not search */ |
| | | ignorePatterns: string[], |
| | | /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL. |
| | |
| | | |
| | | function ArticleTitle({ fileData }: QuartzComponentProps) { |
| | | const title = fileData.frontmatter?.title |
| | | const displayTitle = fileData.slug === "index" ? undefined : title |
| | | if (displayTitle) { |
| | | return <h1 class="article-title">{displayTitle}</h1> |
| | | if (title) { |
| | | return <h1 class="article-title">{title}</h1> |
| | | } else { |
| | | return null |
| | | } |
| | |
| | | return <> |
| | | <hr /> |
| | | <footer> |
| | | <p>Made by {name} using <a>Quartz</a>, © {year}</p> |
| | | <p>Made by {name} using <a href="https://quartz.jzhao.xyz/">Quartz</a>, © {year}</p> |
| | | <ul>{Object.entries(links).map(([text, link]) => <li> |
| | | <a href={link}>{text}</a> |
| | | </li>)}</ul> |
| | |
| | | import { JSResourceToScriptElement } from "../resources" |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | |
| | | interface Options { |
| | | prefetchContentIndex: boolean |
| | | } |
| | | |
| | | const defaultOptions: Options = { |
| | | prefetchContentIndex: true |
| | | } |
| | | |
| | | export default ((opts?: Options) => { |
| | | export default (() => { |
| | | function Head({ fileData, externalResources }: QuartzComponentProps) { |
| | | const slug = fileData.slug! |
| | | const title = fileData.frontmatter?.title ?? "Untitled" |
| | |
| | | const iconPath = baseDir + "/static/icon.png" |
| | | const ogImagePath = baseDir + "/static/og-image.png" |
| | | |
| | | const prefetchContentIndex = opts?.prefetchContentIndex ?? defaultOptions.prefetchContentIndex |
| | | const contentIndexPath = baseDir + "/static/contentIndex.json" |
| | | const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())` |
| | | |
| | | return <head> |
| | | <title>{title}</title> |
| | | <meta charSet="utf-8" /> |
| | |
| | | <link rel="icon" href={iconPath} /> |
| | | <meta name="description" content={description} /> |
| | | <meta name="generator" content="Quartz" /> |
| | | <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| | | <link rel="preconnect" href="https://fonts.gstatic.com" /> |
| | | {prefetchContentIndex && <script spa-preserve>{contentIndexScript}</script>} |
| | | <link rel="preconnect" href="https://fonts.googleapis.com"/> |
| | | <link rel="preconnect" href="https://fonts.gstatic.com"/> |
| | | {css.map(href => <link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />)} |
| | | {js.filter(resource => resource.loadTime === "beforeDOMReady").map(res => JSResourceToScriptElement(res, true))} |
| | | </head> |
| | |
| | | flex-direction: row; |
| | | align-items: center; |
| | | margin: 2em 0; |
| | | gap: 1.5rem; |
| | | } |
| | | |
| | | header h1 { |
| | |
| | | |
| | | export function PageList({ fileData, allFiles }: QuartzComponentProps) { |
| | | const slug = fileData.slug! |
| | | return <ul class="section-ul"> |
| | | return <ul class="section-ul popover-hint"> |
| | | {allFiles.sort(byDateAndAlphabetical).map(page => { |
| | | const title = page.frontmatter?.title |
| | | const pageSlug = page.slug! |
| | |
| | | <div class="desc"> |
| | | <h3><a href={stripIndex(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3> |
| | | </div> |
| | | <div class="spacer"></div> |
| | | <ul class="tags"> |
| | | {tags.map(tag => <li><a href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)} |
| | | {tags.map(tag => <li><a class="internal" href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)} |
| | | </ul> |
| | | </div> |
| | | </li> |
| | |
| | | const display = `#${tag}` |
| | | const linkDest = baseDir + `/tags/${slugAnchor(tag)}` |
| | | return <li> |
| | | <a href={linkDest}>{display}</a> |
| | | <a href={linkDest} class="internal">{display}</a> |
| | | </li> |
| | | })}</ul> |
| | | } else { |
| | |
| | | display: flex; |
| | | padding-left: 0; |
| | | gap: 0.4rem; |
| | | } |
| | | |
| | | .tags > li { |
| | | display: inline-block; |
| | | margin: 0; |
| | | overflow-wrap: normal; |
| | | } |
| | | |
| | | & > li { |
| | | display: inline-block; |
| | | margin: 0; |
| | | |
| | | & > a { |
| | | border-radius: 8px; |
| | | border: var(--lightgray) 1px solid; |
| | | padding: 0.2rem 0.5rem; |
| | | } |
| | | } |
| | | .tags > li > a { |
| | | border-radius: 8px; |
| | | background-color: var(--highlight); |
| | | padding: 0.2rem 0.5rem; |
| | | } |
| | | ` |
| | | |
| | |
| | | function Content({ tree }: QuartzComponentProps) { |
| | | // @ts-ignore (preact makes it angry) |
| | | const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) |
| | | return <article>{content}</article> |
| | | return <article class="popover-hint">{content}</article> |
| | | } |
| | | |
| | | export default (() => Content) satisfies QuartzComponentConstructor |
| | | export default (() => Content) satisfies QuartzComponentConstructor |
| | |
| | | |
| | | export function pageResources(slug: string, staticResources: StaticResources): StaticResources { |
| | | const baseDir = resolveToRoot(slug) |
| | | |
| | | const contentIndexPath = baseDir + "/static/contentIndex.json" |
| | | const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())` |
| | | |
| | | return { |
| | | css: [baseDir + "/index.css", ...staticResources.css], |
| | | js: [ |
| | | { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" }, |
| | | { loadTime: "afterDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript }, |
| | | ...staticResources.js, |
| | | { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" } |
| | | ] |
| | |
| | | const Header = HeaderConstructor() |
| | | const Body = BodyConstructor() |
| | | |
| | | const LeftComponent = |
| | | <div class="left"> |
| | | <div class="left-inner"> |
| | | {left.map(BodyComponent => <BodyComponent {...componentData} />)} |
| | | </div> |
| | | </div> |
| | | |
| | | const RightComponent = |
| | | <div class="right"> |
| | | <div class="right-inner"> |
| | | {right.map(BodyComponent => <BodyComponent {...componentData} />)} |
| | | </div> |
| | | </div> |
| | | |
| | | const doc = <html> |
| | | <Head {...componentData} /> |
| | | <body data-slug={slug}> |
| | | <div id="quartz-root" class="page"> |
| | | <Header {...componentData} > |
| | | {header.map(HeaderComponent => <HeaderComponent {...componentData} />)} |
| | | </Header> |
| | | <div class="popover-hint"> |
| | | {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)} |
| | | <div class="page-header"> |
| | | <Header {...componentData} > |
| | | {header.map(HeaderComponent => <HeaderComponent {...componentData} />)} |
| | | </Header> |
| | | <div class="popover-hint"> |
| | | {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)} |
| | | </div> |
| | | </div> |
| | | <Body {...componentData}> |
| | | <div class="left"> |
| | | {left.map(BodyComponent => <BodyComponent {...componentData} />)} |
| | | </div> |
| | | <div class="center popover-hint"> |
| | | {LeftComponent} |
| | | <div class="center"> |
| | | <Content {...componentData} /> |
| | | <Footer {...componentData} /> |
| | | </div> |
| | | <div class="right"> |
| | | {right.map(BodyComponent => <BodyComponent {...componentData} />)} |
| | | </div> |
| | | {RightComponent} |
| | | </Body> |
| | | <Footer {...componentData} /> |
| | | </div> |
| | | </body> |
| | | {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))} |
| | |
| | | const currentTheme = localStorage.getItem('theme') ?? userPref |
| | | document.documentElement.setAttribute('saved-theme', currentTheme) |
| | | |
| | | window.addEventListener('DOMContentLoaded', () => { |
| | | document.addEventListener("nav", () => { |
| | | const switchTheme = (e: any) => { |
| | | if (e.target.checked) { |
| | | document.documentElement.setAttribute('saved-theme', 'dark') |
| | |
| | | |
| | | // Darkmode toggle |
| | | const toggleSwitch = document.querySelector('#darkmode-toggle') as HTMLInputElement |
| | | toggleSwitch.addEventListener('change', switchTheme, false) |
| | | toggleSwitch.removeEventListener('change', switchTheme) |
| | | toggleSwitch.addEventListener('change', switchTheme) |
| | | if (currentTheme === 'dark') { |
| | | toggleSwitch.checked = true |
| | | } |
| | |
| | | }) |
| | | } |
| | | |
| | | function renderGlobalGraph() { |
| | | async function renderGlobalGraph() { |
| | | const slug = document.body.dataset["slug"]! |
| | | renderGraph("global-graph-container", slug) |
| | | await renderGraph("global-graph-container", slug) |
| | | const container = document.getElementById("global-graph-outer") |
| | | container?.classList.add("active") |
| | | |
| | |
| | | containerIcon?.addEventListener("click", renderGlobalGraph) |
| | | }) |
| | | |
| | | window.addEventListener('resize', async () => { |
| | | const slug = document.body.dataset["slug"]! |
| | | await renderGraph("graph-container", slug) |
| | | let resizeEventDebounce: number | undefined = undefined |
| | | window.addEventListener('resize', () => { |
| | | if (resizeEventDebounce) { |
| | | clearTimeout(resizeEventDebounce) |
| | | } |
| | | |
| | | resizeEventDebounce = window.setTimeout(async () => { |
| | | const slug = document.body.dataset["slug"]! |
| | | await renderGraph("graph-container", slug) |
| | | }, 50) |
| | | }) |
| New file |
| | |
| | | import Plausible from 'plausible-tracker' |
| | | const { trackPageview } = Plausible() |
| | | document.addEventListener("nav", () => trackPageview()) |
| | |
| | | import { computePosition, flip, inline, shift } from "@floating-ui/dom" |
| | | |
| | | // from micromorph/src/utils.ts |
| | | // https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5 |
| | | export function normalizeRelativeURLs( |
| | | el: Element | Document, |
| | | base: string | URL |
| | | ) { |
| | | const update = (el: Element, attr: string, base: string | URL) => { |
| | | el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname) |
| | | } |
| | | |
| | | el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) => |
| | | update(item, 'href', base) |
| | | ) |
| | | |
| | | el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) => |
| | | update(item, 'src', base) |
| | | ) |
| | | } |
| | | |
| | | document.addEventListener("nav", () => { |
| | | const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] |
| | | const p = new DOMParser() |
| | |
| | | |
| | | if (!contents) return |
| | | const html = p.parseFromString(contents, "text/html") |
| | | normalizeRelativeURLs(html, targetUrl) |
| | | const elts = [...html.getElementsByClassName("popover-hint")] |
| | | if (elts.length === 0) return |
| | | |
| | |
| | | setPosition(popoverElement) |
| | | link.appendChild(popoverElement) |
| | | link.dataset.fetchedPopover = "true" |
| | | |
| | | const heading = popoverInner.querySelector(hash) as HTMLElement | null |
| | | if (heading) { |
| | | // leave ~12px of buffer when scrolling to a heading |
| | | popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' }) |
| | | |
| | | if (hash !== "") { |
| | | const heading = popoverInner.querySelector(hash) as HTMLElement | null |
| | | if (heading) { |
| | | // leave ~12px of buffer when scrolling to a heading |
| | | popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' }) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | |
| | | & > ul { |
| | | list-style: none; |
| | | padding: 0; |
| | | margin: 0; |
| | | margin: 0.5rem 0; |
| | | |
| | | & > li { |
| | | margin: 0.5rem 0; |
| | | padding: 0.25rem 1rem; |
| | | border: var(--lightgray) 1px solid; |
| | | border-radius: 5px; |
| | | & > a { |
| | | background-color: transparent; |
| | | } |
| | |
| | | footer { |
| | | text-align: left; |
| | | opacity: 0.8; |
| | | margin-bottom: 4rem; |
| | | |
| | | & ul { |
| | | list-style: none; |
| | | margin: 0; |
| | |
| | | height: 250px; |
| | | margin: 0.5em 0; |
| | | position: relative; |
| | | overflow: hidden; |
| | | |
| | | & > #global-graph-icon { |
| | | color: var(--dark); |
| | |
| | | background-color: var(--lightgray); |
| | | } |
| | | } |
| | | |
| | | & > #graph-container > svg { |
| | | margin-bottom: -5px; |
| | | } |
| | | } |
| | | |
| | | & > #global-graph-outer { |
| | |
| | | margin-bottom: 1em; |
| | | |
| | | & > .section { |
| | | display: flex; |
| | | align-items: center; |
| | | display: grid; |
| | | grid-template-columns: 6em 3fr 1fr; |
| | | |
| | | @media all and (max-width: 600px) { |
| | | & .tags { |
| | | & > .tags { |
| | | display: none; |
| | | } |
| | | } |
| | | |
| | | & h3 > a { |
| | | font-weight: 700; |
| | | margin: 0; |
| | | background-color: transparent; |
| | | & > .tags { |
| | | justify-self: end; |
| | | margin-left: 1rem; |
| | | } |
| | | |
| | | & p { |
| | | & > .desc a { |
| | | background-color: transparent; |
| | | } |
| | | |
| | | & > .meta { |
| | | margin: 0; |
| | | padding-right: 1em; |
| | | flex-basis: 6em; |
| | | opacity: 0.6; |
| | | } |
| | | } |
| | | } |
| | | |
| | | & .meta { |
| | | opacity: 0.6; |
| | | // modifications in popover context |
| | | .popover .section { |
| | | grid-template-columns: 6em 1fr !important; |
| | | & > .tags { |
| | | display: none; |
| | | } |
| | | } |
| | |
| | | height: 20rem; |
| | | padding: 0 1rem 1rem 1rem; |
| | | font-weight: initial; |
| | | line-height: initial; |
| | | line-height: normal; |
| | | font-size: initial; |
| | | font-family: var(--bodyFont); |
| | | border: 1px solid var(--gray); |
| | |
| | | .search { |
| | | min-width: 5rem; |
| | | max-width: 12rem; |
| | | max-width: 14rem; |
| | | flex-grow: 0.3; |
| | | margin: 0 1.5rem; |
| | | |
| | | & > #search-icon { |
| | | background-color: var(--lightgray); |
| | |
| | | externalResources: StaticResources |
| | | fileData: QuartzPluginData |
| | | cfg: GlobalConfiguration |
| | | children: QuartzComponent[] | JSX.Element[] |
| | | children: (QuartzComponent | JSX.Element)[] |
| | | tree: Node<QuartzPluginData> |
| | | allFiles: QuartzPluginData[] |
| | | } |
| | |
| | | return s.replace(/\s/g, '-') |
| | | } |
| | | |
| | | // on the client, 'index' isn't ever rendered so we should clean it up |
| | | export function clientSideSlug(fp: string): string { |
| | | if (fp.endsWith("index")) { |
| | | fp = fp.slice(0, -"index".length) |
| | | } |
| | | |
| | | return fp |
| | | } |
| | | |
| | | export function trimPathSuffix(fp: string): string { |
| | | fp = clientSideSlug(fp) |
| | | let [cleanPath, anchor] = fp.split("#", 2) |
| | | anchor = anchor === undefined ? "" : "#" + anchor |
| | | |
| | |
| | | // resolve /a/b/c to ../../ |
| | | export function resolveToRoot(slug: string): string { |
| | | let fp = trimPathSuffix(slug) |
| | | if (fp.endsWith("index")) { |
| | | fp = fp.slice(0, -"index".length) |
| | | } |
| | | |
| | | if (fp === "") { |
| | | return "." |
| | |
| | | const base = cfg.canonicalUrl ?? "" |
| | | const root = `https://${base}` |
| | | |
| | | // TODO: ogimage |
| | | const createURLEntry = (slug: string, content: ContentDetails): string => `<items> |
| | | <title>${content.title}</title> |
| | | <link>${root}/${slug}</link> |
| | |
| | | import { GlobalConfiguration } from '../cfg' |
| | | import { QuartzComponent } from '../components/types' |
| | | import { StaticResources } from '../resources' |
| | | import { googleFontHref, joinStyles } from '../theme' |
| | | import { joinStyles } from '../theme' |
| | | import { EmitCallback, PluginTypes } from './types' |
| | | import styles from '../styles/base.scss' |
| | | |
| | | // @ts-ignore |
| | | import spaRouterScript from '../components/scripts/spa.inline' |
| | | // @ts-ignore |
| | | import popoverScript from '../components/scripts/popover.inline' |
| | | import popoverStyle from '../components/styles/popover.scss' |
| | | |
| | | export type ComponentResources = { |
| | | css: string[], |
| | | beforeDOMLoaded: string[], |
| | | afterDOMLoaded: string[] |
| | | } |
| | | |
| | | function joinScripts(scripts: string[]): string { |
| | | // wrap with iife to prevent scope collision |
| | | return scripts.map(script => `(function () {${script}})();`).join("\n") |
| | | } |
| | | |
| | | export function emitComponentResources(cfg: GlobalConfiguration, resources: StaticResources, plugins: PluginTypes, emit: EmitCallback) { |
| | | const fps: string[] = [] |
| | | export function getComponentResources(plugins: PluginTypes): ComponentResources { |
| | | const allComponents: Set<QuartzComponent> = new Set() |
| | | for (const emitter of plugins.emitters) { |
| | | const components = emitter.getQuartzComponents() |
| | |
| | | componentResources.afterDOMLoaded.push(afterDOMLoaded) |
| | | } |
| | | } |
| | | |
| | | if (cfg.enablePopovers) { |
| | | componentResources.afterDOMLoaded.push(popoverScript) |
| | | componentResources.css.push(popoverStyle) |
| | | } |
| | | |
| | | if (cfg.enableSPA) { |
| | | componentResources.afterDOMLoaded.push(spaRouterScript) |
| | | } else { |
| | | componentResources.afterDOMLoaded.push(` |
| | | window.spaNavigate = (url, _) => window.location.assign(url) |
| | | const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } }) |
| | | document.dispatchEvent(event)` |
| | | ) |
| | | } |
| | | return componentResources |
| | | } |
| | | |
| | | emit({ |
| | | slug: "index", |
| | | ext: ".css", |
| | | content: joinStyles(cfg.theme, styles, ...componentResources.css) |
| | | }) |
| | | emit({ |
| | | slug: "prescript", |
| | | ext: ".js", |
| | | content: joinScripts(componentResources.beforeDOMLoaded) |
| | | }) |
| | | emit({ |
| | | slug: "postscript", |
| | | ext: ".js", |
| | | content: joinScripts(componentResources.afterDOMLoaded) |
| | | }) |
| | | function joinScripts(scripts: string[]): string { |
| | | // wrap with iife to prevent scope collision |
| | | return scripts.map(script => `(function () {${script}})();`).join("\n") |
| | | } |
| | | |
| | | fps.push("index.css", "prescript.js", "postscript.js") |
| | | resources.css.push(googleFontHref(cfg.theme)) |
| | | export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<string[]> { |
| | | const fps = await Promise.all([ |
| | | emit({ |
| | | slug: "index", |
| | | ext: ".css", |
| | | content: joinStyles(cfg.theme, styles, ...res.css) |
| | | }), |
| | | emit({ |
| | | slug: "prescript", |
| | | ext: ".js", |
| | | content: joinScripts(res.beforeDOMLoaded) |
| | | }), |
| | | emit({ |
| | | slug: "postscript", |
| | | ext: ".js", |
| | | content: joinScripts(res.afterDOMLoaded) |
| | | }) |
| | | ]) |
| | | return fps |
| | | |
| | | } |
| | | |
| | | export function getStaticResourcesFromPlugins(plugins: PluginTypes) { |
| | |
| | | import { PluggableList } from "unified" |
| | | import remarkGfm from "remark-gfm" |
| | | import smartypants from 'remark-smartypants' |
| | | import { QuartzTransformerPlugin } from "../types" |
| | |
| | | return { |
| | | name: "GitHubFlavoredMarkdown", |
| | | markdownPlugins() { |
| | | return opts.enableSmartyPants ? [remarkGfm] : [remarkGfm, smartypants] |
| | | return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm] |
| | | }, |
| | | htmlPlugins() { |
| | | if (opts.linkHeadings) { |
| | | return [rehypeSlug, [rehypeAutolinkHeadings, { |
| | | behavior: 'append', content: { |
| | | type: 'text', |
| | | value: ' §' |
| | | value: ' §', |
| | | } |
| | | }]] |
| | | } else { |
| | |
| | | import { QuartzTransformerPlugin } from "../types" |
| | | import { relativeToRoot, slugify, trimPathSuffix } from "../../path" |
| | | import { clientSideSlug, relativeToRoot, slugify, trimPathSuffix } from "../../path" |
| | | import path from "path" |
| | | import { visit } from 'unist-util-visit' |
| | | import isAbsoluteUrl from "is-absolute-url" |
| | |
| | | htmlPlugins() { |
| | | return [() => { |
| | | return (tree, file) => { |
| | | const curSlug = file.data.slug! |
| | | const curSlug = clientSideSlug(file.data.slug!) |
| | | const transformLink = (target: string) => { |
| | | const targetSlug = slugify(decodeURI(target).trim()) |
| | | if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) { |
| | |
| | | let dest = node.properties.href |
| | | node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal" |
| | | |
| | | |
| | | // don't process external links or intra-document anchors |
| | | if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) { |
| | | node.properties.href = transformLink(dest) |
| | |
| | | import path from "path" |
| | | import fs from "fs" |
| | | import { QuartzConfig } from "../cfg" |
| | | import { GlobalConfiguration, QuartzConfig } from "../cfg" |
| | | import { PerfTimer } from "../perf" |
| | | import { emitComponentResources, getStaticResourcesFromPlugins } from "../plugins" |
| | | import { ComponentResources, emitComponentResources, getComponentResources, getStaticResourcesFromPlugins } from "../plugins" |
| | | import { EmitCallback } from "../plugins/types" |
| | | import { ProcessedContent } from "../plugins/vfile" |
| | | import { QUARTZ, slugify } from "../path" |
| | | import { globbyStream } from "globby" |
| | | import chalk from "chalk" |
| | | import { googleFontHref } from '../theme' |
| | | |
| | | // @ts-ignore |
| | | import spaRouterScript from '../components/scripts/spa.inline' |
| | | // @ts-ignore |
| | | import plausibleScript from '../components/scripts/plausible.inline' |
| | | // @ts-ignore |
| | | import popoverScript from '../components/scripts/popover.inline' |
| | | import popoverStyle from '../components/styles/popover.scss' |
| | | import { StaticResources } from "../resources" |
| | | |
| | | function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: StaticResources, componentResources: ComponentResources) { |
| | | // font and other resources |
| | | staticResources.css.push(googleFontHref(cfg.theme)) |
| | | |
| | | // popovers |
| | | if (cfg.enablePopovers) { |
| | | componentResources.afterDOMLoaded.push(popoverScript) |
| | | componentResources.css.push(popoverStyle) |
| | | } |
| | | |
| | | if (cfg.analytics?.provider === "google") { |
| | | const tagId = cfg.analytics.tagId |
| | | staticResources.js.push({ |
| | | src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`, |
| | | contentType: 'external', |
| | | loadTime: 'afterDOMReady', |
| | | }) |
| | | componentResources.afterDOMLoaded.push(` |
| | | window.dataLayer = window.dataLayer || []; |
| | | function gtag() { dataLayer.push(arguments); } |
| | | gtag(\`js\`, new Date()); |
| | | gtag(\`config\`, \`${tagId}\`, { send_page_view: false }); |
| | | |
| | | document.addEventListener(\`nav\`, () => { |
| | | gtag(\`event\`, \`page_view\`, { |
| | | page_title: document.title, |
| | | page_location: location.href, |
| | | }); |
| | | });` |
| | | ) |
| | | } else if (cfg.analytics?.provider === "plausible") { |
| | | componentResources.afterDOMLoaded.push(plausibleScript) |
| | | } |
| | | |
| | | // spa |
| | | if (cfg.enableSPA) { |
| | | componentResources.afterDOMLoaded.push(spaRouterScript) |
| | | } else { |
| | | componentResources.afterDOMLoaded.push(` |
| | | window.spaNavigate = (url, _) => window.location.assign(url) |
| | | const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } }) |
| | | document.dispatchEvent(event)` |
| | | ) |
| | | } |
| | | } |
| | | |
| | | export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], verbose: boolean) { |
| | | const perf = new PerfTimer() |
| | |
| | | return pathToPage |
| | | } |
| | | |
| | | // initialize from plugins |
| | | const staticResources = getStaticResourcesFromPlugins(cfg.plugins) |
| | | emitComponentResources(cfg.configuration, staticResources, cfg.plugins, emit) |
| | | |
| | | // component specific scripts and styles |
| | | const componentResources = getComponentResources(cfg.plugins) |
| | | // important that this goes *after* component scripts |
| | | // as the "nav" event gets triggered here and we should make sure |
| | | // that everyone else had the chance to register a listener for it |
| | | addGlobalPageResources(cfg.configuration, staticResources, componentResources) |
| | | |
| | | // emit in one go |
| | | const emittedResources = await emitComponentResources(cfg.configuration, componentResources, emit) |
| | | if (verbose) { |
| | | for (const file of emittedResources) { |
| | | console.log(`[emit:Resources] ${file}`) |
| | | } |
| | | } |
| | | |
| | | // emitter plugins |
| | | let emittedFiles = 0 |
| | | for (const emitter of cfg.plugins.emitters) { |
| | | try { |
| | |
| | | |
| | | export type JSResource = { |
| | | loadTime: 'beforeDOMReady' | 'afterDOMReady' |
| | | moduleType?: 'module' |
| | | moduleType?: 'module', |
| | | spaPreserve?: boolean |
| | | } & ({ |
| | | src: string |
| | | contentType: 'external' |
| | |
| | | |
| | | export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element { |
| | | const scriptType = resource.moduleType ?? 'application/javascript' |
| | | const spaPreserve = preserve ?? resource.spaPreserve |
| | | if (resource.contentType === 'external') { |
| | | return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={preserve} /> |
| | | return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve} /> |
| | | } else { |
| | | const content = resource.script |
| | | return <script key={randomUUID()} type={scriptType} spa-preserve={preserve}>{content}</script> |
| | | return <script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>{content}</script> |
| | | } |
| | | } |
| | | |
| | |
| | | box-sizing: border-box; |
| | | background-color: var(--light); |
| | | font-family: var(--bodyFont); |
| | | --pageWidth: 800px; |
| | | --sidePanelWidth: 400px; |
| | | --topSpacing: 6rem; |
| | | } |
| | | |
| | | .text-highlight { |
| | |
| | | a { |
| | | font-weight: 600; |
| | | text-decoration: none; |
| | | transition: all 0.2s ease; |
| | | transition: color 0.2s ease; |
| | | color: var(--secondary); |
| | | |
| | | &:hover { |
| | |
| | | } |
| | | |
| | | .page { |
| | | margin: 6rem 35vw 6rem 20vw; |
| | | max-width: 1000px; |
| | | position: relative; |
| | | & > .page-header { |
| | | max-width: var(--pageWidth); |
| | | margin: var(--topSpacing) auto 0 auto; |
| | | } |
| | | |
| | | & .left, & .right { |
| | | position: fixed; |
| | | height: 100vh; |
| | | overflow-y: scroll; |
| | | box-sizing: border-box; |
| | | & > #quartz-body { |
| | | width: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | top: 0; |
| | | gap: 2rem; |
| | | padding: 6rem; |
| | | } |
| | | |
| | | & .left { |
| | | left: 0; |
| | | padding-left: 10vw; |
| | | width: 20vw; |
| | | } |
| | | |
| | | & .right { |
| | | right: 0; |
| | | padding-right: 10vw; |
| | | width: 35vw; |
| | | } |
| | | & .left, & .right { |
| | | flex: 1; |
| | | width: calc(calc(100vw - var(--pageWidth)) / 2); |
| | | } |
| | | |
| | | & .left-inner, & .right-inner { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2rem; |
| | | top: 0; |
| | | width: var(--sidePanelWidth); |
| | | margin-top: calc(var(--topSpacing)); |
| | | box-sizing: border-box; |
| | | padding: 0 4rem; |
| | | position: fixed; |
| | | } |
| | | |
| | | & .left-inner { |
| | | left: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth)); |
| | | } |
| | | |
| | | & .right-inner { |
| | | right: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth)); |
| | | } |
| | | |
| | | & .center { |
| | | width: var(--pageWidth); |
| | | margin: 0 auto; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .page { |
| | | @media all and (max-width: 1200px) { |
| | | margin: 25px 5vw; |
| | | & .left, & .right { |
| | |
| | | & > h1 { |
| | | font-size: 2rem; |
| | | } |
| | | |
| | | // darkmode diagrams |
| | | & svg { |
| | | stroke: var(--dark); |
| | | } |
| | | |
| | | & ul:has(input[type='checkbox']) { |
| | | list-style-type: none; |
| | | padding-left: 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | input[type="checkbox"] { |
| | | transform: translateY(2px); |
| | | color: var(--secondary); |
| | | border-color: var(--lightgray); |
| | | background-color: var(--light); |
| | | } |
| | | |
| | | blockquote { |
| | | margin: 1rem 0; |
| | | border-left: 3px solid var(--secondary); |
| | |
| | | } |
| | | |
| | | h1, h2, h3, h4, h5, h6 { |
| | | &[id] > a { |
| | | &[id] > a[href^="#"] { |
| | | margin: 0 0.5rem; |
| | | opacity: 0; |
| | | transition: opacity 0.2s ease; |