diff --git a/quartz/build.ts b/quartz/build.ts index 0c0aba5e3bab2..7e6536c232d59 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -311,6 +311,8 @@ async function partialRebuildFromEntrypoint( } await rimraf([...destinationsToDelete]); + console.log(chalk.green(`Done rebuilding in ${perf.timeSince()}`)); + toRemove.clear(); release(); clientRefresh(); diff --git a/quartz/components/ContentMeta.tsx b/quartz/components/ContentMeta.tsx index bcbe4285de50c..5dfec1448afac 100644 --- a/quartz/components/ContentMeta.tsx +++ b/quartz/components/ContentMeta.tsx @@ -3,16 +3,20 @@ import { QuartzComponentConstructor, QuartzComponentProps } from "./types" import readingTime from "reading-time" import { classNames } from "../util/lang" import { i18n } from "../i18n" +import { JSX } from "preact" +import style from "./styles/contentMeta.scss" interface ContentMetaOptions { /** * Whether to display reading time */ showReadingTime: boolean + showComma: boolean } const defaultOptions: ContentMetaOptions = { showReadingTime: true, + showComma: true, } export default ((opts?: Partial) => { @@ -23,7 +27,7 @@ export default ((opts?: Partial) => { const text = fileData.text if (text) { - const segments: string[] = [] + const segments: (string | JSX.Element)[] = [] if (fileData.dates) { segments.push(formatDate(getDate(cfg, fileData)!, cfg.locale)) @@ -38,17 +42,19 @@ export default ((opts?: Partial) => { segments.push(displayedTime) } - return

{segments.join(", ")}

+ const segmentsElements = segments.map((segment) => {segment}) + + return ( +

+ {segmentsElements} +

+ ) } else { return null } } - ContentMetadata.css = ` - .content-meta { - margin-top: 0; - color: var(--gray); - } - ` + ContentMetadata.css = style + return ContentMetadata }) satisfies QuartzComponentConstructor diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index 49f6f290252fc..924c566a0e9c4 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -1,6 +1,7 @@ import { i18n } from "../i18n"; -import { FullSlug, joinSegments, pathToRoot, sluggify } from "../util/path"; +import { FullSlug, joinSegments, pathToRoot } from "../util/path"; import { JSResourceToScriptElement } from "../util/resources"; +import { googleFontHref } from "../util/theme"; import { getMetaImage } from "./scripts/util"; import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"; @@ -21,10 +22,11 @@ export default (() => { {title} - {cfg.theme.cdnCaching && ( + {cfg.theme.cdnCaching && cfg.theme.fontOrigin === "googleFonts" && ( <> + )} diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx index 0c18544483ea5..251a53f2ed1b1 100644 --- a/quartz/components/renderPage.tsx +++ b/quartz/components/renderPage.tsx @@ -118,11 +118,12 @@ export function renderPage( // skip until we find the blockref that matches if (el.properties?.id === blockRef) { startIdx = i - startDepth = Number(el.tagName.substring(1)) + startDepth = depth } } else if (depth <= startDepth) { // looking for new header that is same level or higher endIdx = i + break } } diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index 18b65dc633042..bb3ddd0766259 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -1,4 +1,5 @@ import { Data } from "vfile"; + import { GlobalConfiguration } from "../../cfg"; import { sluggify } from "../../util/path"; diff --git a/quartz/components/styles/contentMeta.scss b/quartz/components/styles/contentMeta.scss new file mode 100644 index 0000000000000..4d89f65d565d2 --- /dev/null +++ b/quartz/components/styles/contentMeta.scss @@ -0,0 +1,14 @@ +.content-meta { + margin-top: 0; + color: var(--gray); + + &[show-comma="true"] { + > span:not(:last-child) { + margin-right: 8px; + + &::after { + content: ","; + } + } + } +} diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index 6abc881db146f..32acd730232f4 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -11,7 +11,6 @@ import DepGraph from "../../depgraph"; import styles from "../../styles/custom.scss"; import { BuildCtx } from "../../util/ctx"; import { FilePath, FullSlug, joinSegments } from "../../util/path"; -import { StaticResources } from "../../util/resources"; import { googleFontHref, joinStyles } from "../../util/theme"; import { QuartzEmitterPlugin } from "../types"; import { write } from "./helpers"; @@ -69,13 +68,8 @@ async function joinScripts(scripts: string[]): Promise { return res.code; } -function addGlobalPageResources( - ctx: BuildCtx, - staticResources: StaticResources, - componentResources: ComponentResources, -) { +function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentResources) { const cfg = ctx.cfg.configuration; - const reloadScript = ctx.argv.serve; // popovers if (cfg.enablePopovers) { @@ -85,12 +79,12 @@ function addGlobalPageResources( if (cfg.analytics?.provider === "google") { const tagId = cfg.analytics.tagId; - staticResources.js.push({ - src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`, - contentType: "external", - loadTime: "afterDOMReady", - }); componentResources.afterDOMLoaded.push(` + const gtagScript = document.createElement("script") + gtagScript.src = "https://www.googletagmanager.com/gtag/js?id=${tagId}" + gtagScript.async = true + document.head.appendChild(gtagScript) + window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag("js", new Date()); @@ -147,115 +141,72 @@ function addGlobalPageResources( document.dispatchEvent(event) `); } - - let wsUrl = `ws://localhost:${ctx.argv.wsPort}`; - - if (ctx.argv.remoteDevHost) { - wsUrl = `wss://${ctx.argv.remoteDevHost}:${ctx.argv.wsPort}`; - } - - if (reloadScript) { - 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)) - `, - }); - } -} - -interface Options { - fontOrigin: "googleFonts" | "local" } -const defaultOptions: Options = { - fontOrigin: "googleFonts", -}; - -export const ComponentResources: QuartzEmitterPlugin = (opts?: Partial) => { - const { fontOrigin } = { ...defaultOptions, ...opts }; +// This emitter should not update the `resources` parameter. If it does, partial +// rebuilds may not work as expected. +export const ComponentResources: QuartzEmitterPlugin = () => { return { name: "ComponentResources", getQuartzComponents() { return []; }, - async getDependencyGraph(ctx, content, _resources) { - // This emitter adds static resources to the `resources` parameter. One - // important resource this emitter adds is the code to start a websocket - // connection and listen to rebuild messages, which triggers a page reload. - // The resources parameter with the reload logic is later used by the - // ContentPage emitter while creating the final html page. In order for - // the reload logic to be included, and so for partial rebuilds to work, - // we need to run this emitter for all markdown files. - const graph = new DepGraph(); - - for (const [_tree, file] of content) { - const sourcePath = file.data.filePath!; - const slug = file.data.slug!; - graph.addEdge(sourcePath, joinSegments(ctx.argv.output, slug + ".html") as FilePath); - } - - return graph; + async getDependencyGraph(_ctx, _content, _resources) { + return new DepGraph(); }, - async emit(ctx, _content, resources): Promise { + async emit(ctx, _content, _resources): Promise { const promises: Promise[] = []; const cfg = ctx.cfg.configuration; // component specific scripts and styles const componentResources = getComponentResources(ctx); let googleFontsStyleSheet = ""; - if (fontOrigin === "local") { + if (cfg.theme.fontOrigin === "local") { // let the user do it themselves in css - } else if (fontOrigin === "googleFonts") { - if (cfg.theme.cdnCaching) { - resources.css.push(googleFontHref(cfg.theme)); - } else { - let match; - - const fontSourceRegex = /url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g; - - googleFontsStyleSheet = await ( - await fetch(googleFontHref(ctx.cfg.configuration.theme)) - ).text(); - - while ((match = fontSourceRegex.exec(googleFontsStyleSheet)) !== null) { - // match[0] is the `url(path)`, match[1] is the `path` - const url = match[1]; - // the static name of this file. - const [filename, ext] = url.split("/").pop()!.split("."); - - googleFontsStyleSheet = googleFontsStyleSheet.replace( - url, - `/static/fonts/${filename}.ttf`, - ); - - promises.push( - fetch(url) - .then((res) => { - if (!res.ok) { - throw new Error("Failed to fetch font"); - } - return res.arrayBuffer(); - }) - .then((buf) => - write({ - ctx, - slug: joinSegments("static", "fonts", filename) as FullSlug, - ext: `.${ext}`, - content: Buffer.from(buf), - }), - ), - ); - } + } else if (cfg.theme.fontOrigin === "googleFonts" && !cfg.theme.cdnCaching) { + // when cdnCaching is true, we link to google fonts in Head.tsx + let match; + + const fontSourceRegex = /url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g; + + googleFontsStyleSheet = await ( + await fetch(googleFontHref(ctx.cfg.configuration.theme)) + ).text(); + + while ((match = fontSourceRegex.exec(googleFontsStyleSheet)) !== null) { + // match[0] is the `url(path)`, match[1] is the `path` + const url = match[1]; + // the static name of this file. + const [filename, ext] = url.split("/").pop()!.split("."); + + googleFontsStyleSheet = googleFontsStyleSheet.replace( + url, + `https://${cfg.baseUrl}/static/fonts/${filename}.ttf`, + ); + + promises.push( + fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error("Failed to fetch font"); + } + return res.arrayBuffer(); + }) + .then((buf) => + write({ + ctx, + slug: joinSegments("static", "fonts", filename) as FullSlug, + ext: `.${ext}`, + content: Buffer.from(buf), + }), + ), + ); } } // important that this goes *after* component scripts // as the "nav" event gets triggered here and we should make sure // that everyone else had the chance to register a listener for it - addGlobalPageResources(ctx, resources, componentResources); + addGlobalPageResources(ctx, componentResources); const stylesheet = joinStyles( ctx.cfg.configuration.theme, @@ -304,4 +255,4 @@ export const ComponentResources: QuartzEmitterPlugin = (opts?: Partial< return await Promise.all(promises); }, }; -}; +}; \ No newline at end of file diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts index d52243b95f1cd..f246da4782d8b 100644 --- a/quartz/plugins/index.ts +++ b/quartz/plugins/index.ts @@ -18,6 +18,23 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) { } } + // if serving locally, listen for rebuilds and reload the page + if (ctx.argv.serve) { + const wsUrl = ctx.argv.remoteDevHost + ? `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)) + `, + }); + } + return staticResources; } diff --git a/quartz/plugins/transformers/citations.ts b/quartz/plugins/transformers/citations.ts new file mode 100644 index 0000000000000..bb302e437db82 --- /dev/null +++ b/quartz/plugins/transformers/citations.ts @@ -0,0 +1,52 @@ +import rehypeCitation from "rehype-citation" +import { PluggableList } from "unified" +import { visit } from "unist-util-visit" +import { QuartzTransformerPlugin } from "../types" + +export interface Options { + bibliographyFile: string + suppressBibliography: boolean + linkCitations: boolean + csl: string +} + +const defaultOptions: Options = { + bibliographyFile: "./bibliography.bib", + suppressBibliography: false, + linkCitations: false, + csl: "apa", +} + +export const Citations: QuartzTransformerPlugin | undefined> = (userOpts) => { + const opts = { ...defaultOptions, ...userOpts } + return { + name: "Citations", + htmlPlugins() { + const plugins: PluggableList = [] + + // Add rehype-citation to the list of plugins + plugins.push([ + rehypeCitation, + { + bibliography: opts.bibliographyFile, + suppressBibliography: opts.suppressBibliography, + linkCitations: opts.linkCitations, + }, + ]) + + // Transform the HTML of the citattions; add data-no-popover property to the citation links + // 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) => { + if (node.tagName === "a" && node.properties?.href?.startsWith("#bib")) { + node.properties["data-no-popover"] = true + } + }) + } + }) + + return plugins + }, + } +} diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts index 684bc8d7534de..e02c1028b564f 100644 --- a/quartz/plugins/transformers/description.ts +++ b/quartz/plugins/transformers/description.ts @@ -62,6 +62,7 @@ export const Description: QuartzTransformerPlugin | undefined> const currentSentence = sentence.endsWith(".") ? sentence : sentence + "."; finalDesc.push(currentSentence); currentDescriptionLength += currentSentence.length; + sentenceIdx++; } } diff --git a/quartz/plugins/transformers/index.ts b/quartz/plugins/transformers/index.ts index af85af7d5e5e9..41d5f4ae3402a 100644 --- a/quartz/plugins/transformers/index.ts +++ b/quartz/plugins/transformers/index.ts @@ -1,3 +1,4 @@ +export { Citations } from "./citations"; export { Description } from "./description"; export { FrontMatter } from "./frontmatter"; export { GitHubFlavoredMarkdown } from "./gfm"; diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index e90374f7ba728..e41dd542023ce 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -104,9 +104,9 @@ export const arrowRegex = new RegExp(/(-{1,2}>|={1,2}>|<-{1,2}|<={1,2})/, "g"); // \[\[ -> open brace // ([^\[\]\|\#]+) -> one or more non-special characters ([,],|, or #) (name) // (#[^\[\]\|\#]+)? -> # then one or more non-special characters (heading link) -// (\|[^\[\]\#]+)? -> | then one or more non-special characters (alias) +// (\|[^\[\]\#]+)? -> \| then one or more non-special characters (alias) export const wikilinkRegex = new RegExp( - /!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\#]+)?\]\]/, + /!?\[\[([^\[\]\|\#\\]+)?(#+[^\[\]\|\#\\]+)?(\\?\|[^\[\]\#]+)?\]\]/, "g", ); const highlightRegex = new RegExp(/==([^=]+)==/, "g"); @@ -177,13 +177,18 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin const anchor = rawHeader?.trim().replace(/^#+/, ""); const blockRef = anchor?.startsWith("^") ? "^" : ""; const displayAnchor = anchor ? `#${blockRef}${slugAnchor(anchor)}` : ""; - const displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""; + let displayAlias = rawAlias ?? rawHeader?.replace("#", "|") ?? ""; const embedDisplay = value.startsWith("!") ? "!" : ""; if (rawFp?.match(externalLinkRegex)) { return `${embedDisplay}[${displayAlias.replace(/^\|/, "")}](${rawFp})`; } + //transform `[[note#^block_ref|^block_ref]]` to `[[note#^block_ref\|^block_ref]]`, display correctly in table. + if (displayAlias && displayAlias.startsWith("|")) { + displayAlias = `\\${displayAlias}`; + } + return `${embedDisplay}[[${fp}${displayAnchor}${displayAlias}]]`; }); } diff --git a/quartz/util/theme.ts b/quartz/util/theme.ts index 1c789ece1e90b..5355559aadae7 100644 --- a/quartz/util/theme.ts +++ b/quartz/util/theme.ts @@ -22,6 +22,7 @@ export interface Theme { } cdnCaching: boolean colors: Colors + fontOrigin: "googleFonts" | "local" } export type ThemeKey = keyof Colors