various polish

This commit is contained in:
Jacky Zhao 2023-07-02 13:08:29 -07:00
parent ba6df2d95f
commit 83c5fa64ea
30 changed files with 339 additions and 190 deletions

View File

@ -4,12 +4,7 @@ import * as Plugin from "./quartz/plugins"
const sharedPageComponents = { const sharedPageComponents = {
head: Component.Head(), head: Component.Head(),
header: [ header: [],
Component.PageTitle(),
Component.Spacer(),
Component.Search(),
Component.Darkmode()
],
footer: Component.Footer({ footer: Component.Footer({
authorName: "Jacky", authorName: "Jacky",
links: { links: {
@ -25,11 +20,15 @@ const contentPageLayout: PageLayout = {
Component.ReadingTime(), Component.ReadingTime(),
Component.TagList(), Component.TagList(),
], ],
left: [], left: [
Component.PageTitle(),
Component.Search(),
Component.TableOfContents(),
Component.Darkmode()
],
right: [ right: [
Component.Graph(), Component.Graph(),
Component.TableOfContents(), Component.Backlinks(),
Component.Backlinks()
], ],
} }
@ -37,7 +36,11 @@ const listPageLayout: PageLayout = {
beforeBody: [ beforeBody: [
Component.ArticleTitle() Component.ArticleTitle()
], ],
left: [], left: [
Component.PageTitle(),
Component.Search(),
Component.Darkmode()
],
right: [], right: [],
} }
@ -46,6 +49,9 @@ const config: QuartzConfig = {
pageTitle: "🪴 Quartz 4.0", pageTitle: "🪴 Quartz 4.0",
enableSPA: true, enableSPA: true,
enablePopovers: true, enablePopovers: true,
analytics: {
provider: 'plausible',
},
canonicalUrl: "quartz.jzhao.xyz", canonicalUrl: "quartz.jzhao.xyz",
ignorePatterns: ["private", "templates"], ignorePatterns: ["private", "templates"],
theme: { theme: {
@ -102,16 +108,16 @@ const config: QuartzConfig = {
...contentPageLayout, ...contentPageLayout,
pageBody: Component.Content(), pageBody: Component.Content(),
}), }),
Plugin.TagPage({
...sharedPageComponents,
...listPageLayout,
pageBody: Component.TagContent(),
}),
Plugin.FolderPage({ Plugin.FolderPage({
...sharedPageComponents, ...sharedPageComponents,
...listPageLayout, ...listPageLayout,
pageBody: Component.FolderContent(), pageBody: Component.FolderContent(),
}), }),
Plugin.TagPage({
...sharedPageComponents,
...listPageLayout,
pageBody: Component.TagContent(),
}),
Plugin.ContentIndex({ Plugin.ContentIndex({
enableSiteMap: true, enableSiteMap: true,
enableRSS: true, enableRSS: true,

View File

@ -64,7 +64,7 @@ yargs(hideBin(process.argv))
packages: "external", packages: "external",
plugins: [ plugins: [
sassPlugin({ sassPlugin({
type: 'css-text' type: 'css-text',
}), }),
{ {
name: 'inline-script-loader', name: 'inline-script-loader',

View File

@ -2,12 +2,23 @@ import { QuartzComponent } from "./components/types"
import { PluginTypes } from "./plugins/types" import { PluginTypes } from "./plugins/types"
import { Theme } from "./theme" import { Theme } from "./theme"
export type Analytics = null
| {
provider: 'plausible'
}
| {
provider: 'google',
tagId: string
}
export interface GlobalConfiguration { export interface GlobalConfiguration {
pageTitle: string, pageTitle: string,
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
enableSPA: boolean, enableSPA: boolean,
/** Whether to display Wikipedia-style popovers when hovering over links */ /** Whether to display Wikipedia-style popovers when hovering over links */
enablePopovers: boolean, enablePopovers: boolean,
/** Analytics mode */
analytics: Analytics
/** Glob patterns to not search */ /** Glob patterns to not search */
ignorePatterns: string[], ignorePatterns: string[],
/** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL. /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.

View File

@ -2,9 +2,8 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function ArticleTitle({ fileData }: QuartzComponentProps) { function ArticleTitle({ fileData }: QuartzComponentProps) {
const title = fileData.frontmatter?.title const title = fileData.frontmatter?.title
const displayTitle = fileData.slug === "index" ? undefined : title if (title) {
if (displayTitle) { return <h1 class="article-title">{title}</h1>
return <h1 class="article-title">{displayTitle}</h1>
} else { } else {
return null return null
} }

View File

@ -14,7 +14,7 @@ export default ((opts?: Options) => {
return <> return <>
<hr /> <hr />
<footer> <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> <ul>{Object.entries(links).map(([text, link]) => <li>
<a href={link}>{text}</a> <a href={link}>{text}</a>
</li>)}</ul> </li>)}</ul>

View File

@ -2,15 +2,7 @@ import { resolveToRoot } from "../path"
import { JSResourceToScriptElement } from "../resources" import { JSResourceToScriptElement } from "../resources"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
interface Options { export default (() => {
prefetchContentIndex: boolean
}
const defaultOptions: Options = {
prefetchContentIndex: true
}
export default ((opts?: Options) => {
function Head({ fileData, externalResources }: QuartzComponentProps) { function Head({ fileData, externalResources }: QuartzComponentProps) {
const slug = fileData.slug! const slug = fileData.slug!
const title = fileData.frontmatter?.title ?? "Untitled" const title = fileData.frontmatter?.title ?? "Untitled"
@ -20,10 +12,6 @@ export default ((opts?: Options) => {
const iconPath = baseDir + "/static/icon.png" const iconPath = baseDir + "/static/icon.png"
const ogImagePath = baseDir + "/static/og-image.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> return <head>
<title>{title}</title> <title>{title}</title>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
@ -36,9 +24,8 @@ export default ((opts?: Options) => {
<link rel="icon" href={iconPath} /> <link rel="icon" href={iconPath} />
<meta name="description" content={description} /> <meta name="description" content={description} />
<meta name="generator" content="Quartz" /> <meta name="generator" content="Quartz" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link rel="preconnect" href="https://fonts.gstatic.com"/>
{prefetchContentIndex && <script spa-preserve>{contentIndexScript}</script>}
{css.map(href => <link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />)} {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))} {js.filter(resource => resource.loadTime === "beforeDOMReady").map(res => JSResourceToScriptElement(res, true))}
</head> </head>

View File

@ -12,6 +12,7 @@ header {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin: 2em 0; margin: 2em 0;
gap: 1.5rem;
} }
header h1 { header h1 {

View File

@ -23,7 +23,7 @@ function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): numb
export function PageList({ fileData, allFiles }: QuartzComponentProps) { export function PageList({ fileData, allFiles }: QuartzComponentProps) {
const slug = fileData.slug! const slug = fileData.slug!
return <ul class="section-ul"> return <ul class="section-ul popover-hint">
{allFiles.sort(byDateAndAlphabetical).map(page => { {allFiles.sort(byDateAndAlphabetical).map(page => {
const title = page.frontmatter?.title const title = page.frontmatter?.title
const pageSlug = page.slug! const pageSlug = page.slug!
@ -36,9 +36,8 @@ export function PageList({ fileData, allFiles }: QuartzComponentProps) {
<div class="desc"> <div class="desc">
<h3><a href={stripIndex(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3> <h3><a href={stripIndex(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3>
</div> </div>
<div class="spacer"></div>
<ul class="tags"> <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> </ul>
</div> </div>
</li> </li>

View File

@ -11,7 +11,7 @@ function TagList({ fileData }: QuartzComponentProps) {
const display = `#${tag}` const display = `#${tag}`
const linkDest = baseDir + `/tags/${slugAnchor(tag)}` const linkDest = baseDir + `/tags/${slugAnchor(tag)}`
return <li> return <li>
<a href={linkDest}>{display}</a> <a href={linkDest} class="internal">{display}</a>
</li> </li>
})}</ul> })}</ul>
} else { } else {
@ -25,17 +25,18 @@ TagList.css = `
display: flex; display: flex;
padding-left: 0; padding-left: 0;
gap: 0.4rem; gap: 0.4rem;
}
& > li { .tags > li {
display: inline-block; display: inline-block;
margin: 0; margin: 0;
overflow-wrap: normal;
}
& > a { .tags > li > a {
border-radius: 8px; border-radius: 8px;
border: var(--lightgray) 1px solid; background-color: var(--highlight);
padding: 0.2rem 0.5rem; padding: 0.2rem 0.5rem;
}
}
} }
` `

View File

@ -5,7 +5,7 @@ import { toJsxRuntime } from "hast-util-to-jsx-runtime"
function Content({ tree }: QuartzComponentProps) { function Content({ tree }: QuartzComponentProps) {
// @ts-ignore (preact makes it angry) // @ts-ignore (preact makes it angry)
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) 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

View File

@ -17,10 +17,15 @@ interface RenderComponents {
export function pageResources(slug: string, staticResources: StaticResources): StaticResources { export function pageResources(slug: string, staticResources: StaticResources): StaticResources {
const baseDir = resolveToRoot(slug) const baseDir = resolveToRoot(slug)
const contentIndexPath = baseDir + "/static/contentIndex.json"
const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
return { return {
css: [baseDir + "/index.css", ...staticResources.css], css: [baseDir + "/index.css", ...staticResources.css],
js: [ js: [
{ src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" }, { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
{ loadTime: "afterDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript },
...staticResources.js, ...staticResources.js,
{ src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" } { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" }
] ]
@ -32,28 +37,40 @@ export function renderPage(slug: string, componentData: QuartzComponentProps, co
const Header = HeaderConstructor() const Header = HeaderConstructor()
const Body = BodyConstructor() 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> const doc = <html>
<Head {...componentData} /> <Head {...componentData} />
<body data-slug={slug}> <body data-slug={slug}>
<div id="quartz-root" class="page"> <div id="quartz-root" class="page">
<Header {...componentData} > <div class="page-header">
{header.map(HeaderComponent => <HeaderComponent {...componentData} />)} <Header {...componentData} >
</Header> {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
<div class="popover-hint"> </Header>
{beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)} <div class="popover-hint">
{beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
</div>
</div> </div>
<Body {...componentData}> <Body {...componentData}>
<div class="left"> {LeftComponent}
{left.map(BodyComponent => <BodyComponent {...componentData} />)} <div class="center">
</div>
<div class="center popover-hint">
<Content {...componentData} /> <Content {...componentData} />
<Footer {...componentData} />
</div> </div>
<div class="right"> {RightComponent}
{right.map(BodyComponent => <BodyComponent {...componentData} />)}
</div>
</Body> </Body>
<Footer {...componentData} />
</div> </div>
</body> </body>
{pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))} {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}

View File

@ -2,7 +2,7 @@ const userPref = window.matchMedia('(prefers-color-scheme: light)').matches ? 'l
const currentTheme = localStorage.getItem('theme') ?? userPref const currentTheme = localStorage.getItem('theme') ?? userPref
document.documentElement.setAttribute('saved-theme', currentTheme) document.documentElement.setAttribute('saved-theme', currentTheme)
window.addEventListener('DOMContentLoaded', () => { document.addEventListener("nav", () => {
const switchTheme = (e: any) => { const switchTheme = (e: any) => {
if (e.target.checked) { if (e.target.checked) {
document.documentElement.setAttribute('saved-theme', 'dark') document.documentElement.setAttribute('saved-theme', 'dark')
@ -16,7 +16,8 @@ window.addEventListener('DOMContentLoaded', () => {
// Darkmode toggle // Darkmode toggle
const toggleSwitch = document.querySelector('#darkmode-toggle') as HTMLInputElement const toggleSwitch = document.querySelector('#darkmode-toggle') as HTMLInputElement
toggleSwitch.addEventListener('change', switchTheme, false) toggleSwitch.removeEventListener('change', switchTheme)
toggleSwitch.addEventListener('change', switchTheme)
if (currentTheme === 'dark') { if (currentTheme === 'dark') {
toggleSwitch.checked = true toggleSwitch.checked = true
} }

View File

@ -266,9 +266,9 @@ async function renderGraph(container: string, slug: string) {
}) })
} }
function renderGlobalGraph() { async function renderGlobalGraph() {
const slug = document.body.dataset["slug"]! const slug = document.body.dataset["slug"]!
renderGraph("global-graph-container", slug) await renderGraph("global-graph-container", slug)
const container = document.getElementById("global-graph-outer") const container = document.getElementById("global-graph-outer")
container?.classList.add("active") container?.classList.add("active")
@ -293,7 +293,14 @@ document.addEventListener("nav", async (e: unknown) => {
containerIcon?.addEventListener("click", renderGlobalGraph) containerIcon?.addEventListener("click", renderGlobalGraph)
}) })
window.addEventListener('resize', async () => { let resizeEventDebounce: number | undefined = undefined
const slug = document.body.dataset["slug"]! window.addEventListener('resize', () => {
await renderGraph("graph-container", slug) if (resizeEventDebounce) {
clearTimeout(resizeEventDebounce)
}
resizeEventDebounce = window.setTimeout(async () => {
const slug = document.body.dataset["slug"]!
await renderGraph("graph-container", slug)
}, 50)
}) })

View File

@ -0,0 +1,3 @@
import Plausible from 'plausible-tracker'
const { trackPageview } = Plausible()
document.addEventListener("nav", () => trackPageview())

View File

@ -1,5 +1,24 @@
import { computePosition, flip, inline, shift } from "@floating-ui/dom" 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", () => { document.addEventListener("nav", () => {
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
const p = new DOMParser() const p = new DOMParser()
@ -41,6 +60,7 @@ document.addEventListener("nav", () => {
if (!contents) return if (!contents) return
const html = p.parseFromString(contents, "text/html") const html = p.parseFromString(contents, "text/html")
normalizeRelativeURLs(html, targetUrl)
const elts = [...html.getElementsByClassName("popover-hint")] const elts = [...html.getElementsByClassName("popover-hint")]
if (elts.length === 0) return if (elts.length === 0) return
@ -55,10 +75,12 @@ document.addEventListener("nav", () => {
link.appendChild(popoverElement) link.appendChild(popoverElement)
link.dataset.fetchedPopover = "true" link.dataset.fetchedPopover = "true"
const heading = popoverInner.querySelector(hash) as HTMLElement | null if (hash !== "") {
if (heading) { const heading = popoverInner.querySelector(hash) as HTMLElement | null
// leave ~12px of buffer when scrolling to a heading if (heading) {
popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' }) // leave ~12px of buffer when scrolling to a heading
popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' })
}
} }
}) })
} }

View File

@ -7,13 +7,9 @@
& > ul { & > ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0.5rem 0;
& > li { & > li {
margin: 0.5rem 0;
padding: 0.25rem 1rem;
border: var(--lightgray) 1px solid;
border-radius: 5px;
& > a { & > a {
background-color: transparent; background-color: transparent;
} }

View File

@ -1,6 +1,8 @@
footer { footer {
text-align: left; text-align: left;
opacity: 0.8; opacity: 0.8;
margin-bottom: 4rem;
& ul { & ul {
list-style: none; list-style: none;
margin: 0; margin: 0;

View File

@ -11,6 +11,7 @@
height: 250px; height: 250px;
margin: 0.5em 0; margin: 0.5em 0;
position: relative; position: relative;
overflow: hidden;
& > #global-graph-icon { & > #global-graph-icon {
color: var(--dark); color: var(--dark);
@ -30,10 +31,6 @@
background-color: var(--lightgray); background-color: var(--lightgray);
} }
} }
& > #graph-container > svg {
margin-bottom: -5px;
}
} }
& > #global-graph-outer { & > #global-graph-outer {

View File

@ -8,29 +8,36 @@ li.section-li {
margin-bottom: 1em; margin-bottom: 1em;
& > .section { & > .section {
display: flex; display: grid;
align-items: center; grid-template-columns: 6em 3fr 1fr;
@media all and (max-width: 600px) { @media all and (max-width: 600px) {
& .tags { & > .tags {
display: none; display: none;
} }
} }
& h3 > a { & > .tags {
font-weight: 700; justify-self: end;
margin: 0; margin-left: 1rem;
}
& > .desc a {
background-color: transparent; background-color: transparent;
} }
& p { & > .meta {
margin: 0; margin: 0;
padding-right: 1em;
flex-basis: 6em; flex-basis: 6em;
opacity: 0.6;
} }
} }
}
& .meta { // modifications in popover context
opacity: 0.6; .popover .section {
grid-template-columns: 6em 1fr !important;
& > .tags {
display: none;
} }
} }

View File

@ -24,7 +24,7 @@
height: 20rem; height: 20rem;
padding: 0 1rem 1rem 1rem; padding: 0 1rem 1rem 1rem;
font-weight: initial; font-weight: initial;
line-height: initial; line-height: normal;
font-size: initial; font-size: initial;
font-family: var(--bodyFont); font-family: var(--bodyFont);
border: 1px solid var(--gray); border: 1px solid var(--gray);

View File

@ -1,8 +1,7 @@
.search { .search {
min-width: 5rem; min-width: 5rem;
max-width: 12rem; max-width: 14rem;
flex-grow: 0.3; flex-grow: 0.3;
margin: 0 1.5rem;
& > #search-icon { & > #search-icon {
background-color: var(--lightgray); background-color: var(--lightgray);

View File

@ -8,7 +8,7 @@ export type QuartzComponentProps = {
externalResources: StaticResources externalResources: StaticResources
fileData: QuartzPluginData fileData: QuartzPluginData
cfg: GlobalConfiguration cfg: GlobalConfiguration
children: QuartzComponent[] | JSX.Element[] children: (QuartzComponent | JSX.Element)[]
tree: Node<QuartzPluginData> tree: Node<QuartzPluginData>
allFiles: QuartzPluginData[] allFiles: QuartzPluginData[]
} }

View File

@ -5,7 +5,17 @@ function slugSegment(s: string): string {
return s.replace(/\s/g, '-') 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 { export function trimPathSuffix(fp: string): string {
fp = clientSideSlug(fp)
let [cleanPath, anchor] = fp.split("#", 2) let [cleanPath, anchor] = fp.split("#", 2)
anchor = anchor === undefined ? "" : "#" + anchor anchor = anchor === undefined ? "" : "#" + anchor
@ -27,9 +37,6 @@ export function slugify(s: string): string {
// resolve /a/b/c to ../../ // resolve /a/b/c to ../../
export function resolveToRoot(slug: string): string { export function resolveToRoot(slug: string): string {
let fp = trimPathSuffix(slug) let fp = trimPathSuffix(slug)
if (fp.endsWith("index")) {
fp = fp.slice(0, -"index".length)
}
if (fp === "") { if (fp === "") {
return "." return "."

View File

@ -36,7 +36,6 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
const base = cfg.canonicalUrl ?? "" const base = cfg.canonicalUrl ?? ""
const root = `https://${base}` const root = `https://${base}`
// TODO: ogimage
const createURLEntry = (slug: string, content: ContentDetails): string => `<items> const createURLEntry = (slug: string, content: ContentDetails): string => `<items>
<title>${content.title}</title> <title>${content.title}</title>
<link>${root}/${slug}</link> <link>${root}/${slug}</link>

View File

@ -1,29 +1,17 @@
import { GlobalConfiguration } from '../cfg' import { GlobalConfiguration } from '../cfg'
import { QuartzComponent } from '../components/types' import { QuartzComponent } from '../components/types'
import { StaticResources } from '../resources' import { StaticResources } from '../resources'
import { googleFontHref, joinStyles } from '../theme' import { joinStyles } from '../theme'
import { EmitCallback, PluginTypes } from './types' import { EmitCallback, PluginTypes } from './types'
import styles from '../styles/base.scss' 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 = { export type ComponentResources = {
css: string[], css: string[],
beforeDOMLoaded: string[], beforeDOMLoaded: string[],
afterDOMLoaded: string[] afterDOMLoaded: string[]
} }
function joinScripts(scripts: string[]): string { export function getComponentResources(plugins: PluginTypes): ComponentResources {
// 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[] = []
const allComponents: Set<QuartzComponent> = new Set() const allComponents: Set<QuartzComponent> = new Set()
for (const emitter of plugins.emitters) { for (const emitter of plugins.emitters) {
const components = emitter.getQuartzComponents() const components = emitter.getQuartzComponents()
@ -51,40 +39,34 @@ export function emitComponentResources(cfg: GlobalConfiguration, resources: Stat
} }
} }
if (cfg.enablePopovers) { return componentResources
componentResources.afterDOMLoaded.push(popoverScript) }
componentResources.css.push(popoverStyle)
}
if (cfg.enableSPA) { function joinScripts(scripts: string[]): string {
componentResources.afterDOMLoaded.push(spaRouterScript) // wrap with iife to prevent scope collision
} else { return scripts.map(script => `(function () {${script}})();`).join("\n")
componentResources.afterDOMLoaded.push(` }
window.spaNavigate = (url, _) => window.location.assign(url)
const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
document.dispatchEvent(event)`
)
}
emit({ export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<string[]> {
slug: "index", const fps = await Promise.all([
ext: ".css", emit({
content: joinStyles(cfg.theme, styles, ...componentResources.css) slug: "index",
}) ext: ".css",
emit({ content: joinStyles(cfg.theme, styles, ...res.css)
slug: "prescript", }),
ext: ".js", emit({
content: joinScripts(componentResources.beforeDOMLoaded) slug: "prescript",
}) ext: ".js",
emit({ content: joinScripts(res.beforeDOMLoaded)
slug: "postscript", }),
ext: ".js", emit({
content: joinScripts(componentResources.afterDOMLoaded) slug: "postscript",
}) ext: ".js",
content: joinScripts(res.afterDOMLoaded)
fps.push("index.css", "prescript.js", "postscript.js") })
resources.css.push(googleFontHref(cfg.theme)) ])
return fps return fps
} }
export function getStaticResourcesFromPlugins(plugins: PluginTypes) { export function getStaticResourcesFromPlugins(plugins: PluginTypes) {

View File

@ -1,4 +1,3 @@
import { PluggableList } from "unified"
import remarkGfm from "remark-gfm" import remarkGfm from "remark-gfm"
import smartypants from 'remark-smartypants' import smartypants from 'remark-smartypants'
import { QuartzTransformerPlugin } from "../types" import { QuartzTransformerPlugin } from "../types"
@ -20,14 +19,14 @@ export const GitHubFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> |
return { return {
name: "GitHubFlavoredMarkdown", name: "GitHubFlavoredMarkdown",
markdownPlugins() { markdownPlugins() {
return opts.enableSmartyPants ? [remarkGfm] : [remarkGfm, smartypants] return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm]
}, },
htmlPlugins() { htmlPlugins() {
if (opts.linkHeadings) { if (opts.linkHeadings) {
return [rehypeSlug, [rehypeAutolinkHeadings, { return [rehypeSlug, [rehypeAutolinkHeadings, {
behavior: 'append', content: { behavior: 'append', content: {
type: 'text', type: 'text',
value: ' §' value: ' §',
} }
}]] }]]
} else { } else {

View File

@ -1,5 +1,5 @@
import { QuartzTransformerPlugin } from "../types" import { QuartzTransformerPlugin } from "../types"
import { relativeToRoot, slugify, trimPathSuffix } from "../../path" import { clientSideSlug, relativeToRoot, slugify, trimPathSuffix } from "../../path"
import path from "path" import path from "path"
import { visit } from 'unist-util-visit' import { visit } from 'unist-util-visit'
import isAbsoluteUrl from "is-absolute-url" import isAbsoluteUrl from "is-absolute-url"
@ -27,7 +27,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
htmlPlugins() { htmlPlugins() {
return [() => { return [() => {
return (tree, file) => { return (tree, file) => {
const curSlug = file.data.slug! const curSlug = clientSideSlug(file.data.slug!)
const transformLink = (target: string) => { const transformLink = (target: string) => {
const targetSlug = slugify(decodeURI(target).trim()) const targetSlug = slugify(decodeURI(target).trim())
if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) { if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) {
@ -49,7 +49,6 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
let dest = node.properties.href let dest = node.properties.href
node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal" node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal"
// don't process external links or intra-document anchors // don't process external links or intra-document anchors
if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) { if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
node.properties.href = transformLink(dest) node.properties.href = transformLink(dest)

View File

@ -1,13 +1,69 @@
import path from "path" import path from "path"
import fs from "fs" import fs from "fs"
import { QuartzConfig } from "../cfg" import { GlobalConfiguration, QuartzConfig } from "../cfg"
import { PerfTimer } from "../perf" import { PerfTimer } from "../perf"
import { emitComponentResources, getStaticResourcesFromPlugins } from "../plugins" import { ComponentResources, emitComponentResources, getComponentResources, getStaticResourcesFromPlugins } from "../plugins"
import { EmitCallback } from "../plugins/types" import { EmitCallback } from "../plugins/types"
import { ProcessedContent } from "../plugins/vfile" import { ProcessedContent } from "../plugins/vfile"
import { QUARTZ, slugify } from "../path" import { QUARTZ, slugify } from "../path"
import { globbyStream } from "globby" import { globbyStream } from "globby"
import chalk from "chalk" 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) { export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], verbose: boolean) {
const perf = new PerfTimer() const perf = new PerfTimer()
@ -19,9 +75,25 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu
return pathToPage return pathToPage
} }
// initialize from plugins
const staticResources = getStaticResourcesFromPlugins(cfg.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 let emittedFiles = 0
for (const emitter of cfg.plugins.emitters) { for (const emitter of cfg.plugins.emitters) {
try { try {

View File

@ -3,7 +3,8 @@ import { JSX } from "preact/jsx-runtime"
export type JSResource = { export type JSResource = {
loadTime: 'beforeDOMReady' | 'afterDOMReady' loadTime: 'beforeDOMReady' | 'afterDOMReady'
moduleType?: 'module' moduleType?: 'module',
spaPreserve?: boolean
} & ({ } & ({
src: string src: string
contentType: 'external' contentType: 'external'
@ -14,11 +15,12 @@ export type JSResource = {
export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element { export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
const scriptType = resource.moduleType ?? 'application/javascript' const scriptType = resource.moduleType ?? 'application/javascript'
const spaPreserve = preserve ?? resource.spaPreserve
if (resource.contentType === 'external') { 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 { } else {
const content = resource.script 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>
} }
} }

View File

@ -11,6 +11,9 @@ body {
box-sizing: border-box; box-sizing: border-box;
background-color: var(--light); background-color: var(--light);
font-family: var(--bodyFont); font-family: var(--bodyFont);
--pageWidth: 800px;
--sidePanelWidth: 400px;
--topSpacing: 6rem;
} }
.text-highlight { .text-highlight {
@ -27,7 +30,7 @@ p, ul, text, a, tr, td, li, ol, ul, .katex {
a { a {
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
transition: all 0.2s ease; transition: color 0.2s ease;
color: var(--secondary); color: var(--secondary);
&:hover { &:hover {
@ -43,34 +46,48 @@ a {
} }
.page { .page {
margin: 6rem 35vw 6rem 20vw; & > .page-header {
max-width: 1000px; max-width: var(--pageWidth);
position: relative; margin: var(--topSpacing) auto 0 auto;
}
& .left, & .right { & > #quartz-body {
position: fixed; width: 100%;
height: 100vh;
overflow-y: scroll;
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column;
top: 0;
gap: 2rem;
padding: 6rem;
}
& .left { & .left, & .right {
left: 0; flex: 1;
padding-left: 10vw; width: calc(calc(100vw - var(--pageWidth)) / 2);
width: 20vw; }
}
& .right { & .left-inner, & .right-inner {
right: 0; display: flex;
padding-right: 10vw; flex-direction: column;
width: 35vw; 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) { @media all and (max-width: 1200px) {
margin: 25px 5vw; margin: 25px 5vw;
& .left, & .right { & .left, & .right {
@ -89,9 +106,26 @@ a {
& > h1 { & > h1 {
font-size: 2rem; 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 { blockquote {
margin: 1rem 0; margin: 1rem 0;
border-left: 3px solid var(--secondary); border-left: 3px solid var(--secondary);
@ -120,7 +154,7 @@ thead {
} }
h1, h2, h3, h4, h5, h6 { h1, h2, h3, h4, h5, h6 {
&[id] > a { &[id] > a[href^="#"] {
margin: 0 0.5rem; margin: 0 0.5rem;
opacity: 0; opacity: 0;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;