diff --git a/quartz/build.ts b/quartz/build.ts index 7e6536c232d59..970bc9298de54 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -40,8 +40,13 @@ type BuildData = { type FileEvent = "add" | "change" | "delete" +function newBuildId() { + return new Date().toISOString() +} + async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { const ctx: BuildCtx = { + buildId: newBuildId(), argv, cfg, allSlugs: [], @@ -167,8 +172,9 @@ async function partialRebuildFromEntrypoint( return; } - const perf = new PerfTimer(); - console.log(chalk.yellow("Detected change, rebuilding...")); + const perf = new PerfTimer() + console.log(chalk.yellow("Detected change, rebuilding...")) + ctx.buildId = newBuildId() // UPDATE DEP GRAPH const fp = joinSegments(argv.directory, toPosixPath(filepath)) as FilePath; @@ -363,21 +369,17 @@ async function rebuildFromEntrypoint( return; } - const perf = new PerfTimer(); - console.log(chalk.yellow("Detected change, rebuilding...")); - try { - const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)); - - const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])] - .filter((fp) => !toRemove.has(fp)) - .map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath)); + const perf = new PerfTimer() + console.log(chalk.yellow("Detected change, rebuilding...")) + ctx.buildId = newBuildId() - ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])]; - const parsedContent = await parseMarkdown(ctx, filesToRebuild); - for (const content of parsedContent) { - const [_tree, vfile] = content; - contentMap.set(vfile.data.filePath!, content); - } + try { + const filesToRebuild = [...toRebuild].filter((fp) => !toRemove.has(fp)) + const parsedContent = await parseMarkdown(ctx, filesToRebuild) + for (const content of parsedContent) { + const [_tree, vfile] = content + contentMap.set(vfile.data.filePath!, content) + } for (const fp of toRemove) { contentMap.delete(fp); @@ -386,17 +388,24 @@ async function rebuildFromEntrypoint( const parsedFiles = [...contentMap.values()]; const filteredContent = filterContent(ctx, parsedFiles); - // TODO: we can probably traverse the link graph to figure out what's safe to delete here - // instead of just deleting everything - await rimraf(path.join(argv.output, ".*"), { glob: true }); - await emitContent(ctx, filteredContent); - console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)); - } catch (err) { - console.log(chalk.yellow("Rebuild failed. Waiting on a change to fix the error...")); - if (argv.verbose) { - console.log(chalk.red(err)); - } - } + // re-update slugs + const trackedSlugs = [...new Set([...contentMap.keys(), ...toRebuild, ...trackedAssets])] + .filter((fp) => !toRemove.has(fp)) + .map((fp) => slugifyFilePath(path.posix.relative(argv.directory, fp) as FilePath)) + + ctx.allSlugs = [...new Set([...initialSlugs, ...trackedSlugs])] + + // TODO: we can probably traverse the link graph to figure out what's safe to delete here + // instead of just deleting everything + await rimraf(path.join(argv.output, ".*"), { glob: true }) + await emitContent(ctx, filteredContent) + console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)) + } catch (err) { + console.log(chalk.yellow(`Rebuild failed. Waiting on a change to fix the error...`)) + if (argv.verbose) { + console.log(chalk.red(err)) + } + } release(); clientRefresh(); diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 7f85ea40fae9d..134ecd765ac40 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -82,10 +82,11 @@ export interface FullPageLayout { header: QuartzComponent[] beforeBody: QuartzComponent[] pageBody: QuartzComponent + afterBody: QuartzComponent[] left: QuartzComponent[] right: QuartzComponent[] footer: QuartzComponent } export type PageLayout = Pick -export type SharedLayout = Pick +export type SharedLayout = Pick diff --git a/quartz/components/Comments.tsx b/quartz/components/Comments.tsx new file mode 100644 index 0000000000000..8e449402683c1 --- /dev/null +++ b/quartz/components/Comments.tsx @@ -0,0 +1,44 @@ +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" +import { classNames } from "../util/lang" +// @ts-ignore +import script from "./scripts/comments.inline" + +type Options = { + provider: "giscus" + options: { + repo: `${string}/${string}` + repoId: string + category: string + categoryId: string + mapping?: "url" | "title" | "og:title" | "specific" | "number" | "pathname" + strict?: boolean + reactionsEnabled?: boolean + inputPosition?: "top" | "bottom" + } +} + +function boolToStringBool(b: boolean): string { + return b ? "1" : "0" +} + +export default ((opts: Options) => { + const Comments: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { + return ( +
+ ) + } + + Comments.afterDOMLoaded = script + + return Comments +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx index 8ed7c99b0a101..f64aad636ef52 100644 --- a/quartz/components/Darkmode.tsx +++ b/quartz/components/Darkmode.tsx @@ -9,41 +9,38 @@ import { classNames } from "../util/lang" const Darkmode: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { return ( -
- - - -
+ ) } diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index cffc079ef80f7..ec7c48ef77b23 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -44,12 +44,9 @@ export default ((userOpts?: Partial) => { // memoized let fileTree: FileNode let jsonTree: string + let lastBuildId: string = "" function constructFileTree(allFiles: QuartzPluginData[]) { - if (fileTree) { - return - } - // Construct tree from allFiles fileTree = new FileNode("") allFiles.forEach((file) => fileTree.add(file)) @@ -76,12 +73,17 @@ export default ((userOpts?: Partial) => { } const Explorer: QuartzComponent = ({ + ctx, cfg, allFiles, displayClass, fileData, }: QuartzComponentProps) => { - constructFileTree(allFiles) + if (ctx.buildId !== lastBuildId) { + lastBuildId = ctx.buildId + constructFileTree(allFiles) + } + return (
diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx index 937ad752c6b10..3dd981c3e67d3 100644 --- a/quartz/components/PageList.tsx +++ b/quartz/components/PageList.tsx @@ -1,102 +1,81 @@ -import path from "path"; -import { GlobalConfiguration } from "../cfg"; -import { QuartzPluginData } from "../plugins/vfile"; -import { FilePath, FullSlug, pathToRoot, resolveRelative, sluggify, slugifyFilePath } from "../util/path"; -import { Date, getDate } from "./Date"; -import { QuartzComponent, QuartzComponentProps } from "./types"; -import fs from 'fs'; -import { pathToFileURL } from "url"; - +import { GlobalConfiguration } from "../cfg" +import { QuartzPluginData } from "../plugins/vfile" +import { FullSlug, resolveRelative } from "../util/path" +import { Date, getDate } from "./Date" +import { QuartzComponent, QuartzComponentProps } from "./types" +export type SortFn = (f1: QuartzPluginData, f2: QuartzPluginData) => number export function byDateAndAlphabetical( - cfg: GlobalConfiguration, + cfg: GlobalConfiguration, ): (f1: QuartzPluginData, f2: QuartzPluginData) => number { - return (f1, f2) => { - if (f1.dates && f2.dates) { - // sort descending - return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime(); - } else if (f1.dates && !f2.dates) { - // prioritize files with dates - return -1; - } else if (!f1.dates && f2.dates) { - return 1; - } + return (f1, f2) => { + if (f1.dates && f2.dates) { + // sort descending + return getDate(cfg, f2)!.getTime() - getDate(cfg, f1)!.getTime() + } else if (f1.dates && !f2.dates) { + // prioritize files with dates + return -1 + } else if (!f1.dates && f2.dates) { + return 1 + } - // otherwise, sort lexographically by title - const f1Title = f1.frontmatter?.title?.toLowerCase() ?? ""; - const f2Title = f2.frontmatter?.title?.toLowerCase() ?? ""; - return f1Title.localeCompare(f2Title); - }; + // otherwise, sort lexographically by title + const f1Title = f1.frontmatter?.title?.toLowerCase() ?? "" + const f2Title = f2.frontmatter?.title?.toLowerCase() ?? "" + return f1Title.localeCompare(f2Title) + } } type Props = { limit?: number + sort?: SortFn } & QuartzComponentProps export const PageList: QuartzComponent = ({ cfg, fileData, allFiles, limit }: Props) => { - let list = allFiles.sort(byDateAndAlphabetical(cfg)); - if (limit) { - list = list.slice(0, limit); - } + let list = allFiles.sort(byDateAndAlphabetical(cfg)) + if (limit) { + list = list.slice(0, limit) + } + + return ( +
    + {list.map((page) => { + const title = page.frontmatter?.title + const tags = page.frontmatter?.tags ?? [] - return ( -
      - {list.map((page) => { - let title = page.frontmatter?.title ?? page.slug; - if (title === "index") { - const path = page.slug?.split("/"); - title = path?.[path.length - 2].replaceAll("-", " ") ?? "index"; - } - const tags = page.frontmatter?.tags ?? []; - const description = page.frontmatter?.description; - let image : string | undefined = page.frontmatter?.image as string ?? undefined ; - if (cfg.ogImageDir && image) { - const imageFromOg = `${cfg.ogImageDir}/${image}` - const imageRelative = resolveRelative(fileData.slug!, imageFromOg as FullSlug); - //verify if image exists in `content/_assets/img` folder - image = fs.existsSync(path.resolve(`content/${imageFromOg}`)) ? imageRelative : undefined; - } - return ( -
    • -
      -
      -

      - - {title} - -

      -
      -
      - {image && ( - - )} -
      -

      {description}

      -
      -

      - -
      - -
      -
    • - ); - })} -
    - ); -}; + return ( +
  • +
    + {page.dates && ( +

    + +

    + )} + + +
    +
  • + ) + })} +
+ ) +} PageList.css = ` .section h3 { @@ -122,4 +101,4 @@ PageList.css = ` left: -15px !important; transition: left 0.3s ease; } -`; +` diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx index d2655a2cd0067..2ba8b9c602f46 100644 --- a/quartz/components/PageTitle.tsx +++ b/quartz/components/PageTitle.tsx @@ -1,22 +1,23 @@ -import { i18n } from "../i18n"; -import { classNames } from "../util/lang"; -import { pathToRoot } from "../util/path"; -import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"; +import { i18n } from "../i18n" +import { classNames } from "../util/lang" +import { pathToRoot } from "../util/path" +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" const PageTitle: QuartzComponent = ({ fileData, cfg, displayClass }: QuartzComponentProps) => { - const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title; - const baseDir = pathToRoot(fileData.slug!); - return ( -

- {title} -

- ); -}; + const title = cfg?.pageTitle ?? i18n(cfg.locale).propertyDefaults.title + const baseDir = pathToRoot(fileData.slug!) + return ( +

+ {title} +

+ ) +} PageTitle.css = ` .page-title { + font-size: 1.75rem; margin: 0; } -`; +` -export default (() => PageTitle) satisfies QuartzComponentConstructor; \ No newline at end of file +export default (() => PageTitle) satisfies QuartzComponentConstructor diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx index 0703f8aa79409..d0e8041c71da7 100644 --- a/quartz/components/Search.tsx +++ b/quartz/components/Search.tsx @@ -20,24 +20,16 @@ export default ((userOpts?: Partial) => { const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder return (
-
+
+
- -
+
    {fileData.toc.map((tocEntry) => (
  • diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 1bd78589a0ae1..3e4a945a29af8 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -1,47 +1,49 @@ -import ArticleTitle from "./ArticleTitle"; -import Backlinks from "./Backlinks"; -import Breadcrumbs from "./Breadcrumbs"; -import ContentMeta from "./ContentMeta"; -import Darkmode from "./Darkmode"; -import DesktopOnly from "./DesktopOnly"; -import Explorer from "./Explorer"; -import ExplorerBurger from "./ExplorerBurger"; -import Footer from "./Footer"; -import Graph from "./Graph"; -import Head from "./Head"; -import MobileOnly from "./MobileOnly"; -import NotFound from "./pages/404"; -import Content from "./pages/Content"; -import FolderContent from "./pages/FolderContent"; -import TagContent from "./pages/TagContent"; -import PageTitle from "./PageTitle"; -import RecentNotes from "./RecentNotes"; -import Search from "./Search"; -import Spacer from "./Spacer"; -import TableOfContents from "./TableOfContents"; -import TagList from "./TagList"; +import Content from "./pages/Content" +import TagContent from "./pages/TagContent" +import FolderContent from "./pages/FolderContent" +import NotFound from "./pages/404" +import ArticleTitle from "./ArticleTitle" +import Darkmode from "./Darkmode" +import Head from "./Head" +import PageTitle from "./PageTitle" +import ContentMeta from "./ContentMeta" +import Spacer from "./Spacer" +import TableOfContents from "./TableOfContents" +import Explorer from "./Explorer" +import TagList from "./TagList" +import Graph from "./Graph" +import Backlinks from "./Backlinks" +import Search from "./Search" +import Footer from "./Footer" +import DesktopOnly from "./DesktopOnly" +import MobileOnly from "./MobileOnly" +import RecentNotes from "./RecentNotes" +import Breadcrumbs from "./Breadcrumbs" +import Comments from "./Comments" +import ExplorerBurger from "./ExplorerBurger" export { - ArticleTitle, - Backlinks, - Breadcrumbs, - Content, - ContentMeta, - Darkmode, - DesktopOnly, - Explorer, - ExplorerBurger, - FolderContent, - Footer, - Graph, - Head, - MobileOnly, - NotFound, - PageTitle, - RecentNotes, - Search, - Spacer, - TableOfContents, - TagContent, - TagList, -}; + ArticleTitle, + Content, + TagContent, + FolderContent, + Darkmode, + Head, + PageTitle, + ContentMeta, + Spacer, + TableOfContents, + Explorer, + TagList, + Graph, + Backlinks, + Search, + Footer, + DesktopOnly, + MobileOnly, + RecentNotes, + NotFound, + Breadcrumbs, + Comments, + ExplorerBurger, +} diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx index ffd8255a61fa9..509d7d4009267 100644 --- a/quartz/components/pages/FolderContent.tsx +++ b/quartz/components/pages/FolderContent.tsx @@ -5,7 +5,7 @@ import { i18n } from "../../i18n" import { htmlToJsx } from "../../util/jsx" import { classNames } from "../../util/lang" import { simplifySlug, stripSlashes } from "../../util/path" -import { PageList } from "../PageList" +import { PageList, SortFn } from "../PageList" import style from "../styles/listPage.scss" import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" @@ -14,6 +14,7 @@ interface FolderContentOptions { * Whether to display number of folders */ showFolderCount: boolean + sort?: SortFn } const defaultOptions: FolderContentOptions = { @@ -37,6 +38,7 @@ export default ((opts?: Partial) => { const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] const listProps = { ...props, + sort: options.sort, allFiles: allPagesInFolder, } diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx index be01ad7d41bcb..e41ab4644083b 100644 --- a/quartz/components/pages/TagContent.tsx +++ b/quartz/components/pages/TagContent.tsx @@ -1,114 +1,127 @@ -import { Root } from "hast"; +import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types" +import style from "../styles/listPage.scss" +import { PageList, SortFn } from "../PageList" +import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path" +import { QuartzPluginData } from "../../plugins/vfile" +import { Root } from "hast" +import { htmlToJsx } from "../../util/jsx" +import { i18n } from "../../i18n" -import { i18n } from "../../i18n"; -import { QuartzPluginData } from "../../plugins/vfile"; -import { htmlToJsx } from "../../util/jsx"; -import { classNames } from "../../util/lang"; -import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"; -import { PageList } from "../PageList"; -import style from "../styles/listPage.scss"; -import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "../types"; +interface TagContentOptions { + sort?: SortFn + numPages: number +} -const numPages = 10; -const TagContent: QuartzComponent = (props: QuartzComponentProps) => { - const { tree, fileData, allFiles, cfg } = props; - const slug = fileData.slug; +const defaultOptions: TagContentOptions = { + numPages: 10, +} - if (!(slug?.startsWith("tags/") || slug === "tags")) { - throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`); - } +export default ((opts?: Partial) => { + const options: TagContentOptions = { ...defaultOptions, ...opts } - const tag = simplifySlug(slug.slice("tags/".length) as FullSlug); - const allPagesWithTag = (tag: string) => - allFiles.filter((file) => - (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag), - ); + const TagContent: QuartzComponent = (props: QuartzComponentProps) => { + const { tree, fileData, allFiles, cfg } = props + const slug = fileData.slug - const content = - (tree as Root).children.length === 0 - ? fileData.description - : htmlToJsx(fileData.filePath!, tree); - const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? []; - if (tag === "/") { - const tags = [ - ...new Set( - allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes), - ), - ].sort((a, b) => a.localeCompare(b)); - const tagItemMap: Map = new Map(); - for (const tag of tags) { - tagItemMap.set(tag, allPagesWithTag(tag)); - } - return ( -
    -
    -

    {content}

    -
    -

    {i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}

    -
    - {tags.map((tag) => { - const pages = tagItemMap.get(tag)!; - const listProps = { - ...props, - allFiles: pages, - }; + if (!(slug?.startsWith("tags/") || slug === "tags")) { + throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`) + } - const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`).at(0); + const tag = simplifySlug(slug.slice("tags/".length) as FullSlug) + const allPagesWithTag = (tag: string) => + allFiles.filter((file) => + (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag), + ) - const root = contentPage?.htmlAst; - const content = - !root || root?.children.length === 0 - ? contentPage?.description - : htmlToJsx(contentPage.filePath!, root); + const content = + (tree as Root).children.length === 0 + ? fileData.description + : htmlToJsx(fileData.filePath!, tree) + const cssClasses: string[] = fileData.frontmatter?.cssclasses ?? [] + const classes = ["popover-hint", ...cssClasses].join(" ") + if (tag === "/") { + const tags = [ + ...new Set( + allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes), + ), + ].sort((a, b) => a.localeCompare(b)) + const tagItemMap: Map = new Map() + for (const tag of tags) { + tagItemMap.set(tag, allPagesWithTag(tag)) + } + return ( +
    +
    +

    {content}

    +
    +

    {i18n(cfg.locale).pages.tagContent.totalTags({ count: tags.length })}

    +
    + {tags.map((tag) => { + const pages = tagItemMap.get(tag)! + const listProps = { + ...props, + allFiles: pages, + } - return ( -
    -

    - - {tag} - -

    - {content &&

    {content}

    } -
    -

    - {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })} - {pages.length > numPages && ( - <> - {" "} - - {i18n(cfg.locale).pages.tagContent.showingFirst({ count: numPages })} - - - )} -

    - -
    -
    - ); - })} -
    -
    - ); - } else { - const pages = allPagesWithTag(tag); - const listProps = { - ...props, - allFiles: pages, - }; + const contentPage = allFiles.filter((file) => file.slug === `tags/${tag}`).at(0) - return ( -
    -
    {content}
    -
    -

    {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}

    -
    - -
    -
    -
    - ); - } -}; + const root = contentPage?.htmlAst + const content = + !root || root?.children.length === 0 + ? contentPage?.description + : htmlToJsx(contentPage.filePath!, root) -TagContent.css = style + PageList.css; -export default (() => TagContent) satisfies QuartzComponentConstructor; + return ( +
    +

    + + {tag} + +

    + {content &&

    {content}

    } +
    +

    + {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })} + {pages.length > options.numPages && ( + <> + {" "} + + {i18n(cfg.locale).pages.tagContent.showingFirst({ + count: options.numPages, + })} + + + )} +

    + +
    +
    + ) + })} +
    +
    + ) + } else { + const pages = allPagesWithTag(tag) + const listProps = { + ...props, + allFiles: pages, + } + + return ( +
    +
    {content}
    +
    +

    {i18n(cfg.locale).pages.tagContent.itemsUnderTag({ count: pages.length })}

    +
    + +
    +
    +
    + ) + } + } + + TagContent.css = style + PageList.css + return TagContent +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 251a53f2ed1b1..ec5124f4f8c08 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -14,6 +14,7 @@ interface RenderComponents { header: QuartzComponent[] beforeBody: QuartzComponent[] pageBody: QuartzComponent + afterBody: QuartzComponent[] left: QuartzComponent[] right: QuartzComponent[] footer: QuartzComponent @@ -187,6 +188,7 @@ export function renderPage( header, beforeBody, pageBody: Content, + afterBody, left, right, footer: Footer, @@ -232,6 +234,12 @@ export function renderPage(
+
+
{RightComponent} diff --git a/quartz/components/scripts/comments.inline.ts b/quartz/components/scripts/comments.inline.ts new file mode 100644 index 0000000000000..4ab29f087cc08 --- /dev/null +++ b/quartz/components/scripts/comments.inline.ts @@ -0,0 +1,67 @@ +const changeTheme = (e: CustomEventMap["themechange"]) => { + const theme = e.detail.theme + const iframe = document.querySelector("iframe.giscus-frame") as HTMLIFrameElement + if (!iframe) { + return + } + + if (!iframe.contentWindow) { + return + } + + iframe.contentWindow.postMessage( + { + giscus: { + setConfig: { + theme: theme, + }, + }, + }, + "https://giscus.app", + ) +} + +type GiscusElement = Omit & { + dataset: DOMStringMap & { + repo: `${string}/${string}` + repoId: string + category: string + categoryId: string + mapping: "url" | "title" | "og:title" | "specific" | "number" | "pathname" + strict: string + reactionsEnabled: string + inputPosition: "top" | "bottom" + } +} + +document.addEventListener("nav", () => { + const giscusContainer = document.querySelector(".giscus") as GiscusElement + if (!giscusContainer) { + return + } + + const giscusScript = document.createElement("script") + giscusScript.src = "https://giscus.app/client.js" + giscusScript.async = true + giscusScript.crossOrigin = "anonymous" + giscusScript.setAttribute("data-loading", "lazy") + giscusScript.setAttribute("data-emit-metadata", "0") + giscusScript.setAttribute("data-repo", giscusContainer.dataset.repo) + giscusScript.setAttribute("data-repo-id", giscusContainer.dataset.repoId) + giscusScript.setAttribute("data-category", giscusContainer.dataset.category) + giscusScript.setAttribute("data-category-id", giscusContainer.dataset.categoryId) + giscusScript.setAttribute("data-mapping", giscusContainer.dataset.mapping) + giscusScript.setAttribute("data-strict", giscusContainer.dataset.strict) + giscusScript.setAttribute("data-reactions-enabled", giscusContainer.dataset.reactionsEnabled) + giscusScript.setAttribute("data-input-position", giscusContainer.dataset.inputPosition) + + const theme = document.documentElement.getAttribute("saved-theme") + if (theme) { + giscusScript.setAttribute("data-theme", theme) + } + + giscusContainer.appendChild(giscusScript) + + document.addEventListener("themechange", changeTheme) + window.addCleanup(() => document.removeEventListener("themechange", changeTheme)) +}) diff --git a/quartz/components/scripts/darkmode.inline.ts b/quartz/components/scripts/darkmode.inline.ts index 50b9c1ad72072..438bf8dffda35 100644 --- a/quartz/components/scripts/darkmode.inline.ts +++ b/quartz/components/scripts/darkmode.inline.ts @@ -10,28 +10,25 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => { }; document.addEventListener("nav", () => { - const switchTheme = (e: Event) => { - const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light"; - document.documentElement.setAttribute("saved-theme", newTheme); - localStorage.setItem("theme", newTheme); - emitThemeChangeEvent(newTheme); - }; + const switchTheme = (e: Event) => { + const newTheme = + document.documentElement.getAttribute("saved-theme") === "dark" ? "light" : "dark" + document.documentElement.setAttribute("saved-theme", newTheme) + localStorage.setItem("theme", newTheme) + emitThemeChangeEvent(newTheme) + } - const themeChange = (e: MediaQueryListEvent) => { - const newTheme = e.matches ? "dark" : "light"; - document.documentElement.setAttribute("saved-theme", newTheme); - localStorage.setItem("theme", newTheme); - toggleSwitch.checked = e.matches; - emitThemeChangeEvent(newTheme); - }; + const themeChange = (e: MediaQueryListEvent) => { + const newTheme = e.matches ? "dark" : "light" + document.documentElement.setAttribute("saved-theme", newTheme) + localStorage.setItem("theme", newTheme) + emitThemeChangeEvent(newTheme) + } - // Darkmode toggle - const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement; - toggleSwitch.addEventListener("change", switchTheme); - window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme)); - if (currentTheme === "dark") { - toggleSwitch.checked = true; - } + // Darkmode toggle + const themeButton = document.querySelector("#darkmode") as HTMLButtonElement + themeButton.addEventListener("click", switchTheme) + window.addCleanup(() => themeButton.removeEventListener("click", switchTheme)) // Listen for changes in prefers-color-scheme const colorSchemeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts index bd111d44230a6..596a1a2cee354 100644 --- a/quartz/components/scripts/explorer.inline.ts +++ b/quartz/components/scripts/explorer.inline.ts @@ -16,9 +16,13 @@ const observer = new IntersectionObserver((entries) => { }); function toggleExplorer(this: HTMLElement) { - this.classList.toggle("collapsed"); - const content = this.nextElementSibling as MaybeHTMLElement; - if (!content) return; + this.classList.toggle("collapsed") + this.setAttribute( + "aria-expanded", + this.getAttribute("aria-expanded") === "true" ? "false" : "true", + ) + const content = this.nextElementSibling as MaybeHTMLElement + if (!content) return content.classList.toggle("collapsed"); content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"; diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index 48e059b8fb450..6bf43aa84e227 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -1,19 +1,56 @@ -import type { ContentDetails, ContentIndex } from "../../plugins/emitters/contentIndex" -import * as d3 from "d3" +import type { ContentDetails } from "../../plugins/emitters/contentIndex" +import { + SimulationNodeDatum, + SimulationLinkDatum, + Simulation, + forceSimulation, + forceManyBody, + forceCenter, + forceLink, + forceCollide, + zoomIdentity, + select, + drag, + zoom, +} from "d3" +import { Text, Graphics, Application, Container, Circle } from "pixi.js" +import { Group as TweenGroup, Tween as Tweened } from "@tweenjs/tween.js" import { registerEscapeHandler, removeAllChildren } from "./util" import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" +import { D3Config } from "../Graph" + +type GraphicsInfo = { + color: string + gfx: Graphics + alpha: number + active: boolean +} type NodeData = { id: SimpleSlug text: string tags: string[] -} & d3.SimulationNodeDatum +} & SimulationNodeDatum -type LinkData = { +type SimpleLinkData = { source: SimpleSlug target: SimpleSlug } +type LinkData = { + source: NodeData + target: NodeData +} & SimulationLinkDatum + +type LinkRenderData = GraphicsInfo & { + simulationData: LinkData +} + +type NodeRenderData = GraphicsInfo & { + simulationData: NodeData + label: Text +} + const localStorageKey = "graph-visited" function getVisited(): Set { return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]")) @@ -25,6 +62,11 @@ function addToVisited(slug: SimpleSlug) { localStorage.setItem(localStorageKey, JSON.stringify([...visited])) } +type TweenNode = { + update: (time: number) => void + stop: () => void +} + async function renderGraph(container: string, fullSlug: FullSlug) { const slug = simplifySlug(fullSlug) const visited = getVisited() @@ -45,7 +87,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { removeTags, showTags, focusOnHover, - } = JSON.parse(graph.dataset["cfg"]!) + } = JSON.parse(graph.dataset["cfg"]!) as D3Config const data: Map = new Map( Object.entries(await fetchData).map(([k, v]) => [ @@ -53,10 +95,11 @@ async function renderGraph(container: string, fullSlug: FullSlug) { v, ]), ) - const links: LinkData[] = [] + const links: SimpleLinkData[] = [] const tags: SimpleSlug[] = [] - const validLinks = new Set(data.keys()) + + const tweens = new Map() for (const [source, details] of data.entries()) { const outgoing = details.links ?? [] @@ -100,271 +143,446 @@ async function renderGraph(container: string, fullSlug: FullSlug) { if (showTags) tags.forEach((tag) => neighbourhood.add(tag)) } + const nodes = [...neighbourhood].map((url) => { + const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url) + return { + id: url, + text, + tags: data.get(url)?.tags ?? [], + } + }) const graphData: { nodes: NodeData[]; links: LinkData[] } = { - nodes: [...neighbourhood].map((url) => { - const text = url.startsWith("tags/") ? "#" + url.substring(5) : data.get(url)?.title ?? url - return { - id: url, - text: text, - tags: data.get(url)?.tags ?? [], - } - }), - links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)), + nodes, + links: links + .filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)) + .map((l) => ({ + source: nodes.find((n) => n.id === l.source)!, + target: nodes.find((n) => n.id === l.target)!, + })), } - const simulation: d3.Simulation = d3 - .forceSimulation(graphData.nodes) - .force("charge", d3.forceManyBody().strength(-100 * repelForce)) - .force( - "link", - d3 - .forceLink(graphData.links) - .id((d: any) => d.id) - .distance(linkDistance), - ) - .force("center", d3.forceCenter().strength(centerForce)) + // we virtualize the simulation and use pixi to actually render it + const simulation: Simulation = forceSimulation(graphData.nodes) + .force("charge", forceManyBody().strength(-100 * repelForce)) + .force("center", forceCenter().strength(centerForce)) + .force("link", forceLink(graphData.links).distance(linkDistance)) + .force("collide", forceCollide((n) => nodeRadius(n)).iterations(3)) - const height = Math.max(graph.offsetHeight, 250) const width = graph.offsetWidth + const height = Math.max(graph.offsetHeight, 250) - const svg = d3 - .select("#" + container) - .append("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale]) - - // draw links between nodes - const link = svg - .append("g") - .selectAll("line") - .data(graphData.links) - .join("line") - .attr("class", "link") - .attr("stroke", "var(--lightgray)") - .attr("stroke-width", 1) - - // svg groups - const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g") + // precompute style prop strings as pixi doesn't support css variables + const cssVars = [ + "--secondary", + "--tertiary", + "--gray", + "--light", + "--lightgray", + "--dark", + "--darkgray", + "--bodyFont", + ] as const + const computedStyleMap = cssVars.reduce( + (acc, key) => { + acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key) + return acc + }, + {} as Record<(typeof cssVars)[number], string>, + ) // calculate color const color = (d: NodeData) => { const isCurrent = d.id === slug if (isCurrent) { - return "var(--secondary)" + return computedStyleMap["--secondary"] } else if (visited.has(d.id) || d.id.startsWith("tags/")) { - return "var(--tertiary)" + return computedStyleMap["--tertiary"] } else { - return "var(--gray)" + return computedStyleMap["--gray"] } } - const drag = (simulation: d3.Simulation) => { - function dragstarted(event: any, d: NodeData) { - if (!event.active) simulation.alphaTarget(1).restart() - d.fx = d.x - d.fy = d.y - } + function nodeRadius(d: NodeData) { + const numLinks = graphData.links.filter( + (l) => l.source.id === d.id || l.target.id === d.id, + ).length + return 2 + Math.sqrt(numLinks) + } + + let hoveredNodeId: string | null = null + let hoveredNeighbours: Set = new Set() + const linkRenderData: LinkRenderData[] = [] + const nodeRenderData: NodeRenderData[] = [] + function updateHoverInfo(newHoveredId: string | null) { + hoveredNodeId = newHoveredId + + if (newHoveredId === null) { + hoveredNeighbours = new Set() + for (const n of nodeRenderData) { + n.active = false + } - function dragged(event: any, d: NodeData) { - d.fx = event.x - d.fy = event.y + for (const l of linkRenderData) { + l.active = false + } + } else { + hoveredNeighbours = new Set() + for (const l of linkRenderData) { + const linkData = l.simulationData + if (linkData.source.id === newHoveredId || linkData.target.id === newHoveredId) { + hoveredNeighbours.add(linkData.source.id) + hoveredNeighbours.add(linkData.target.id) + } + + l.active = linkData.source.id === newHoveredId || linkData.target.id === newHoveredId + } + + for (const n of nodeRenderData) { + n.active = hoveredNeighbours.has(n.simulationData.id) + } } + } - function dragended(event: any, d: NodeData) { - if (!event.active) simulation.alphaTarget(0) - d.fx = null - d.fy = null + let dragStartTime = 0 + let dragging = false + + function renderLinks() { + tweens.get("link")?.stop() + const tweenGroup = new TweenGroup() + + for (const l of linkRenderData) { + let alpha = 1 + + // if we are hovering over a node, we want to highlight the immediate neighbours + // with full alpha and the rest with default alpha + if (hoveredNodeId) { + alpha = l.active ? 1 : 0.2 + } + + l.color = l.active ? computedStyleMap["--gray"] : computedStyleMap["--lightgray"] + tweenGroup.add(new Tweened(l).to({ alpha }, 200)) } - const noop = () => {} - return d3 - .drag() - .on("start", enableDrag ? dragstarted : noop) - .on("drag", enableDrag ? dragged : noop) - .on("end", enableDrag ? dragended : noop) + tweenGroup.getAll().forEach((tw) => tw.start()) + tweens.set("link", { + update: tweenGroup.update.bind(tweenGroup), + stop() { + tweenGroup.getAll().forEach((tw) => tw.stop()) + }, + }) } - function nodeRadius(d: NodeData) { - const numLinks = links.filter((l: any) => l.source.id === d.id || l.target.id === d.id).length - return 2 + Math.sqrt(numLinks) - } + function renderLabels() { + tweens.get("label")?.stop() + const tweenGroup = new TweenGroup() + + const defaultScale = 1 / scale + const activeScale = defaultScale * 1.1 + for (const n of nodeRenderData) { + const nodeId = n.simulationData.id + + if (hoveredNodeId === nodeId) { + tweenGroup.add( + new Tweened(n.label).to( + { + alpha: 1, + scale: { x: activeScale, y: activeScale }, + }, + 100, + ), + ) + } else { + tweenGroup.add( + new Tweened(n.label).to( + { + alpha: n.label.alpha, + scale: { x: defaultScale, y: defaultScale }, + }, + 100, + ), + ) + } + } - let connectedNodes: SimpleSlug[] = [] - - // draw individual nodes - const node = graphNode - .append("circle") - .attr("class", "node") - .attr("id", (d) => d.id) - .attr("r", nodeRadius) - .attr("fill", color) - .style("cursor", "pointer") - .on("click", (_, d) => { - const targ = resolveRelative(fullSlug, d.id) - window.spaNavigate(new URL(targ, window.location.toString())) + tweenGroup.getAll().forEach((tw) => tw.start()) + tweens.set("label", { + update: tweenGroup.update.bind(tweenGroup), + stop() { + tweenGroup.getAll().forEach((tw) => tw.stop()) + }, }) - .on("mouseover", function (_, d) { - const currentId = d.id - const linkNodes = d3 - .selectAll(".link") - .filter((d: any) => d.source.id === currentId || d.target.id === currentId) - - if (focusOnHover) { - // fade out non-neighbour nodes - connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id]) - - d3.selectAll(".link") - .transition() - .duration(200) - .style("opacity", 0.2) - d3.selectAll(".node") - .filter((d) => !connectedNodes.includes(d.id)) - .transition() - .duration(200) - .style("opacity", 0.2) - - d3.selectAll(".node") - .filter((d) => !connectedNodes.includes(d.id)) - .nodes() - .map((it) => d3.select(it.parentNode as HTMLElement).select("text")) - .forEach((it) => { - let opacity = parseFloat(it.style("opacity")) - it.transition() - .duration(200) - .attr("opacityOld", opacity) - .style("opacity", Math.min(opacity, 0.2)) - }) + } + + function renderNodes() { + tweens.get("hover")?.stop() + + const tweenGroup = new TweenGroup() + for (const n of nodeRenderData) { + let alpha = 1 + + // if we are hovering over a node, we want to highlight the immediate neighbours + if (hoveredNodeId !== null && focusOnHover) { + alpha = n.active ? 1 : 0.2 } - // highlight links - linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1) - - const bigFont = fontSize * 1.5 - - // show text for self - const parent = this.parentNode as HTMLElement - d3.select(parent) - .raise() - .select("text") - .transition() - .duration(200) - .attr("opacityOld", d3.select(parent).select("text").style("opacity")) - .style("opacity", 1) - .style("font-size", bigFont + "em") + tweenGroup.add(new Tweened(n.gfx, tweenGroup).to({ alpha }, 200)) + } + + tweenGroup.getAll().forEach((tw) => tw.start()) + tweens.set("hover", { + update: tweenGroup.update.bind(tweenGroup), + stop() { + tweenGroup.getAll().forEach((tw) => tw.stop()) + }, }) - .on("mouseleave", function (_, d) { - if (focusOnHover) { - d3.selectAll(".link").transition().duration(200).style("opacity", 1) - d3.selectAll(".node").transition().duration(200).style("opacity", 1) - - d3.selectAll(".node") - .filter((d) => !connectedNodes.includes(d.id)) - .nodes() - .map((it) => d3.select(it.parentNode as HTMLElement).select("text")) - .forEach((it) => it.transition().duration(200).style("opacity", it.attr("opacityOld"))) - } - const currentId = d.id - const linkNodes = d3 - .selectAll(".link") - .filter((d: any) => d.source.id === currentId || d.target.id === currentId) - - linkNodes.transition().duration(200).attr("stroke", "var(--lightgray)") - - const parent = this.parentNode as HTMLElement - d3.select(parent) - .select("text") - .transition() - .duration(200) - .style("opacity", d3.select(parent).select("text").attr("opacityOld")) - .style("font-size", fontSize + "em") + } + + function renderPixiFromD3() { + renderNodes() + renderLinks() + renderLabels() + } + + tweens.forEach((tween) => tween.stop()) + tweens.clear() + + const app = new Application() + await app.init({ + width, + height, + antialias: true, + autoStart: false, + autoDensity: true, + backgroundAlpha: 0, + preference: "webgpu", + resolution: window.devicePixelRatio, + eventMode: "static", + }) + graph.appendChild(app.canvas) + + const stage = app.stage + stage.interactive = false + + const labelsContainer = new Container({ zIndex: 3 }) + const nodesContainer = new Container({ zIndex: 2 }) + const linkContainer = new Container({ zIndex: 1 }) + stage.addChild(nodesContainer, labelsContainer, linkContainer) + + for (const n of graphData.nodes) { + const nodeId = n.id + + const label = new Text({ + interactive: false, + eventMode: "none", + text: n.text, + alpha: 0, + anchor: { x: 0.5, y: 1.2 }, + style: { + fontSize: fontSize * 15, + fill: computedStyleMap["--dark"], + fontFamily: computedStyleMap["--bodyFont"], + }, + resolution: window.devicePixelRatio * 4, + }) + label.scale.set(1 / scale) + + let oldLabelOpacity = 0 + const isTagNode = nodeId.startsWith("tags/") + const gfx = new Graphics({ + interactive: true, + label: nodeId, + eventMode: "static", + hitArea: new Circle(0, 0, nodeRadius(n)), + cursor: "pointer", }) - // @ts-ignore - .call(drag(simulation)) - - // make tags hollow circles - node - .filter((d) => d.id.startsWith("tags/")) - .attr("stroke", color) - .attr("stroke-width", 2) - .attr("fill", "var(--light)") - - // draw labels - const labels = graphNode - .append("text") - .attr("dx", 0) - .attr("dy", (d) => -nodeRadius(d) + "px") - .attr("text-anchor", "middle") - .text((d) => d.text) - .style("opacity", (opacityScale - 1) / 3.75) - .style("pointer-events", "none") - .style("font-size", fontSize + "em") - .raise() - // @ts-ignore - .call(drag(simulation)) - - // set panning + .circle(0, 0, nodeRadius(n)) + .fill({ color: isTagNode ? computedStyleMap["--light"] : color(n) }) + .stroke({ width: isTagNode ? 2 : 0, color: color(n) }) + .on("pointerover", (e) => { + updateHoverInfo(e.target.label) + oldLabelOpacity = label.alpha + if (!dragging) { + renderPixiFromD3() + } + }) + .on("pointerleave", () => { + updateHoverInfo(null) + label.alpha = oldLabelOpacity + if (!dragging) { + renderPixiFromD3() + } + }) + + nodesContainer.addChild(gfx) + labelsContainer.addChild(label) + + const nodeRenderDatum: NodeRenderData = { + simulationData: n, + gfx, + label, + color: color(n), + alpha: 1, + active: false, + } + + nodeRenderData.push(nodeRenderDatum) + } + + for (const l of graphData.links) { + const gfx = new Graphics({ interactive: false, eventMode: "none" }) + linkContainer.addChild(gfx) + + const linkRenderDatum: LinkRenderData = { + simulationData: l, + gfx, + color: computedStyleMap["--lightgray"], + alpha: 1, + active: false, + } + + linkRenderData.push(linkRenderDatum) + } + + let currentTransform = zoomIdentity + if (enableDrag) { + select(app.canvas).call( + drag() + .container(() => app.canvas) + .subject(() => graphData.nodes.find((n) => n.id === hoveredNodeId)) + .on("start", function dragstarted(event) { + if (!event.active) simulation.alphaTarget(1).restart() + event.subject.fx = event.subject.x + event.subject.fy = event.subject.y + event.subject.__initialDragPos = { + x: event.subject.x, + y: event.subject.y, + fx: event.subject.fx, + fy: event.subject.fy, + } + dragStartTime = Date.now() + dragging = true + }) + .on("drag", function dragged(event) { + const initPos = event.subject.__initialDragPos + event.subject.fx = initPos.x + (event.x - initPos.x) / currentTransform.k + event.subject.fy = initPos.y + (event.y - initPos.y) / currentTransform.k + }) + .on("end", function dragended(event) { + if (!event.active) simulation.alphaTarget(0) + event.subject.fx = null + event.subject.fy = null + dragging = false + + // if the time between mousedown and mouseup is short, we consider it a click + if (Date.now() - dragStartTime < 500) { + const node = graphData.nodes.find((n) => n.id === event.subject.id) as NodeData + const targ = resolveRelative(fullSlug, node.id) + window.spaNavigate(new URL(targ, window.location.toString())) + } + }), + ) + } else { + for (const node of nodeRenderData) { + node.gfx.on("click", () => { + const targ = resolveRelative(fullSlug, node.simulationData.id) + window.spaNavigate(new URL(targ, window.location.toString())) + }) + } + } + if (enableZoom) { - svg.call( - d3 - .zoom() + select(app.canvas).call( + zoom() .extent([ [0, 0], [width, height], ]) .scaleExtent([0.25, 4]) .on("zoom", ({ transform }) => { - link.attr("transform", transform) - node.attr("transform", transform) + currentTransform = transform + stage.scale.set(transform.k, transform.k) + stage.position.set(transform.x, transform.y) + + // zoom adjusts opacity of labels too const scale = transform.k * opacityScale - const scaledOpacity = Math.max((scale - 1) / 3.75, 0) - labels.attr("transform", transform).style("opacity", scaledOpacity) + let scaleOpacity = Math.max((scale - 1) / 3.75, 0) + const activeNodes = nodeRenderData.filter((n) => n.active).flatMap((n) => n.label) + + for (const label of labelsContainer.children) { + if (!activeNodes.includes(label)) { + label.alpha = scaleOpacity + } + } }), ) } - // progress the simulation - simulation.on("tick", () => { - link - .attr("x1", (d: any) => d.source.x) - .attr("y1", (d: any) => d.source.y) - .attr("x2", (d: any) => d.target.x) - .attr("y2", (d: any) => d.target.y) - node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y) - labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y) - }) + function animate(time: number) { + for (const n of nodeRenderData) { + const { x, y } = n.simulationData + if (!x || !y) continue + n.gfx.position.set(x + width / 2, y + height / 2) + if (n.label) { + n.label.position.set(x + width / 2, y + height / 2) + } + } + + for (const l of linkRenderData) { + const linkData = l.simulationData + l.gfx.clear() + l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2) + l.gfx + .lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2) + .stroke({ alpha: l.alpha, width: 1, color: l.color }) + } + + tweens.forEach((t) => t.update(time)) + app.renderer.render(stage) + requestAnimationFrame(animate) + } + + const graphAnimationFrameHandle = requestAnimationFrame(animate) + window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle)) } -function renderGlobalGraph() { - const slug = getFullSlug(window) +document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { + const slug = e.detail.url + addToVisited(simplifySlug(slug)) + await renderGraph("graph-container", slug) + const container = document.getElementById("global-graph-outer") const sidebar = container?.closest(".sidebar") as HTMLElement - container?.classList.add("active") - if (sidebar) { - sidebar.style.zIndex = "1" - } - renderGraph("global-graph-container", slug) + function renderGlobalGraph() { + const slug = getFullSlug(window) + container?.classList.add("active") + if (sidebar) { + sidebar.style.zIndex = "1" + } + + renderGraph("global-graph-container", slug) + registerEscapeHandler(container, hideGlobalGraph) + } function hideGlobalGraph() { container?.classList.remove("active") - const graph = document.getElementById("global-graph-container") if (sidebar) { sidebar.style.zIndex = "unset" } - if (!graph) return - removeAllChildren(graph) } - registerEscapeHandler(container, hideGlobalGraph) -} - -document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { - const slug = e.detail.url - addToVisited(simplifySlug(slug)) - await renderGraph("graph-container", slug) + async function shortcutHandler(e: HTMLElementEventMap["keydown"]) { + if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) { + e.preventDefault() + const globalGraphOpen = container?.classList.contains("active") + globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph() + } + } const containerIcon = document.getElementById("global-graph-icon") containerIcon?.addEventListener("click", renderGlobalGraph) window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph)) -}) \ No newline at end of file + + document.addEventListener("keydown", shortcutHandler) + window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) +}) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index ba04535430d7b..e5e0cb603d8d5 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -4,8 +4,8 @@ import { normalizeRelativeURLs } from "../../util/path"; const p = new DOMParser(); async function mouseEnterHandler( - this: HTMLLinkElement, - { clientX, clientY }: { clientX: number; clientY: number }, + this: HTMLAnchorElement, + { clientX, clientY }: { clientX: number; clientY: number }, ) { const link = this; if (link.dataset.noPopover === "true") { @@ -30,13 +30,13 @@ async function mouseEnterHandler( 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 = ""; + const thisUrl = new URL(document.location.href) + thisUrl.hash = "" + thisUrl.search = "" + const targetUrl = new URL(link.href) + const hash = decodeURIComponent(targetUrl.hash) + targetUrl.hash = "" + targetUrl.search = "" const response = await fetch(`${targetUrl}`).catch((err) => { console.error(err); @@ -101,9 +101,9 @@ async function mouseEnterHandler( } document.addEventListener("nav", () => { - const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]; - for (const link of links) { - link.addEventListener("mouseenter", mouseEnterHandler); - window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler)); - } -}); + const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[] + for (const link of links) { + link.addEventListener("mouseenter", mouseEnterHandler) + window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler)) + } +}) diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index bb2f1a63a5317..e1d581a8559f8 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -145,14 +145,14 @@ function highlightHTML(searchTerm: string, el: HTMLElement) { } document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { - const currentSlug = e.detail.url; - const data = await fetchData; - const container = document.getElementById("search-container"); - const sidebar = container?.closest(".sidebar") as HTMLElement; - const searchIcon = document.getElementById("search-icon"); - const searchBar = document.getElementById("search-bar") as HTMLInputElement | null; - const searchLayout = document.getElementById("search-layout"); - const idDataMap = Object.keys(data) as FullSlug[]; + const currentSlug = e.detail.url + const data = await fetchData + const container = document.getElementById("search-container") + const sidebar = container?.closest(".sidebar") as HTMLElement + const searchButton = document.getElementById("search-button") + const searchBar = document.getElementById("search-bar") as HTMLInputElement | null + const searchLayout = document.getElementById("search-layout") + const idDataMap = Object.keys(data) as FullSlug[] const appendLayout = (el: HTMLElement) => { if (searchLayout?.querySelector(`#${el.id}`) === null) { @@ -191,8 +191,10 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { searchLayout.classList.remove("display-results"); } - searchType = "basic"; // reset search type after closing - } + searchType = "basic" // reset search type after closing + + searchButton?.focus() + } function showSearch(searchTypeNew: SearchType) { searchType = searchTypeNew; @@ -435,12 +437,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { await displayResults(finalResults); } - document.addEventListener("keydown", shortcutHandler); - window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)); - searchIcon?.addEventListener("click", () => showSearch("basic")); - window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic"))); - searchBar?.addEventListener("input", onType); - window.addCleanup(() => searchBar?.removeEventListener("input", onType)); + document.addEventListener("keydown", shortcutHandler) + window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) + searchButton?.addEventListener("click", () => showSearch("basic")) + window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic"))) + searchBar?.addEventListener("input", onType) + window.addCleanup(() => searchBar?.removeEventListener("input", onType)) registerEscapeHandler(container, hideSearch); await fillDocument(data); diff --git a/quartz/components/scripts/toc.inline.ts b/quartz/components/scripts/toc.inline.ts index 14e6ab9e3441c..5e5c9b793bf31 100644 --- a/quartz/components/scripts/toc.inline.ts +++ b/quartz/components/scripts/toc.inline.ts @@ -15,11 +15,15 @@ const observer = new IntersectionObserver((entries) => { }); function toggleToc(this: HTMLElement) { - this.classList.toggle("collapsed"); - const content = this.nextElementSibling as HTMLElement | undefined; - if (!content) return; - content.classList.toggle("collapsed"); - content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"; + this.classList.toggle("collapsed") + this.setAttribute( + "aria-expanded", + this.getAttribute("aria-expanded") === "true" ? "false" : "true", + ) + const content = this.nextElementSibling as HTMLElement | undefined + if (!content) return + content.classList.toggle("collapsed") + content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px" } function setupToc() { diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index bb3ddd0766259..5968b97ed81ca 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -4,12 +4,13 @@ import { GlobalConfiguration } from "../../cfg"; import { sluggify } from "../../util/path"; export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: () => void) { - if (!outsideContainer) return; - function click(this: HTMLElement, e: HTMLElementEventMap["click"]) { - if (e.target !== this) return; - e.preventDefault(); - cb(); - } + if (!outsideContainer) return + function click(this: HTMLElement, e: HTMLElementEventMap["click"]) { + if (e.target !== this) return + e.preventDefault() + e.stopPropagation() + cb() + } function esc(e: HTMLElementEventMap["keydown"]) { if (!e.key.startsWith("Esc")) return; diff --git a/quartz/components/styles/breadcrumbs.scss b/quartz/components/styles/breadcrumbs.scss index 789808baf68a4..3c97a00341ec6 100644 --- a/quartz/components/styles/breadcrumbs.scss +++ b/quartz/components/styles/breadcrumbs.scss @@ -9,14 +9,14 @@ } .breadcrumb-element { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; p { margin: 0; margin-left: 0.5rem; padding: 0; line-height: normal; } - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; } diff --git a/quartz/components/styles/darkmode.scss b/quartz/components/styles/darkmode.scss index 1e93aa6841ed6..f4d55b02d5191 100644 --- a/quartz/components/styles/darkmode.scss +++ b/quartz/components/styles/darkmode.scss @@ -1,17 +1,15 @@ .darkmode { + cursor: pointer; + padding: 0; position: relative; + background: none; + border: none; width: 20px; height: 20px; margin: 0 10px; - - & > .toggle { - display: none; - box-sizing: border-box; - } + text-align: inherit; & svg { - cursor: pointer; - opacity: 0; position: absolute; width: 20px; height: 20px; @@ -29,22 +27,22 @@ color-scheme: light; } -:root[saved-theme="dark"] .toggle ~ label { +:root[saved-theme="dark"] .darkmode { & > #dayIcon { - opacity: 0; + display: none; } & > #nightIcon { - opacity: 1; + display: inline; } } -:root .toggle ~ label { +:root .darkmode { & > #dayIcon { - opacity: 1; + display: inline; } & > #nightIcon { - opacity: 0; + display: none; } } diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss index dacf78425945d..79828ef50d451 100644 --- a/quartz/components/styles/explorer.scss +++ b/quartz/components/styles/explorer.scss @@ -1,7 +1,6 @@ @use "../../styles/variables.scss" as *; button#explorer { - all: unset; background-color: transparent; border: none; text-align: left; @@ -11,7 +10,7 @@ button#explorer { display: flex; align-items: center; - & h1 { + & h2 { font-size: 1rem; display: inline-block; margin: 0; @@ -46,11 +45,23 @@ button#explorer { list-style: none; overflow: hidden; max-height: none; + transition: + max-height 0.35s ease, + visibility 0s linear 0s; + margin-top: 0.5rem; + visibility: visible; :not(.explorer-burger-menu) & { transition: max-height 0.35s ease; margin-top: 0.5rem; } + &.collapsed { + transition: + max-height 0.35s ease, + visibility 0s linear 0.35s; + visibility: hidden; + } + &.collapsed > .overflow::after { opacity: 0; } diff --git a/quartz/components/styles/graph.scss b/quartz/components/styles/graph.scss index 3deaa1feb68a9..188907d1b969b 100644 --- a/quartz/components/styles/graph.scss +++ b/quartz/components/styles/graph.scss @@ -16,10 +16,13 @@ overflow: hidden; & > #global-graph-icon { + cursor: pointer; + background: none; + border: none; color: var(--dark); opacity: 0.5; - width: 18px; - height: 18px; + width: 24px; + height: 24px; position: absolute; padding: 0.2rem; margin: 0.3rem; @@ -59,8 +62,8 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - height: 60vh; - width: 50vw; + height: 80vh; + width: 80vw; @media all and (max-width: $fullPageWidth) { width: 90%; diff --git a/quartz/components/styles/listPage.scss b/quartz/components/styles/listPage.scss index f8a6e8ae52dc5..bbd21bbd3684d 100644 --- a/quartz/components/styles/listPage.scss +++ b/quartz/components/styles/listPage.scss @@ -32,7 +32,7 @@ li.section-li { background-color: transparent; } - & > .meta { + & .meta { margin: 0 1em 0 0; opacity: 0.6; } diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index b1694f97cb787..bd0d829036db8 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -64,12 +64,13 @@ font-size: 1.5rem; } - visibility: hidden; - opacity: 0; - transition: - opacity 0.3s ease, - visibility 0.3s ease; - + & { + visibility: hidden; + opacity: 0; + transition: + opacity 0.3s ease, + visibility 0.3s ease; + } @media all and (max-width: $mobileBreakpoint) { display: none !important; } diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 8a9ec6714ac36..cc2daca3beb62 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -5,18 +5,21 @@ max-width: 14rem; flex-grow: 0.3; - & > #search-icon { + & > .search-button { background-color: var(--lightgray); + border: none; border-radius: 4px; + font-family: inherit; + font-size: inherit; height: 2rem; + padding: 0; display: flex; align-items: center; + text-align: inherit; cursor: pointer; white-space: nowrap; - - & > div { - flex-grow: 1; - } + width: 100%; + justify-content: space-between; & > p { display: inline; diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss index 27ff62a4028cd..6845812f51642 100644 --- a/quartz/components/styles/toc.scss +++ b/quartz/components/styles/toc.scss @@ -29,8 +29,18 @@ button#toc { list-style: none; overflow: hidden; max-height: none; - transition: max-height 0.5s ease; + transition: + max-height 0.5s ease, + visibility 0s linear 0s; position: relative; + visibility: visible; + + &.collapsed { + transition: + max-height 0.5s ease, + visibility 0s linear 0.5s; + visibility: hidden; + } &.collapsed > .overflow::after { opacity: 0; diff --git a/quartz/i18n/index.ts b/quartz/i18n/index.ts index 2dc450c3e594a..9e434ca4828b2 100644 --- a/quartz/i18n/index.ts +++ b/quartz/i18n/index.ts @@ -1,5 +1,6 @@ import { Translation, CalloutTranslation } from "./locales/definition" -import en from "./locales/en-US" +import enUs from "./locales/en-US" +import enGb from "./locales/en-GB" import fr from "./locales/fr-FR" import it from "./locales/it-IT" import ja from "./locales/ja-JP" @@ -20,8 +21,8 @@ import fa from "./locales/fa-IR" import pl from "./locales/pl-PL" export const TRANSLATIONS = { - "en-US": en, - "en-GB": en, + "en-US": enUs, + "en-GB": enGb, "fr-FR": fr, "it-IT": it, "ja-JP": ja, diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx index f4938026e779f..2ac132147358a 100644 --- a/quartz/plugins/emitters/contentPage.tsx +++ b/quartz/plugins/emitters/contentPage.tsx @@ -59,14 +59,25 @@ export const ContentPage: QuartzEmitterPlugin> = (userOp ...userOpts, } - const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts + const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts const Header = HeaderConstructor() const Body = BodyConstructor() return { name: "ContentPage", getQuartzComponents() { - return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] + return [ + Head, + Header, + Body, + ...header, + ...beforeBody, + pageBody, + ...afterBody, + ...left, + ...right, + Footer, + ] }, async getDependencyGraph(ctx, content, _resources) { const graph = new DepGraph() diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx index d892b282a1c7b..7eebb21c7e9b7 100644 --- a/quartz/plugins/emitters/folderPage.tsx +++ b/quartz/plugins/emitters/folderPage.tsx @@ -3,7 +3,7 @@ import { QuartzComponentProps } from "../../components/types" import HeaderConstructor from "../../components/Header" import BodyConstructor from "../../components/Body" import { pageResources, renderPage } from "../../components/renderPage" -import { ProcessedContent, defaultProcessedContent } from "../vfile" +import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile" import { FullPageLayout } from "../../cfg" import path from "path" import { @@ -21,22 +21,37 @@ import { write } from "./helpers" import { i18n } from "../../i18n" import DepGraph from "../../depgraph" -export const FolderPage: QuartzEmitterPlugin> = (userOpts) => { +interface FolderPageOptions extends FullPageLayout { + sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number +} + +export const FolderPage: QuartzEmitterPlugin> = (userOpts) => { const opts: FullPageLayout = { ...sharedPageComponents, ...defaultListPageLayout, - pageBody: FolderContent(), + pageBody: FolderContent({ sort: userOpts?.sort }), ...userOpts, } - const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts + const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts const Header = HeaderConstructor() const Body = BodyConstructor() return { name: "FolderPage", getQuartzComponents() { - return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] + return [ + Head, + Header, + Body, + ...header, + ...beforeBody, + pageBody, + ...afterBody, + ...left, + ...right, + Footer, + ] }, async getDependencyGraph(_ctx, content, _resources) { // Example graph: diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx index d88d0722aab9d..066d4ec2641bd 100644 --- a/quartz/plugins/emitters/tagPage.tsx +++ b/quartz/plugins/emitters/tagPage.tsx @@ -3,7 +3,7 @@ import { QuartzComponentProps } from "../../components/types" import HeaderConstructor from "../../components/Header" import BodyConstructor from "../../components/Body" import { pageResources, renderPage } from "../../components/renderPage" -import { ProcessedContent, defaultProcessedContent } from "../vfile" +import { ProcessedContent, QuartzPluginData, defaultProcessedContent } from "../vfile" import { FullPageLayout } from "../../cfg" import { FilePath, @@ -18,22 +18,37 @@ import { write } from "./helpers" import { i18n } from "../../i18n" import DepGraph from "../../depgraph" -export const TagPage: QuartzEmitterPlugin> = (userOpts) => { +interface TagPageOptions extends FullPageLayout { + sort?: (f1: QuartzPluginData, f2: QuartzPluginData) => number +} + +export const TagPage: QuartzEmitterPlugin> = (userOpts) => { const opts: FullPageLayout = { ...sharedPageComponents, ...defaultListPageLayout, - pageBody: TagContent(), + pageBody: TagContent({ sort: userOpts?.sort }), ...userOpts, } - const { head: Head, header, beforeBody, pageBody, left, right, footer: Footer } = opts + const { head: Head, header, beforeBody, pageBody, afterBody, left, right, footer: Footer } = opts const Header = HeaderConstructor() const Body = BodyConstructor() return { name: "TagPage", getQuartzComponents() { - return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer] + return [ + Head, + Header, + Body, + ...header, + ...beforeBody, + pageBody, + ...afterBody, + ...left, + ...right, + Footer, + ] }, async getDependencyGraph(ctx, content, _resources) { const graph = new DepGraph() diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts index f246da4782d8b..ec82d0e33585c 100644 --- a/quartz/plugins/index.ts +++ b/quartz/plugins/index.ts @@ -24,16 +24,16 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) { ? `wss://${ctx.argv.remoteDevHost}:${ctx.argv.wsPort}` : `ws://localhost:${ctx.argv.wsPort}`; - staticResources.js.push({ - loadTime: "afterDOMReady", - contentType: "inline", - script: ` - const socket = new WebSocket('${wsUrl}') - // reload(true) ensures resources like images and scripts are fetched again in firefox - socket.addEventListener('message', () => document.location.reload(true)) - `, - }); - } + staticResources.js.push({ + loadTime: "afterDOMReady", + contentType: "inline", + script: ` + const socket = new WebSocket('${wsUrl}') + // reload(true) ensures resources like images and scripts are fetched again in firefox + socket.addEventListener('message', () => document.location.reload(true)) + `, + }) + } return staticResources; } diff --git a/quartz/plugins/transformers/citations.ts b/quartz/plugins/transformers/citations.ts index bb302e437db82..5242f37c5ccba 100644 --- a/quartz/plugins/transformers/citations.ts +++ b/quartz/plugins/transformers/citations.ts @@ -17,7 +17,7 @@ const defaultOptions: Options = { csl: "apa", } -export const Citations: QuartzTransformerPlugin | undefined> = (userOpts) => { +export const Citations: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } return { name: "Citations", @@ -38,7 +38,7 @@ export const Citations: QuartzTransformerPlugin | undefined> = // using https://github.com/syntax-tree/unist-util-visit as they're just anochor links plugins.push(() => { return (tree, _file) => { - visit(tree, "element", (node, index, parent) => { + visit(tree, "element", (node, _index, _parent) => { if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) { node.properties["data-no-popover"] = true } diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts index e02c1028b564f..4574fdd5ed193 100644 --- a/quartz/plugins/transformers/description.ts +++ b/quartz/plugins/transformers/description.ts @@ -19,16 +19,16 @@ const urlRegex = new RegExp( "g", ); -export const Description: QuartzTransformerPlugin | undefined> = (userOpts) => { - const opts = { ...defaultOptions, ...userOpts }; - return { - name: "Description", - htmlPlugins() { - return [ - () => { - return async (tree: HTMLRoot, file) => { - let frontMatterDescription = file.data.frontmatter?.description; - let text = escapeHTML(toString(tree)); +export const Description: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "Description", + htmlPlugins() { + return [ + () => { + return async (tree: HTMLRoot, file) => { + let frontMatterDescription = file.data.frontmatter?.description + let text = escapeHTML(toString(tree)) if (opts.replaceExternalLinks) { frontMatterDescription = frontMatterDescription?.replace( diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts index dcf45e1e1f190..a5d1bbd9941de 100644 --- a/quartz/plugins/transformers/frontmatter.ts +++ b/quartz/plugins/transformers/frontmatter.ts @@ -41,22 +41,22 @@ function coerceToArray(input: string | string[]): string[] | undefined { .map((tag: string | number) => tag.toString()); } -export const FrontMatter: QuartzTransformerPlugin | undefined> = (userOpts) => { - const opts = { ...defaultOptions, ...userOpts }; - return { - name: "FrontMatter", - markdownPlugins({ cfg }) { - return [ - [remarkFrontmatter, ["yaml", "toml"]], - () => { - return (_, file) => { - const { data } = matter(Buffer.from(file.value), { - ...opts, - engines: { - yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object, - toml: (s) => toml.parse(s) as object, - }, - }); +export const FrontMatter: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "FrontMatter", + markdownPlugins({ cfg }) { + return [ + [remarkFrontmatter, ["yaml", "toml"]], + () => { + return (_, file) => { + const { data } = matter(Buffer.from(file.value), { + ...opts, + engines: { + yaml: (s) => yaml.load(s, { schema: yaml.JSON_SCHEMA }) as object, + toml: (s) => toml.parse(s) as object, + }, + }) if (data.title != null && data.title.toString() !== "") { data.title = data.title.toString(); diff --git a/quartz/plugins/transformers/gfm.ts b/quartz/plugins/transformers/gfm.ts index bd5ec597d7837..38cccb582e737 100644 --- a/quartz/plugins/transformers/gfm.ts +++ b/quartz/plugins/transformers/gfm.ts @@ -15,67 +15,65 @@ const defaultOptions: Options = { linkHeadings: true, }; -export const GitHubFlavoredMarkdown: QuartzTransformerPlugin | undefined> = ( - userOpts, -) => { - const opts = { ...defaultOptions, ...userOpts }; - return { - name: "GitHubFlavoredMarkdown", - markdownPlugins() { - return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm]; - }, - htmlPlugins() { - if (opts.linkHeadings) { - return [ - rehypeSlug, - [ - rehypeAutolinkHeadings, - { - behavior: "append", - properties: { - role: "anchor", - ariaHidden: true, - tabIndex: -1, - "data-no-popover": true, - }, - content: { - type: "element", - tagName: "svg", - properties: { - width: 18, - height: 18, - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - }, - children: [ - { - type: "element", - tagName: "path", - properties: { - d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71", - }, - children: [], - }, - { - type: "element", - tagName: "path", - properties: { - d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71", - }, - children: [], - }, - ], - }, - }, - ], - ]; - } else { - return []; - } - }, - }; -}; +export const GitHubFlavoredMarkdown: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "GitHubFlavoredMarkdown", + markdownPlugins() { + return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm] + }, + htmlPlugins() { + if (opts.linkHeadings) { + return [ + rehypeSlug, + [ + rehypeAutolinkHeadings, + { + behavior: "append", + properties: { + role: "anchor", + ariaHidden: true, + tabIndex: -1, + "data-no-popover": true, + }, + content: { + type: "element", + tagName: "svg", + properties: { + width: 18, + height: 18, + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + }, + children: [ + { + type: "element", + tagName: "path", + properties: { + d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71", + }, + children: [], + }, + { + type: "element", + tagName: "path", + properties: { + d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71", + }, + children: [], + }, + ], + }, + }, + ], + ] + } else { + return [] + } + }, + } +} diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts index c760cef0a0de0..22f78ad7cf220 100644 --- a/quartz/plugins/transformers/lastmod.ts +++ b/quartz/plugins/transformers/lastmod.ts @@ -28,20 +28,18 @@ function coerceDate(fp: string, d: any): Date { } type MaybeDate = undefined | string | number -export const CreatedModifiedDate: QuartzTransformerPlugin | undefined> = ( - userOpts, -) => { - const opts = { ...defaultOptions, ...userOpts }; - return { - name: "CreatedModifiedDate", - markdownPlugins() { - return [ - () => { - let repo: Repository | undefined = undefined; - return async (_tree, file) => { - let created: MaybeDate = undefined; - let modified: MaybeDate = undefined; - let published: MaybeDate = undefined; +export const CreatedModifiedDate: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "CreatedModifiedDate", + markdownPlugins() { + return [ + () => { + let repo: Repository | undefined = undefined + return async (_tree, file) => { + let created: MaybeDate = undefined + let modified: MaybeDate = undefined + let published: MaybeDate = undefined const fp = file.data.filePath!; const fullFp = path.isAbsolute(fp) ? fp : path.posix.join(file.cwd, fp); diff --git a/quartz/plugins/transformers/latex.ts b/quartz/plugins/transformers/latex.ts index 697452c12ad4e..475724834592c 100644 --- a/quartz/plugins/transformers/latex.ts +++ b/quartz/plugins/transformers/latex.ts @@ -6,41 +6,47 @@ import { QuartzTransformerPlugin } from "../types"; interface Options { renderEngine: "katex" | "mathjax" + customMacros: MacroType } -export const Latex: QuartzTransformerPlugin = (opts?: Options) => { - const engine = opts?.renderEngine ?? "katex"; - return { - name: "Latex", - markdownPlugins() { - return [remarkMath]; - }, - htmlPlugins() { - if (engine === "katex") { - return [[rehypeKatex, { output: "html" }]]; - } else { - return [rehypeMathjax]; - } - }, - externalResources() { - if (engine === "katex") { - return { - css: [ - // base css - "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css", - ], - js: [ - { - // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md - src: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.js", - loadTime: "afterDOMReady", - contentType: "external", - }, - ], - }; - } else { - return {}; - } - }, - }; -}; +interface MacroType { + [key: string]: string +} + +export const Latex: QuartzTransformerPlugin> = (opts) => { + const engine = opts?.renderEngine ?? "katex" + const macros = opts?.customMacros ?? {} + return { + name: "Latex", + markdownPlugins() { + return [remarkMath] + }, + htmlPlugins() { + if (engine === "katex") { + return [[rehypeKatex, { output: "html", macros }]] + } else { + return [[rehypeMathjax, { macros }]] + } + }, + externalResources() { + if (engine === "katex") { + return { + css: [ + // base css + "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css", + ], + js: [ + { + // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md + src: "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/copy-tex.min.js", + loadTime: "afterDOMReady", + contentType: "external", + }, + ], + } + } else { + return {} + } + }, + } +} diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts index ca9f04bafa774..52d5bf5b19b1d 100644 --- a/quartz/plugins/transformers/links.ts +++ b/quartz/plugins/transformers/links.ts @@ -33,7 +33,7 @@ const defaultOptions: Options = { externalLinkIcon: true, } -export const CrawlLinks: QuartzTransformerPlugin | undefined> = (userOpts) => { +export const CrawlLinks: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } return { name: 'LinkProcessing', @@ -66,8 +66,9 @@ export const CrawlLinks: QuartzTransformerPlugin | undefined> = type: 'element', tagName: 'svg', properties: { - class: 'external-icon', - viewBox: '0 0 512 512', + "aria-hidden": "true", + class: "external-icon", + viewBox: "0 0 512 512", }, children: [ { diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 51d2e654f778d..4100f69791502 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -2,7 +2,6 @@ import { QuartzTransformerPlugin } from "../types" import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast" import { Element, Literal, Root as HtmlRoot } from "hast" import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace" -import { slug as slugAnchor } from "github-slugger" import rehypeRaw from "rehype-raw" import { SKIP, visit } from "unist-util-visit" import path from "path" @@ -98,7 +97,7 @@ function canonicalizeCallout(calloutName: string): keyof typeof calloutMapping { export const externalLinkRegex = /^https?:\/\//i -export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g") +export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/g) // !? -> optional embedding // \[\[ -> open brace @@ -106,35 +105,30 @@ export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g") // (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) // (\\?\|[^\[\]\#]+)? -> optional escape \ then | then one or more non-special characters (alias) export const wikilinkRegex = new RegExp( - /!?\[\[([^\[\]\|\#\\]+)?(#+[^\[\]\|\#\\]+)?(\\?\|[^\[\]\#]+)?\]\]/, - "g", + /!?\[\[([^\[\]\|\#\\]+)?(#+[^\[\]\|\#\\]+)?(\\?\|[^\[\]\#]+)?\]\]/g, ) // ^\|([^\n])+\|\n(\|) -> matches the header row // ( ?:?-{3,}:? ?\|)+ -> matches the header row separator // (\|([^\n])+\|\n)+ -> matches the body rows -export const tableRegex = new RegExp( - /^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/, - "gm", -) +export const tableRegex = new RegExp(/^\|([^\n])+\|\n(\|)( ?:?-{3,}:? ?\|)+\n(\|([^\n])+\|\n?)+/gm) // matches any wikilink, only used for escaping wikilinks inside tables -export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/, "g") +export const tableWikilinkRegex = new RegExp(/(!?\[\[[^\]]*?\]\])/g) -const highlightRegex = new RegExp(/==([^=]+)==/, "g") -const commentRegex = new RegExp(/%%[\s\S]*?%%/, "g") +const highlightRegex = new RegExp(/==([^=]+)==/g) +const commentRegex = new RegExp(/%%[\s\S]*?%%/g) // from https://github.com/escwxyz/remark-obsidian-callout/blob/main/src/index.ts const calloutRegex = new RegExp(/^\[\!(\w+)\|?(.+?)?\]([+-]?)/) -const calloutLineRegex = new RegExp(/^> *\[\!\w+\|?.*?\][+-]?.*$/, "gm") +const calloutLineRegex = new RegExp(/^> *\[\!\w+\|?.*?\][+-]?.*$/gm) // (?:^| ) -> non-capturing group, tag should start be separated by a space or be the start of the line // #(...) -> capturing group, tag itself must start with # // (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores // (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/" const tagRegex = new RegExp( - /(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/, - "gu", + /(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/gu, ) -const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g") +const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/g) const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ const ytPlaylistLinkRegex = /[?&]list=([^#?&]*)/ const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/) @@ -142,9 +136,7 @@ const wikilinkImageEmbedRegex = new RegExp( /^(?(?!^\d*x?\d*$).*?)?(\|?\s*?(?\d+)(x(?\d+))?)?$/, ) -export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin | undefined> = ( - userOpts, -) => { +export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin> = (userOpts) => { const opts = { ...defaultOptions, ...userOpts } const mdastToHtml = (ast: PhrasingContent | Paragraph) => { @@ -185,8 +177,8 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin // replace all wikilinks inside a table first src = src.replace(tableRegex, (value) => { // escape all aliases and headers in wikilinks inside a table - return value.replace(tableWikilinkRegex, (value, ...capture) => { - const [raw]: (string | undefined)[] = capture + return value.replace(tableWikilinkRegex, (_value, raw) => { + // const [raw]: (string | undefined)[] = capture let escaped = raw ?? "" escaped = escaped.replace("#", "\\#") // escape pipe characters if they are not already escaped @@ -201,7 +193,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin const [rawFp, rawHeader, rawAlias]: (string | undefined)[] = capture const [fp, anchor] = splitAnchor(`${rawFp ?? ""}${rawHeader ?? ""}`) - const blockRef = Boolean(anchor?.startsWith("^")) ? "^" : "" + const blockRef = Boolean(rawHeader?.match(/^#?\^/)) ? "^" : "" const displayAnchor = anchor ? `#${blockRef}${anchor.trim().replace(/^#+/, "")}` : "" const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? "" const embedDisplay = value.startsWith("!") ? "!" : "" @@ -269,14 +261,14 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin } else if ([".pdf"].includes(ext)) { return { type: "html", - value: ``, + value: ``, } } else { const block = anchor return { type: "html", data: { hProperties: { transclude: true } }, - value: `
Transclude of ${url}${block}
`, } @@ -622,11 +614,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin // YouTube video (with optional playlist) node.tagName = "iframe" node.properties = { - class: "external-embed", + class: "external-embed youtube", allow: "fullscreen", frameborder: 0, width: "600px", - height: "350px", src: playlistId ? `https://www.youtube.com/embed/${videoId}?list=${playlistId}` : `https://www.youtube.com/embed/${videoId}`, @@ -635,11 +626,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin // YouTube playlist only. node.tagName = "iframe" node.properties = { - class: "external-embed", + class: "external-embed youtube", allow: "fullscreen", frameborder: 0, width: "600px", - height: "350px", src: `https://www.youtube.com/embed/videoseries?list=${playlistId}`, } } diff --git a/quartz/plugins/transformers/oxhugofm.ts b/quartz/plugins/transformers/oxhugofm.ts index 99cf95cd27bab..ce240d7db3f86 100644 --- a/quartz/plugins/transformers/oxhugofm.ts +++ b/quartz/plugins/transformers/oxhugofm.ts @@ -47,20 +47,18 @@ const quartzLatexRegex = new RegExp(/\$\$[\s\S]*?\$\$|\$.*?\$/, "g"); * markdown to make it compatible with quartz but the list of changes applied it * is not exhaustive. * */ -export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin | undefined> = ( - userOpts, -) => { - const opts = { ...defaultOptions, ...userOpts }; - return { - name: "OxHugoFlavouredMarkdown", - textTransform(_ctx, src) { - if (opts.wikilinks) { - src = src.toString(); - src = src.replaceAll(relrefRegex, (value, ...capture) => { - const [text, link] = capture; - return `[${text}](${link})`; - }); - } +export const OxHugoFlavouredMarkdown: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "OxHugoFlavouredMarkdown", + textTransform(_ctx, src) { + if (opts.wikilinks) { + src = src.toString() + src = src.replaceAll(relrefRegex, (value, ...capture) => { + const [text, link] = capture + return `[${text}](${link})` + }) + } if (opts.removePredefinedAnchor) { src = src.toString(); diff --git a/quartz/plugins/transformers/syntax.ts b/quartz/plugins/transformers/syntax.ts index bcfdf10e377d8..9fcbbb82e438c 100644 --- a/quartz/plugins/transformers/syntax.ts +++ b/quartz/plugins/transformers/syntax.ts @@ -20,10 +20,8 @@ const defaultOptions: Options = { keepBackground: false, }; -export const SyntaxHighlighting: QuartzTransformerPlugin = ( - userOpts?: Partial, -) => { - const opts: Partial = { ...defaultOptions, ...userOpts }; +export const SyntaxHighlighting: QuartzTransformerPlugin> = (userOpts) => { + const opts: CodeOptions = { ...defaultOptions, ...userOpts } return { name: "SyntaxHighlighting", diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts index c5ac549815b31..392bcefbbc98e 100644 --- a/quartz/plugins/transformers/toc.ts +++ b/quartz/plugins/transformers/toc.ts @@ -25,33 +25,31 @@ interface TocEntry { slug: string // this is just the anchor (#some-slug), not the canonical slug } -const slugAnchor = new Slugger(); -export const TableOfContents: QuartzTransformerPlugin | undefined> = ( - userOpts, -) => { - const opts = { ...defaultOptions, ...userOpts }; - return { - name: "TableOfContents", - markdownPlugins() { - return [ - () => { - return async (tree: Root, file) => { - const display = file.data.frontmatter?.enableToc ?? opts.showByDefault; - if (display) { - slugAnchor.reset(); - const toc: TocEntry[] = []; - let highestDepth: number = opts.maxDepth; - visit(tree, "heading", (node) => { - if (node.depth <= opts.maxDepth) { - const text = toString(node); - highestDepth = Math.min(highestDepth, node.depth); - toc.push({ - depth: node.depth, - text, - slug: slugAnchor.slug(text), - }); - } - }); +const slugAnchor = new Slugger() +export const TableOfContents: QuartzTransformerPlugin> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "TableOfContents", + markdownPlugins() { + return [ + () => { + return async (tree: Root, file) => { + const display = file.data.frontmatter?.enableToc ?? opts.showByDefault + if (display) { + slugAnchor.reset() + const toc: TocEntry[] = [] + let highestDepth: number = opts.maxDepth + visit(tree, "heading", (node) => { + if (node.depth <= opts.maxDepth) { + const text = toString(node) + highestDepth = Math.min(highestDepth, node.depth) + toc.push({ + depth: node.depth, + text, + slug: slugAnchor.slug(text), + }) + } + }) if (toc.length > 0 && toc.length > opts.minEntries) { file.data.toc = toc.map((entry) => ({ diff --git a/quartz/processors/parse.ts b/quartz/processors/parse.ts index 5f136eb01dfec..5b389ac23c177 100644 --- a/quartz/processors/parse.ts +++ b/quartz/processors/parse.ts @@ -142,10 +142,10 @@ export async function parseMarkdown(ctx: BuildCtx, fps: FilePath[]): Promise[] = []; - for (const chunk of chunks(fps, CHUNK_SIZE)) { - childPromises.push(pool.exec("parseFiles", [argv, chunk, ctx.allSlugs])); - } + const childPromises: WorkerPromise[] = [] + for (const chunk of chunks(fps, CHUNK_SIZE)) { + childPromises.push(pool.exec("parseFiles", [ctx.buildId, argv, chunk, ctx.allSlugs])) + } const results: ProcessedContent[][] = await WorkerPromise.all(childPromises).catch((err) => { const errString = err.toString().slice("Error:".length); diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index d09729e39eacf..3f822baa2638d 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -20,11 +20,10 @@ section { } .text-highlight { - background-color: #fff23688; + background-color: var(--textHighlight); padding: 0 0.1rem; border-radius: 5px; } - ::selection { background: color-mix(in srgb, var(--tertiary) 60%, rgba(255, 255, 255, 0)); color: var(--darkgray); @@ -209,11 +208,19 @@ a { } } - & .page-header { + & .page-header, + & .page-footer { width: $pageWidth; - margin: $topSpacing auto 0 auto; + margin-top: 1rem; + @media all and (max-width: $fullPageWidth) { width: initial; + } + } + + & .page-header { + margin: $topSpacing auto 0 auto; + @media all and (max-width: $fullPageWidth) { margin-top: 2rem; } } @@ -541,3 +548,11 @@ ol.overflow { overflow-x: auto; overflow-y: hidden; } + +.external-embed.youtube, +iframe.pdf { + aspect-ratio: 16 / 9; + height: 100%; + width: 100%; + border-radius: 5px; +} diff --git a/quartz/styles/callouts.scss b/quartz/styles/callouts.scss index 159e32d1ef86a..609578ee7d19b 100644 --- a/quartz/styles/callouts.scss +++ b/quartz/styles/callouts.scss @@ -10,14 +10,6 @@ transition: max-height 0.3s ease; box-sizing: border-box; - &.spoiler { - display: none; - } - - & > .callout-content > :first-child { - margin-top: 0; - } - --callout-icon-note: url('data:image/svg+xml; utf8, '); --callout-icon-abstract: url('data:image/svg+xml; utf8, '); --callout-icon-info: url('data:image/svg+xml; utf8, '); @@ -32,7 +24,13 @@ --callout-icon-example: url('data:image/svg+xml; utf8, '); --callout-icon-quote: url('data:image/svg+xml; utf8, '); --callout-icon-fold: url('data:image/svg+xml,%3Csvg 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"%3E%3Cpolyline points="6 9 12 15 18 9"%3E%3C/polyline%3E%3C/svg%3E'); + &.spoiler { + display: none; + } + & > .callout-content > :first-child { + margin-top: 0; + } &[data-callout] { --color: #448aff; --border: #448aff44; diff --git a/quartz/styles/custom/callouts.scss b/quartz/styles/custom/callouts.scss index b127fe77fdd00..70b5857cfcbff 100644 --- a/quartz/styles/custom/callouts.scss +++ b/quartz/styles/custom/callouts.scss @@ -58,11 +58,11 @@ --callout-icon: "" --color: transparent; --border: transparent; --bg: transparent; + width: 50px; + padding: 5px; & .callout-title { display: none; } - width: 50px; - padding: 5px; } &[data-callout="dice"] { diff --git a/quartz/styles/custom/grid_callouts.scss b/quartz/styles/custom/grid_callouts.scss index e04343bbd9faf..a9eaa15c4f25d 100644 --- a/quartz/styles/custom/grid_callouts.scss +++ b/quartz/styles/custom/grid_callouts.scss @@ -371,15 +371,14 @@ body { } table { - th { - background-color: var(--aside-bg, rgba(var(--callout-color), 0.5)); - } - --tbl-td-h: 0; --tbl-td-w: 5px; white-space: nowrap; margin: 0; width: 100%; + th { + background-color: var(--aside-bg, rgba(var(--callout-color), 0.5)); + } } p:last-child { @@ -400,7 +399,9 @@ body { border: 0; text-align: center; padding: 0; - + padding: 0px; + margin: auto; + overflow-y: hidden; &.is-collapsible:not(.is-collapsed) { display: flex; flex-direction: row-reverse; @@ -424,10 +425,6 @@ body { } } - padding: 0px; - margin: auto; - overflow-y: hidden; - &:is([data-callout-metadata~="tbl-cln"], [data-callout-metadata~="table-clean"]) { table :is(td, tr, th) { background-color: transparent; diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts index 2beae5b631b89..6fd5fb60d2286 100644 --- a/quartz/util/ctx.ts +++ b/quartz/util/ctx.ts @@ -14,6 +14,7 @@ export interface Argv { } export interface BuildCtx { + buildId: string argv: Argv cfg: QuartzConfig allSlugs: FullSlug[] diff --git a/quartz/util/theme.ts b/quartz/util/theme.ts index 5355559aadae7..a31d02e30d332 100644 --- a/quartz/util/theme.ts +++ b/quartz/util/theme.ts @@ -7,6 +7,7 @@ export interface ColorScheme { secondary: string tertiary: string highlight: string + textHighlight: string } interface Colors { @@ -49,6 +50,7 @@ ${stylesheet.join("\n\n")} --secondary: ${theme.colors.lightMode.secondary}; --tertiary: ${theme.colors.lightMode.tertiary}; --highlight: ${theme.colors.lightMode.highlight}; + --textHighlight: ${theme.colors.lightMode.textHighlight}; --headerFont: "${theme.typography.header}", ${DEFAULT_SANS_SERIF}; --bodyFont: "${theme.typography.body}", ${DEFAULT_SANS_SERIF}; @@ -64,6 +66,7 @@ ${stylesheet.join("\n\n")} --secondary: ${theme.colors.darkMode.secondary}; --tertiary: ${theme.colors.darkMode.tertiary}; --highlight: ${theme.colors.darkMode.highlight}; + --textHighlight: ${theme.colors.darkMode.textHighlight}; } `; } diff --git a/quartz/worker.ts b/quartz/worker.ts index db62c319dffcd..bad2798109bba 100644 --- a/quartz/worker.ts +++ b/quartz/worker.ts @@ -9,13 +9,19 @@ import { options } from "./util/sourcemap"; sourceMapSupport.install(options); // only called from worker thread -export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: FullSlug[]) { - const ctx: BuildCtx = { - cfg, - argv, - allSlugs, - }; - const processor = createProcessor(ctx); - const parse = createFileParser(ctx, fps); - return parse(processor); +export async function parseFiles( + buildId: string, + argv: Argv, + fps: FilePath[], + allSlugs: FullSlug[], +) { + const ctx: BuildCtx = { + buildId, + cfg, + argv, + allSlugs, + } + const processor = createProcessor(ctx) + const parse = createFileParser(ctx, fps) + return parse(processor) }