From 1547c8af0d3fa4dcfe6055ff423d60a4d2ddf184 Mon Sep 17 00:00:00 2001 From: Jacky Zhao <j.zhao2k19@gmail.com> Date: Tue, 4 Jul 2023 10:08:32 -0700 Subject: [PATCH] fix indexing causing main thread freeze, various polish --- package-lock.json | 9 ++ package.json | 1 + quartz.config.ts | 4 +- quartz/components/DesktopOnly.tsx | 20 +++ quartz/components/Graph.tsx | 18 +-- quartz/components/Head.tsx | 4 +- quartz/components/MobileOnly.tsx | 20 +++ quartz/components/PageList.tsx | 2 +- quartz/components/ReadingTime.tsx | 3 +- quartz/components/TableOfContents.tsx | 2 +- quartz/components/TagList.tsx | 1 + quartz/components/index.ts | 6 +- quartz/components/pages/FolderContent.tsx | 10 +- quartz/components/pages/TagContent.tsx | 9 +- quartz/components/renderPage.tsx | 2 +- quartz/components/scripts/graph.inline.ts | 18 +-- quartz/components/scripts/popover.inline.ts | 130 ++++++++++---------- quartz/components/scripts/search.inline.ts | 8 +- quartz/components/styles/darkmode.scss | 1 + quartz/components/styles/graph.scss | 2 +- quartz/components/styles/listPage.scss | 6 +- quartz/components/styles/popover.scss | 4 +- quartz/components/styles/search.scss | 4 +- quartz/path.ts | 8 +- quartz/plugins/emitters/contentIndex.ts | 7 +- quartz/plugins/emitters/folderPage.tsx | 3 +- quartz/plugins/emitters/tagPage.tsx | 3 +- quartz/plugins/index.ts | 26 ++-- quartz/processors/emit.ts | 10 +- quartz/resources.tsx | 2 +- quartz/styles/base.scss | 40 +++--- quartz/styles/variables.scss | 5 + quartz/theme.ts | 8 +- 33 files changed, 255 insertions(+), 141 deletions(-) create mode 100644 quartz/components/DesktopOnly.tsx create mode 100644 quartz/components/MobileOnly.tsx create mode 100644 quartz/styles/variables.scss diff --git a/package-lock.json b/package-lock.json index 92e22eb..fb8f23c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "mdast-util-find-and-replace": "^2.2.2", "mdast-util-to-string": "^3.2.0", "micromorph": "^0.4.5", + "plausible-tracker": "^0.3.8", "preact": "^10.14.1", "preact-render-to-string": "^6.0.3", "pretty-time": "^1.1.0", @@ -3619,6 +3620,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plausible-tracker": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/plausible-tracker/-/plausible-tracker-0.3.8.tgz", + "integrity": "sha512-lmOWYQ7s9KOUJ1R+YTOR3HrjdbxIS2Z4de0P/Jx2dQPteznJl2eX3tXxKClpvbfyGP59B5bbhW8ftN59HbbFSg==", + "engines": { + "node": ">=10" + } + }, "node_modules/preact": { "version": "10.15.1", "resolved": "https://registry.npmjs.org/preact/-/preact-10.15.1.tgz", diff --git a/package.json b/package.json index 614bb76..689548d 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "mdast-util-find-and-replace": "^2.2.2", "mdast-util-to-string": "^3.2.0", "micromorph": "^0.4.5", + "plausible-tracker": "^0.3.8", "preact": "^10.14.1", "preact-render-to-string": "^6.0.3", "pretty-time": "^1.1.0", diff --git a/quartz.config.ts b/quartz.config.ts index 58c1d9c..18f2533 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -23,8 +23,8 @@ const contentPageLayout: PageLayout = { left: [ Component.PageTitle(), Component.Search(), - Component.TableOfContents(), - Component.Darkmode() + Component.Darkmode(), + Component.DesktopOnly(Component.TableOfContents()), ], right: [ Component.Graph(), diff --git a/quartz/components/DesktopOnly.tsx b/quartz/components/DesktopOnly.tsx new file mode 100644 index 0000000..a1c5dae --- /dev/null +++ b/quartz/components/DesktopOnly.tsx @@ -0,0 +1,20 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +export default ((component?: QuartzComponent) => { + if (component) { + const Component = component + function DesktopOnly(props: QuartzComponentProps) { + return <div class="desktop-only"> + <Component {...props} /> + </div> + } + + DesktopOnly.displayName = component.displayName + DesktopOnly.afterDOMLoaded = component?.afterDOMLoaded + DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded + DesktopOnly.css = component?.css + return DesktopOnly + } else { + return () => <></> + } +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx index 0146188..e7f1df2 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -25,23 +25,23 @@ const defaultOptions: GraphOptions = { drag: true, zoom: true, depth: 1, - scale: 1.2, - repelForce: 2, - centerForce: 1, + scale: 1.1, + repelForce: 0.5, + centerForce: 0.3, linkDistance: 30, fontSize: 0.6, - opacityScale: 3 + opacityScale: 1 }, globalGraph: { drag: true, zoom: true, depth: -1, - scale: 1.2, - repelForce: 1, - centerForce: 1, + scale: 0.9, + repelForce: 0.5, + centerForce: 0.3, linkDistance: 30, - fontSize: 0.5, - opacityScale: 3 + fontSize: 0.6, + opacityScale: 1 } } diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index f8439a0..bfc7bae 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -1,10 +1,10 @@ -import { resolveToRoot } from "../path" +import { clientSideSlug, resolveToRoot } from "../path" import { JSResourceToScriptElement } from "../resources" import { QuartzComponentConstructor, QuartzComponentProps } from "./types" export default (() => { function Head({ fileData, externalResources }: QuartzComponentProps) { - const slug = fileData.slug! + const slug = clientSideSlug(fileData.slug!) const title = fileData.frontmatter?.title ?? "Untitled" const description = fileData.description ?? "No description provided" const { css, js } = externalResources diff --git a/quartz/components/MobileOnly.tsx b/quartz/components/MobileOnly.tsx new file mode 100644 index 0000000..b75fd76 --- /dev/null +++ b/quartz/components/MobileOnly.tsx @@ -0,0 +1,20 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" + +export default ((component?: QuartzComponent) => { + if (component) { + const Component = component + function MobileOnly(props: QuartzComponentProps) { + return <div class="mobile-only"> + <Component {...props} /> + </div> + } + + MobileOnly.displayName = component.displayName + MobileOnly.afterDOMLoaded = component?.afterDOMLoaded + MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded + MobileOnly.css = component?.css + return MobileOnly + } else { + return () => <></> + } +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx index 3c39bee..b92720d 100644 --- a/quartz/components/PageList.tsx +++ b/quartz/components/PageList.tsx @@ -23,7 +23,7 @@ function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): numb export function PageList({ fileData, allFiles }: QuartzComponentProps) { const slug = fileData.slug! - return <ul class="section-ul popover-hint"> + return <ul class="section-ul"> {allFiles.sort(byDateAndAlphabetical).map(page => { const title = page.frontmatter?.title const pageSlug = page.slug! diff --git a/quartz/components/ReadingTime.tsx b/quartz/components/ReadingTime.tsx index cac8711..9c64289 100644 --- a/quartz/components/ReadingTime.tsx +++ b/quartz/components/ReadingTime.tsx @@ -3,8 +3,7 @@ import readingTime from "reading-time" function ReadingTime({ fileData }: QuartzComponentProps) { const text = fileData.text - const isHomePage = fileData.slug === "index" - if (text && !isHomePage) { + if (text) { const { text: timeTaken, words } = readingTime(text) return <p class="reading-time">{words} words, {timeTaken}</p> } else { diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx index f3d90bb..a9d7b78 100644 --- a/quartz/components/TableOfContents.tsx +++ b/quartz/components/TableOfContents.tsx @@ -18,7 +18,7 @@ function TableOfContents({ fileData }: QuartzComponentProps) { return null } - return <div> + return <div class="desktop-only"> <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"> diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx index 366889b..6b93719 100644 --- a/quartz/components/TagList.tsx +++ b/quartz/components/TagList.tsx @@ -29,6 +29,7 @@ TagList.css = ` .tags > li { display: inline-block; + white-space: nowrap; margin: 0; overflow-wrap: normal; } diff --git a/quartz/components/index.ts b/quartz/components/index.ts index ed0c668..35d5a64 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -13,6 +13,8 @@ import Graph from "./Graph" import Backlinks from "./Backlinks" import Search from "./Search" import Footer from "./Footer" +import DesktopOnly from "./DesktopOnly" +import MobileOnly from "./MobileOnly" export { ArticleTitle, @@ -29,5 +31,7 @@ export { Graph, Backlinks, Search, - Footer + Footer, + DesktopOnly, + MobileOnly } diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index 4806843..445074c 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -6,7 +6,7 @@ import path from "path" import style from '../styles/listPage.scss' import { PageList } from "../PageList" -function TagContent(props: QuartzComponentProps) { +function FolderContent(props: QuartzComponentProps) { const { tree, fileData, allFiles } = props const folderSlug = fileData.slug! const allPagesInFolder = allFiles.filter(file => { @@ -25,13 +25,15 @@ function TagContent(props: QuartzComponentProps) { // @ts-ignore const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) - return <div> + return <div class="popover-hint"> <article>{content}</article> + <hr/> + <p>{allPagesInFolder.length} items under this folder.</p> <div> <PageList {...listProps} /> </div> </div> } -TagContent.css = style + PageList.css -export default (() => TagContent) satisfies QuartzComponentConstructor +FolderContent.css = style + PageList.css +export default (() => FolderContent) satisfies QuartzComponentConstructor diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index e7e5f6d..b84ce29 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -3,13 +3,14 @@ import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' import { toJsxRuntime } from "hast-util-to-jsx-runtime" import style from '../styles/listPage.scss' import { PageList } from "../PageList" +import { clientSideSlug } from "../../path" function TagContent(props: QuartzComponentProps) { const { tree, fileData, allFiles } = props const slug = fileData.slug - if (slug?.startsWith("tags/")) { - const tag = slug.slice("tags/".length) + if (slug?.startsWith("tags/")) { + const tag = clientSideSlug(slug.slice("tags/".length)) const allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag)) const listProps = { ...props, @@ -18,8 +19,10 @@ function TagContent(props: QuartzComponentProps) { // @ts-ignore const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) - return <div> + return <div class="popover-hint"> <article>{content}</article> + <hr/> + <p>{allPagesWithTag.length} items with this tag.</p> <div> <PageList {...listProps} /> </div> diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index c70f092..4da6370 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -25,7 +25,7 @@ export function pageResources(slug: string, staticResources: StaticResources): S css: [baseDir + "/index.css", ...staticResources.css], js: [ { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" }, - { loadTime: "afterDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript }, + { loadTime: "beforeDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript }, ...staticResources.js, { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" } ] diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index 169b8c4..194a23b 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -110,12 +110,12 @@ async function renderGraph(container: string, slug: string) { .join("line") .attr("class", "link") .attr("stroke", "var(--lightgray)") - .attr("stroke-width", 2) + .attr("stroke-width", 1) // svg groups const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g") - // calculate radius + // calculate color const color = (d: NodeData) => { const isCurrent = d.id === slug if (isCurrent) { @@ -182,7 +182,12 @@ async function renderGraph(container: string, slug: string) { neighbourNodes.transition().duration(200).attr("fill", color) // highlight links - linkNodes.transition().duration(200).attr("stroke", "var(--gray)") + linkNodes + .transition() + .duration(200) + .attr("stroke", "var(--gray)") + .attr("stroke-width", 1) + const bigFont = fontSize * 1.5 @@ -220,7 +225,7 @@ async function renderGraph(container: string, slug: string) { const labels = graphNode .append("text") .attr("dx", 0) - .attr("dy", (d) => nodeRadius(d) + 8 + "px") + .attr("dy", (d) => nodeRadius(d) - 8 + "px") .attr("text-anchor", "middle") .text((d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " ")) .style('opacity', (opacityScale - 1) / 3.75) @@ -266,12 +271,11 @@ async function renderGraph(container: string, slug: string) { }) } -async function renderGlobalGraph() { +function renderGlobalGraph() { const slug = document.body.dataset["slug"]! - await renderGraph("global-graph-container", slug) const container = document.getElementById("global-graph-outer") container?.classList.add("active") - + renderGraph("global-graph-container", slug) function hideGlobalGraph() { container?.classList.remove("active") diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index b388995..666371b 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -19,69 +19,73 @@ export function normalizeRelativeURLs( ) } -document.addEventListener("nav", () => { - const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] - const p = new DOMParser() - for (const link of links) { - link.addEventListener("mouseenter", async ({ clientX, clientY }) => { - async function setPosition(popoverElement: HTMLElement) { - const { x, y } = await computePosition(link, popoverElement, { - middleware: [ - inline({ x: clientX, y: clientY }), - shift(), - flip() - ] - }) - Object.assign(popoverElement.style, { - left: `${x}px`, - top: `${y}px`, - }) - } - - if (link.dataset.fetchedPopover === "true") { - return setPosition(link.lastChild as HTMLElement) - } - - const thisUrl = new URL(document.location.href) - thisUrl.hash = "" - thisUrl.search = "" - const targetUrl = new URL(link.href) - const hash = targetUrl.hash - targetUrl.hash = "" - targetUrl.search = "" - // prevent hover of the same page - if (thisUrl.toString() === targetUrl.toString()) return - - const contents = await fetch(`${targetUrl}`) - .then((res) => res.text()) - .catch((err) => { - console.error(err) - }) - - if (!contents) return - const html = p.parseFromString(contents, "text/html") - normalizeRelativeURLs(html, targetUrl) - const elts = [...html.getElementsByClassName("popover-hint")] - if (elts.length === 0) return - - const popoverElement = document.createElement("div") - popoverElement.classList.add("popover") - const popoverInner = document.createElement("div") - popoverInner.classList.add("popover-inner") - popoverElement.appendChild(popoverInner) - elts.forEach(elt => popoverInner.appendChild(elt)) - - setPosition(popoverElement) - link.appendChild(popoverElement) - link.dataset.fetchedPopover = "true" - - 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' }) - } - } +const p = new DOMParser() +async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: { clientX: number, clientY: number }) { + const link = this + async function setPosition(popoverElement: HTMLElement) { + const { x, y } = await computePosition(link, popoverElement, { + middleware: [ + inline({ x: clientX, y: clientY }), + shift(), + flip() + ] + }) + Object.assign(popoverElement.style, { + left: `${x}px`, + top: `${y}px`, }) } + + // dont refetch if there's already a popover + if ([...link.children].some(child => child.classList.contains("popover"))) { + return setPosition(link.lastChild as HTMLElement) + } + + const thisUrl = new URL(document.location.href) + thisUrl.hash = "" + thisUrl.search = "" + const targetUrl = new URL(link.href) + const hash = targetUrl.hash + targetUrl.hash = "" + targetUrl.search = "" + // prevent hover of the same page + if (thisUrl.toString() === targetUrl.toString()) return + + const contents = await fetch(`${targetUrl}`) + .then((res) => res.text()) + .catch((err) => { + console.error(err) + }) + + if (!contents) return + const html = p.parseFromString(contents, "text/html") + normalizeRelativeURLs(html, targetUrl) + const elts = [...html.getElementsByClassName("popover-hint")] + if (elts.length === 0) return + + const popoverElement = document.createElement("div") + popoverElement.classList.add("popover") + const popoverInner = document.createElement("div") + popoverInner.classList.add("popover-inner") + popoverElement.appendChild(popoverInner) + elts.forEach(elt => popoverInner.appendChild(elt)) + + setPosition(popoverElement) + link.appendChild(popoverElement) + + 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' }) + } + } +} + +document.addEventListener("nav", () => { + const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] + for (const link of links) { + link.removeEventListener("mouseenter", mouseEnterHandler) + link.addEventListener("mouseenter", mouseEnterHandler) + } }) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index 78517fe..f69d77d 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -11,7 +11,8 @@ let index: Document<Item> | undefined = undefined const contextWindowWords = 30 function highlight(searchTerm: string, text: string, trim?: boolean) { - const tokenizedTerms = searchTerm.split(/\s+/).filter(t => t !== "") + // try to highlight longest tokens first + const tokenizedTerms = searchTerm.split(/\s+/).filter(t => t !== "").sort((a, b) => b.length - a.length) let tokenizedText = text .split(/\s+/) .filter(t => t !== "") @@ -42,7 +43,7 @@ function highlight(searchTerm: string, text: string, trim?: boolean) { // see if this tok is prefixed by any search terms for (const searchTok of tokenizedTerms) { if (tok.toLowerCase().includes(searchTok.toLowerCase())) { - const regex = new RegExp(searchTok, "gi") + const regex = new RegExp(searchTok.toLowerCase(), "gi") return tok.replace(regex, `<span class="highlight">$&</span>`) } } @@ -81,7 +82,7 @@ document.addEventListener("nav", async (e: unknown) => { }) for (const [slug, fileData] of Object.entries<ContentDetails>(data)) { - index.add({ + await index.addAsync(slug, { slug, title: fileData.title, content: fileData.content @@ -169,7 +170,6 @@ document.addEventListener("nav", async (e: unknown) => { displayResults(finalResults) } - document.removeEventListener("keydown", shortcutHandler) document.addEventListener("keydown", shortcutHandler) searchIcon?.removeEventListener("click", showSearch) diff --git a/quartz/components/styles/darkmode.scss b/quartz/components/styles/darkmode.scss index 730fcd2..0edfebd 100644 --- a/quartz/components/styles/darkmode.scss +++ b/quartz/components/styles/darkmode.scss @@ -2,6 +2,7 @@ position: relative; width: 20px; height: 20px; + margin: 1rem; & > .toggle { display: none; diff --git a/quartz/components/styles/graph.scss b/quartz/components/styles/graph.scss index 4533a84..2200282 100644 --- a/quartz/components/styles/graph.scss +++ b/quartz/components/styles/graph.scss @@ -40,9 +40,9 @@ top: 0; width: 100vw; height: 100%; - overflow: scroll; backdrop-filter: blur(4px); display: none; + overflow: hidden; &.active { display: inline-block; diff --git a/quartz/components/styles/listPage.scss b/quartz/components/styles/listPage.scss index 1823815..6c8e1b5 100644 --- a/quartz/components/styles/listPage.scss +++ b/quartz/components/styles/listPage.scss @@ -1,3 +1,5 @@ +@use "../../styles/variables.scss" as *; + ul.section-ul { list-style: none; margin-top: 2em; @@ -11,7 +13,7 @@ li.section-li { display: grid; grid-template-columns: 6em 3fr 1fr; - @media all and (max-width: 600px) { + @media all and (max-width: $mobileBreakpoint) { & > .tags { display: none; } @@ -22,7 +24,7 @@ li.section-li { margin-left: 1rem; } - & > .desc a { + & > .desc > h3 > a { background-color: transparent; } diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index 80bdfad..05c6dc7 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -1,3 +1,5 @@ +@use "../../styles/variables.scss" as *; + @keyframes dropin { 0% { opacity: 0; @@ -42,7 +44,7 @@ opacity: 0; transition: opacity 0.3s ease, visibility 0.3s ease; - @media all and (max-width: 600px) { + @media all and (max-width: $mobileBreakpoint) { display: none !important; } } diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index cbf982a..1f0a8b5 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -1,3 +1,5 @@ +@use "../../styles/variables.scss" as *; + .search { min-width: 5rem; max-width: 14rem; @@ -55,7 +57,7 @@ margin-left: auto; margin-right: auto; - @media all and (max-width: 1200px) { + @media all and (max-width: $tabletBreakpoint) { width: 90%; } diff --git a/quartz/path.ts b/quartz/path.ts index 81cdb3a..a0fb223 100644 --- a/quartz/path.ts +++ b/quartz/path.ts @@ -7,10 +7,16 @@ function slugSegment(s: string): string { // on the client, 'index' isn't ever rendered so we should clean it up export function clientSideSlug(fp: string): string { + // remove index if (fp.endsWith("index")) { fp = fp.slice(0, -"index".length) } + // remove trailing slash + if (fp.endsWith("/")) { + fp = fp.slice(0, -1) + } + return fp } @@ -23,7 +29,7 @@ export function trimPathSuffix(fp: string): string { } export function slugify(s: string): string { - const [fp, anchor] = s.split("#", 2) + let [fp, anchor] = s.split("#", 2) const sluggedAnchor = anchor === undefined ? "" : "#" + slugAnchor(anchor) const withoutFileExt = fp.replace(new RegExp(path.extname(fp) + '$'), '') const rawSlugSegments = withoutFileExt.split(path.sep) diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts index cf42756..a1d8648 100644 --- a/quartz/plugins/emitters/contentIndex.ts +++ b/quartz/plugins/emitters/contentIndex.ts @@ -15,11 +15,13 @@ export type ContentDetails = { interface Options { enableSiteMap: boolean enableRSS: boolean + includeEmptyFiles: boolean } const defaultOptions: Options = { enableSiteMap: true, enableRSS: true, + includeEmptyFiles: false, } function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { @@ -57,7 +59,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string { </rss>` } -export const ContentIndex: QuartzEmitterPlugin<Options> = (opts) => { +export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => { opts = { ...defaultOptions, ...opts } return { name: "ContentIndex", @@ -67,6 +69,7 @@ export const ContentIndex: QuartzEmitterPlugin<Options> = (opts) => { for (const [_tree, file] of content) { const slug = file.data.slug! const date = file.data.dates?.modified ?? new Date() + if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { linkIndex.set(slug, { title: file.data.frontmatter?.title!, links: file.data.links ?? [], @@ -75,6 +78,7 @@ export const ContentIndex: QuartzEmitterPlugin<Options> = (opts) => { date: date, description: file.data.description ?? "" }) + } } if (opts?.enableSiteMap) { @@ -106,6 +110,7 @@ export const ContentIndex: QuartzEmitterPlugin<Options> = (opts) => { return [slug, content] }) ) + await emit({ content: JSON.stringify(simplifiedIndex), slug: fp, diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx index ee8f0b9..1eed30d 100644 --- a/quartz/plugins/emitters/folderPage.tsx +++ b/quartz/plugins/emitters/folderPage.tsx @@ -6,6 +6,7 @@ import { pageResources, renderPage } from "../../components/renderPage" import { ProcessedContent, defaultProcessedContent } from "../vfile" import { FullPageLayout } from "../../cfg" import path from "path" +import { clientSideSlug } from "../../path" export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { if (!opts) { @@ -36,7 +37,7 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { ]))) for (const [tree, file] of content) { - const slug = file.data.slug! + const slug = clientSideSlug(file.data.slug!) if (folders.has(slug)) { folderDescriptions[slug] = [tree, file] } diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx index 1f69715..0cdb7c3 100644 --- a/quartz/plugins/emitters/tagPage.tsx +++ b/quartz/plugins/emitters/tagPage.tsx @@ -5,6 +5,7 @@ import BodyConstructor from "../../components/Body" import { pageResources, renderPage } from "../../components/renderPage" import { ProcessedContent, defaultProcessedContent } from "../vfile" import { FullPageLayout } from "../../cfg" +import { clientSideSlug } from "../../path" export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { if (!opts) { @@ -30,7 +31,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { ]))) for (const [tree, file] of content) { - const slug = file.data.slug! + const slug = clientSideSlug(file.data.slug!) if (slug.startsWith("tags/")) { const tag = slug.slice("tags/".length) if (tags.has(tag)) { diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts index c55e4dd..0de0283 100644 --- a/quartz/plugins/index.ts +++ b/quartz/plugins/index.ts @@ -20,26 +20,30 @@ export function getComponentResources(plugins: PluginTypes): ComponentResources } } - const componentResources: ComponentResources = { - css: [], - beforeDOMLoaded: [], - afterDOMLoaded: [] + const componentResources = { + css: new Set<string>(), + beforeDOMLoaded: new Set<string>(), + afterDOMLoaded: new Set<string>() } for (const component of allComponents) { const { css, beforeDOMLoaded, afterDOMLoaded } = component if (css) { - componentResources.css.push(css) + componentResources.css.add(css) } if (beforeDOMLoaded) { - componentResources.beforeDOMLoaded.push(beforeDOMLoaded) + componentResources.beforeDOMLoaded.add(beforeDOMLoaded) } if (afterDOMLoaded) { - componentResources.afterDOMLoaded.push(afterDOMLoaded) + componentResources.afterDOMLoaded.add(afterDOMLoaded) } } - - return componentResources + + return { + css: [...componentResources.css], + beforeDOMLoaded: [...componentResources.beforeDOMLoaded], + afterDOMLoaded: [...componentResources.afterDOMLoaded] + } } function joinScripts(scripts: string[]): string { @@ -78,10 +82,10 @@ export function getStaticResourcesFromPlugins(plugins: PluginTypes) { for (const transformer of plugins.transformers) { const res = transformer.externalResources ? transformer.externalResources() : {} if (res?.js) { - staticResources.js = staticResources.js.concat(res.js) + staticResources.js.push(...res.js) } if (res?.css) { - staticResources.css = staticResources.css.concat(res.css) + staticResources.css.push(...res.css) } } diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts index 7150f5e..59875f5 100644 --- a/quartz/processors/emit.ts +++ b/quartz/processors/emit.ts @@ -8,7 +8,6 @@ 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' @@ -18,9 +17,10 @@ import plausibleScript from '../components/scripts/plausible.inline' import popoverScript from '../components/scripts/popover.inline' import popoverStyle from '../components/styles/popover.scss' import { StaticResources } from "../resources" +import { QuartzLogger } from "../log" +import { googleFontHref } from "../theme" function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: StaticResources, componentResources: ComponentResources) { - // font and other resources staticResources.css.push(googleFontHref(cfg.theme)) // popovers @@ -67,6 +67,9 @@ function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: Stati export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], verbose: boolean) { const perf = new PerfTimer() + const log = new QuartzLogger(verbose) + + log.start(`Emitting output files`) const emit: EmitCallback = async ({ slug, ext, content }) => { const pathToPage = path.join(output, slug + ext) const dir = path.dirname(pathToPage) @@ -80,6 +83,7 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu // 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 @@ -136,5 +140,5 @@ export async function emitContent(contentFolder: string, output: string, cfg: Qu } } - console.log(`Emitted ${emittedFiles} files to \`${output}\` in ${perf.timeSince()}`) + log.success(`Emitted ${emittedFiles} files to \`${output}\` in ${perf.timeSince()}`) } diff --git a/quartz/resources.tsx b/quartz/resources.tsx index 3780751..525210b 100644 --- a/quartz/resources.tsx +++ b/quartz/resources.tsx @@ -17,7 +17,7 @@ export function JSResourceToScriptElement(resource: JSResource, preserve?: boole 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={spaPreserve} /> + 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={spaPreserve}>{content}</script> diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index db0299b..e741755 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -1,5 +1,6 @@ -@import "./syntax.scss"; -@import "./callouts.scss"; +@use "./syntax.scss"; +@use "./callouts.scss"; +@use "./variables.scss" as *; html { scroll-behavior: smooth; @@ -11,9 +12,6 @@ body { box-sizing: border-box; background-color: var(--light); font-family: var(--bodyFont); - --pageWidth: 800px; - --sidePanelWidth: 400px; - --topSpacing: 6rem; } .text-highlight { @@ -47,8 +45,8 @@ a { .page { & > .page-header { - max-width: var(--pageWidth); - margin: var(--topSpacing) auto 0 auto; + max-width: $pageWidth; + margin: $topSpacing auto 0 auto; } & > #quartz-body { @@ -57,7 +55,7 @@ a { & .left, & .right { flex: 1; - width: calc(calc(100vw - var(--pageWidth)) / 2); + width: calc(calc(100vw - $pageWidth) / 2); } & .left-inner, & .right-inner { @@ -65,30 +63,44 @@ a { flex-direction: column; gap: 2rem; top: 0; - width: var(--sidePanelWidth); - margin-top: calc(var(--topSpacing)); + width: $sidePanelWidth; + margin-top: $topSpacing; box-sizing: border-box; padding: 0 4rem; position: fixed; } & .left-inner { - left: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth)); + left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); } & .right-inner { - right: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth)); + right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); } & .center { - width: var(--pageWidth); + width: $pageWidth; margin: 0 auto; } } } +.desktop-only { + display: initial; + @media all and (max-width: ($pageWidth + 2 * $sidePanelWidth)) { + display: none; + } +} + +.mobile-only { + display: none; + @media all and (max-width: ($pageWidth + 2 * $sidePanelWidth)) { + display: initial; + } +} + .page { - @media all and (max-width: 1200px) { + @media all and (max-width: $tabletBreakpoint) { margin: 25px 5vw; & .left, & .right { padding: 0; diff --git a/quartz/styles/variables.scss b/quartz/styles/variables.scss new file mode 100644 index 0000000..792223b --- /dev/null +++ b/quartz/styles/variables.scss @@ -0,0 +1,5 @@ +$pageWidth: 800px; +$mobileBreakpoint: 600px; +$tabletBreakpoint: 1200px; +$sidePanelWidth: 400px; +$topSpacing: 6rem; diff --git a/quartz/theme.ts b/quartz/theme.ts index 318f5cc..820519f 100644 --- a/quartz/theme.ts +++ b/quartz/theme.ts @@ -21,6 +21,8 @@ export interface Theme { } } +const DEFAULT_SANS_SERIF = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif" +const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace" export function googleFontHref(theme: Theme) { const { code, header, body } = theme.typography return `https://fonts.googleapis.com/css2?family=${code}&family=${header}:wght@400;700&family=${body}:ital,wght@0,400;0,600;1,400;1,600&display=swap` @@ -37,9 +39,9 @@ export function joinStyles(theme: Theme, ...stylesheet: string[]) { --tertiary: ${theme.colors.lightMode.tertiary}; --highlight: ${theme.colors.lightMode.highlight}; - --headerFont: ${theme.typography.header}; - --bodyFont: ${theme.typography.body}; - --codeFont: ${theme.typography.code}; + --headerFont: ${theme.typography.header}, ${DEFAULT_SANS_SERIF}; + --bodyFont: ${theme.typography.body}, ${DEFAULT_SANS_SERIF}; + --codeFont: ${theme.typography.code}, ${DEFAULT_MONO}; } :root[saved-theme="dark"] {