diff --git a/quartz.config.ts b/quartz.config.ts index bd7a81dc..0f2ca8da 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -1,7 +1,46 @@ -import { QuartzConfig } from "./quartz/cfg" +import { PageLayout, QuartzConfig } from "./quartz/cfg" import * as Component from "./quartz/components" import * as Plugin from "./quartz/plugins" +const sharedPageComponents = { + head: Component.Head(), + header: [ + Component.PageTitle({ title: "🪴 Quartz 4.0" }), + Component.Spacer(), + Component.Search(), + Component.Darkmode() + ], + footer: Component.Footer({ + authorName: "Jacky", + links: { + "GitHub": "https://github.com/jackyzha0", + "Twitter": "https://twitter.com/_jzhao" + } + }) +} + +const contentPageLayout: PageLayout = { + beforeBody: [ + Component.ArticleTitle(), + Component.ReadingTime(), + Component.TagList(), + ], + left: [], + right: [ + Component.Graph(), + Component.TableOfContents(), + Component.Backlinks() + ], +} + +const listPageLayout: PageLayout = { + beforeBody: [ + Component.ArticleTitle() + ], + left: [], + right: [], +} + const config: QuartzConfig = { configuration: { enableSPA: true, @@ -56,30 +95,22 @@ const config: QuartzConfig = { emitters: [ Plugin.AliasRedirects(), Plugin.ContentPage({ - head: Component.Head(), - header: [ - Component.PageTitle({ title: "🪴 Quartz 4.0" }), - Component.Spacer(), - Component.Search(), - Component.Darkmode() - ], - beforeBody: [ - Component.ArticleTitle(), - Component.ReadingTime(), - Component.TagList(), - ], - content: Component.Content(), - left: [ - ], - right: [ - Component.Graph(), - Component.TableOfContents(), - Component.Backlinks() - ], - footer: [] + ...sharedPageComponents, + ...contentPageLayout, + pageBody: Component.Content(), }), - Plugin.ContentIndex(), // you can exclude this if you don't plan on using popovers, graph, or backlinks, - Plugin.CNAME({ domain: "yoursite.xyz" }) // set this to your final deployed domain + Plugin.TagPage({ + ...sharedPageComponents, + ...listPageLayout, + pageBody: Component.TagContent(), + }), + Plugin.FolderPage({ + ...sharedPageComponents, + ...listPageLayout, + pageBody: Component.FolderContent(), + }), + Plugin.ContentIndex(), // you can exclude this if you don't plan on using popovers, graph view, or backlinks + Plugin.CNAME({ domain: "quartz.jzhao.xyz" }) // set this to your final deployed domain ] }, } diff --git a/quartz/build.ts b/quartz/build.ts index b96bf01c..45595e77 100644 --- a/quartz/build.ts +++ b/quartz/build.ts @@ -57,11 +57,18 @@ export default async function buildQuartz(argv: Argv, version: string) { if (argv.serve) { const server = http.createServer(async (req, res) => { - console.log(chalk.grey(`[req] ${req.url}`)) - return serveHandler(req, res, { + let status = 200 + const result = await serveHandler(req, res, { public: output, directoryListing: false, + }, { + async sendError() { + status = 404 + }, }) + const statusString = status === 200 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`) + console.log(statusString + chalk.grey(` ${req.url}`)) + return result }) server.listen(argv.port) console.log(`Started a Quartz server listening at http://localhost:${argv.port}`) diff --git a/quartz/cfg.ts b/quartz/cfg.ts index e1cf3af1..bb097c95 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -1,9 +1,12 @@ +import { QuartzComponent } from "./components/types" import { PluginTypes } from "./plugins/types" import { Theme } from "./theme" export interface GlobalConfiguration { /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ enableSPA: boolean, + /** Whether to display Wikipedia-style popovers when hovering over links */ + enablePopovers: boolean, /** Glob patterns to not search */ ignorePatterns: string[], theme: Theme @@ -13,3 +16,15 @@ export interface QuartzConfig { configuration: GlobalConfiguration, plugins: PluginTypes, } + +export interface FullPageLayout { + head: QuartzComponent + header: QuartzComponent[], + beforeBody: QuartzComponent[], + pageBody: QuartzComponent, + left: QuartzComponent[], + right: QuartzComponent[], + footer: QuartzComponent, +} + +export type PageLayout = Pick diff --git a/quartz/components/Content.tsx b/quartz/components/Content.tsx deleted file mode 100644 index 0bcab1e2..00000000 --- a/quartz/components/Content.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { QuartzComponentConstructor, QuartzComponentProps } from "./types" -import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' -import { toJsxRuntime } from "hast-util-to-jsx-runtime" - -// @ts-ignore -import popoverScript from './scripts/popover.inline' -import popoverStyle from './styles/popover.scss' - -interface Options { - enablePopover: boolean -} - -const defaultOptions: Options = { - enablePopover: true -} - -export default ((opts?: Partial) => { - function Content({ tree }: QuartzComponentProps) { - // @ts-ignore (preact makes it angry) - const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) - return
{content}
- } - - const enablePopover = opts?.enablePopover ?? defaultOptions.enablePopover - if (enablePopover) { - Content.afterDOMLoaded = popoverScript - Content.css = popoverStyle - } - - return Content -}) satisfies QuartzComponentConstructor diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx new file mode 100644 index 00000000..7ea6ad4d --- /dev/null +++ b/quartz/components/Date.tsx @@ -0,0 +1,12 @@ +interface Props { + date: Date +} + +export function Date({ date }: Props) { + const formattedDate = date.toLocaleDateString('en-US', { + year: "numeric", + month: "short", + day: '2-digit' + }) + return <>{formattedDate} +} diff --git a/quartz/components/Footer.tsx b/quartz/components/Footer.tsx new file mode 100644 index 00000000..4229f9d2 --- /dev/null +++ b/quartz/components/Footer.tsx @@ -0,0 +1,27 @@ +import { QuartzComponentConstructor } from "./types" +import style from "./styles/footer.scss" + +interface Options { + authorName: string, + links: Record +} + +export default ((opts?: Options) => { + function Footer() { + const year = new Date().getFullYear() + const name = opts?.authorName ?? "someone" + const links = opts?.links ?? [] + return <> +
+
+

Made by {name} using Quartz, © {year}

+
    {Object.entries(links).map(([text, link]) =>
  • + {text} +
  • )}
+
+ + } + + Footer.css = style + return Footer +}) satisfies QuartzComponentConstructor diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx new file mode 100644 index 00000000..e5d8dfb2 --- /dev/null +++ b/quartz/components/PageList.tsx @@ -0,0 +1,53 @@ +import { relativeToRoot } from "../path" +import { QuartzPluginData } from "../plugins/vfile" +import { Date } from "./Date" +import { stripIndex } from "./scripts/util" +import { QuartzComponentProps } from "./types" + +function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): number { + if (f1.dates && f2.dates) { + // sort descending by last modified + return f2.dates.modified.getTime() - f1.dates.modified.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) +} + +export function PageList({ fileData, allFiles }: QuartzComponentProps) { + const slug = fileData.slug! + return
    + {allFiles.sort(byDateAndAlphabetical).map(page => { + const title = page.frontmatter?.title + const pageSlug = page.slug! + const tags = page.frontmatter?.tags ?? [] + return
  • +
    + {page.dates &&

    + +

    } +
    +

    {title}

    +
    +
    +
      + {tags.map(tag =>
    • #{tag}
    • )} +
    +
    +
  • + })} +
+} + +PageList.css = ` +.section h3 { + margin: 0; +} +` diff --git a/quartz/components/index.ts b/quartz/components/index.ts index 61df101f..ed0c6680 100644 --- a/quartz/components/index.ts +++ b/quartz/components/index.ts @@ -1,5 +1,7 @@ import ArticleTitle from "./ArticleTitle" -import Content from "./Content" +import Content from "./pages/Content" +import TagContent from "./pages/TagContent" +import FolderContent from "./pages/FolderContent" import Darkmode from "./Darkmode" import Head from "./Head" import PageTitle from "./PageTitle" @@ -10,10 +12,13 @@ import TagList from "./TagList" import Graph from "./Graph" import Backlinks from "./Backlinks" import Search from "./Search" +import Footer from "./Footer" export { ArticleTitle, Content, + TagContent, + FolderContent, Darkmode, Head, PageTitle, @@ -23,5 +28,6 @@ export { TagList, Graph, Backlinks, - Search + Search, + Footer } diff --git a/quartz/components/pages/Content.tsx b/quartz/components/pages/Content.tsx new file mode 100644 index 00000000..7856d6ea --- /dev/null +++ b/quartz/components/pages/Content.tsx @@ -0,0 +1,11 @@ +import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' +import { toJsxRuntime } from "hast-util-to-jsx-runtime" + +function Content({ tree }: QuartzComponentProps) { + // @ts-ignore (preact makes it angry) + const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) + return
{content}
+} + +export default (() => Content) satisfies QuartzComponentConstructor \ No newline at end of file diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx new file mode 100644 index 00000000..48068434 --- /dev/null +++ b/quartz/components/pages/FolderContent.tsx @@ -0,0 +1,37 @@ +import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' +import { toJsxRuntime } from "hast-util-to-jsx-runtime" +import path from "path" + +import style from '../styles/listPage.scss' +import { PageList } from "../PageList" + +function TagContent(props: QuartzComponentProps) { + const { tree, fileData, allFiles } = props + const folderSlug = fileData.slug! + const allPagesInFolder = allFiles.filter(file => { + const fileSlug = file.slug ?? "" + const prefixed = fileSlug.startsWith(folderSlug) + const folderParts = folderSlug.split(path.posix.sep) + const fileParts = fileSlug.split(path.posix.sep) + const isDirectChild = fileParts.length === folderParts.length + 1 + return prefixed && isDirectChild + }) + + const listProps = { + ...props, + allFiles: allPagesInFolder + } + + // @ts-ignore + const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) + return
+
{content}
+
+ +
+
+} + +TagContent.css = style + PageList.css +export default (() => TagContent) satisfies QuartzComponentConstructor diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx new file mode 100644 index 00000000..e7e5f6dc --- /dev/null +++ b/quartz/components/pages/TagContent.tsx @@ -0,0 +1,33 @@ +import { QuartzComponentConstructor, QuartzComponentProps } from "../types" +import { Fragment, jsx, jsxs } from 'preact/jsx-runtime' +import { toJsxRuntime } from "hast-util-to-jsx-runtime" +import style from '../styles/listPage.scss' +import { PageList } from "../PageList" + +function TagContent(props: QuartzComponentProps) { + const { tree, fileData, allFiles } = props + const slug = fileData.slug + if (slug?.startsWith("tags/")) { + const tag = slug.slice("tags/".length) + + const allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag)) + const listProps = { + ...props, + allFiles: allPagesWithTag + } + + // @ts-ignore + const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) + return
+
{content}
+
+ +
+
+ } else { + throw `Component "TagContent" tried to render a non-tag page: ${slug}` + } +} + +TagContent.css = style + PageList.css +export default (() => TagContent) satisfies QuartzComponentConstructor diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx new file mode 100644 index 00000000..0e0f4c0c --- /dev/null +++ b/quartz/components/renderPage.tsx @@ -0,0 +1,63 @@ +import { render } from "preact-render-to-string"; +import { QuartzComponent, QuartzComponentProps } from "./types"; +import HeaderConstructor from "./Header" +import BodyConstructor from "./Body" +import { JSResourceToScriptElement, StaticResources } from "../resources"; +import { resolveToRoot } from "../path"; + +interface RenderComponents { + head: QuartzComponent + header: QuartzComponent[], + beforeBody: QuartzComponent[], + pageBody: QuartzComponent, + left: QuartzComponent[], + right: QuartzComponent[], + footer: QuartzComponent, +} + +export function pageResources(slug: string, staticResources: StaticResources): StaticResources { + const baseDir = resolveToRoot(slug) + return { + css: [baseDir + "/index.css", ...staticResources.css], + js: [ + { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" }, + ...staticResources.js, + { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" } + ] + } +} + +export function renderPage(slug: string, componentData: QuartzComponentProps, components: RenderComponents, pageResources: StaticResources): string { + const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = components + const Header = HeaderConstructor() + const Body = BodyConstructor() + + const doc = + + +
+
+ {header.map(HeaderComponent => )} +
+
+ {beforeBody.map(BodyComponent => )} +
+ +
+ {left.map(BodyComponent => )} +
+
+ +
+
+ {right.map(BodyComponent => )} +
+ +
+
+ + {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))} + + + return "\n" + render(doc) +} diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index 4ff2dfe4..27e9a817 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -13,7 +13,19 @@ type LinkData = { target: string } +const localStorageKey = "graph-visited" +function getVisited(): Set { + return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]")) +} + +function addToVisited(slug: string) { + const visited = getVisited() + visited.add(slug) + localStorage.setItem(localStorageKey, JSON.stringify([...visited])) +} + async function renderGraph(container: string, slug: string) { + const visited = getVisited() const graph = document.getElementById(container) if (!graph) return removeAllChildren(graph) @@ -106,7 +118,13 @@ async function renderGraph(container: string, slug: string) { // calculate radius const color = (d: NodeData) => { const isCurrent = d.id === slug - return isCurrent ? "var(--secondary)" : "var(--gray)" + if (isCurrent) { + return "var(--secondary)" + } else if (visited.has(d.id)) { + return "var(--tertiary)" + } else { + return "var(--gray)" + } } const drag = (simulation: d3.Simulation) => { @@ -267,9 +285,15 @@ function renderGlobalGraph() { document.addEventListener("nav", async (e: unknown) => { const slug = (e as CustomEventMap["nav"]).detail.url + addToVisited(slug) await renderGraph("graph-container", slug) const containerIcon = document.getElementById("global-graph-icon") containerIcon?.removeEventListener("click", renderGlobalGraph) containerIcon?.addEventListener("click", renderGlobalGraph) }) + +window.addEventListener('resize', async () => { + const slug = document.body.dataset["slug"]! + await renderGraph("graph-container", slug) +}) diff --git a/quartz/components/styles/footer.scss b/quartz/components/styles/footer.scss new file mode 100644 index 00000000..d104e508 --- /dev/null +++ b/quartz/components/styles/footer.scss @@ -0,0 +1,13 @@ +footer { + text-align: left; + opacity: 0.8; + & ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: row; + gap: 1rem; + margin-top: -1rem; + } +} diff --git a/quartz/components/styles/graph.scss b/quartz/components/styles/graph.scss index 76323bb2..244f2e4e 100644 --- a/quartz/components/styles/graph.scss +++ b/quartz/components/styles/graph.scss @@ -9,7 +9,6 @@ border: 1px solid var(--lightgray); box-sizing: border-box; height: 250px; - width: 300px; margin: 0.5em 0; position: relative; diff --git a/quartz/components/styles/listPage.scss b/quartz/components/styles/listPage.scss new file mode 100644 index 00000000..a5d0a915 --- /dev/null +++ b/quartz/components/styles/listPage.scss @@ -0,0 +1,36 @@ +ul.section-ul { + list-style: none; + margin-top: 2em; + padding-left: 0; +} + +li.section-li { + margin-bottom: 1em; + + & > .section { + display: flex; + align-items: center; + + @media all and (max-width: 600px) { + & .tags { + display: none; + } + } + + & h3 > a { + font-weight: 700; + margin: 0; + background-color: transparent; + } + + & p { + margin: 0; + padding-right: 1em; + flex-basis: 6em; + } + } + + & .meta { + opacity: 0.6; + } +} diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss index f95dc7b7..5ae09fe4 100644 --- a/quartz/components/styles/popover.scss +++ b/quartz/components/styles/popover.scss @@ -26,6 +26,7 @@ font-weight: initial; line-height: initial; font-size: initial; + font-family: var(--bodyFont); border: 1px solid var(--gray); background-color: var(--light); border-radius: 5px; diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index bac584b3..32d57443 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -102,6 +102,7 @@ & .highlight { color: var(--secondary); + font-weight: 700; } &:hover, &:focus { diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx index 03bc0ff7..576821ad 100644 --- a/quartz/plugins/emitters/contentPage.tsx +++ b/quartz/plugins/emitters/contentPage.tsx @@ -1,90 +1,49 @@ -import { JSResourceToScriptElement, StaticResources } from "../../resources" import { QuartzEmitterPlugin } from "../types" -import { render } from "preact-render-to-string" -import { QuartzComponent } from "../../components/types" -import { resolveToRoot, trimPathSuffix } from "../../path" -import HeaderConstructor from "../../components/Header" import { QuartzComponentProps } from "../../components/types" +import HeaderConstructor from "../../components/Header" import BodyConstructor from "../../components/Body" +import { pageResources, renderPage } from "../../components/renderPage" +import { FullPageLayout } from "../../cfg" -interface Options { - head: QuartzComponent - header: QuartzComponent[], - beforeBody: QuartzComponent[], - content: QuartzComponent, - left: QuartzComponent[], - right: QuartzComponent[], - footer: QuartzComponent[], -} - -export const ContentPage: QuartzEmitterPlugin = (opts) => { +export const ContentPage: QuartzEmitterPlugin = (opts) => { if (!opts) { throw new Error("ContentPage must be initialized with options specifiying the components to use") } - const { head: Head, header, beforeBody, left, right, footer } = opts + const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts const Header = HeaderConstructor() const Body = BodyConstructor() return { name: "ContentPage", getQuartzComponents() { - return [opts.head, Header, Body, ...opts.header, ...opts.beforeBody, opts.content, ...opts.left, ...opts.right, ...opts.footer] + return [Head, Header, Body, ...header, ...beforeBody, Content, ...left, ...right, Footer] }, async emit(_contentDir, cfg, content, resources, emit): Promise { const fps: string[] = [] const allFiles = content.map(c => c[1].data) for (const [tree, file] of content) { - const baseDir = resolveToRoot(file.data.slug!) - const pageResources: StaticResources = { - css: [baseDir + "/index.css", ...resources.css], - js: [ - { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" }, - ...resources.js, - { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" } - ] - } - + const slug = file.data.slug! + const externalResources = pageResources(slug, resources) const componentData: QuartzComponentProps = { fileData: file.data, - externalResources: pageResources, + externalResources, cfg, children: [], tree, allFiles } - const Content = opts.content - const doc = - - -
-
- {header.map(HeaderComponent => )} -
-
- {beforeBody.map(BodyComponent => )} -
- -
- {left.map(BodyComponent => )} -
-
- -
-
- {right.map(BodyComponent => )} -
- - -
- - {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))} - + const content = renderPage( + slug, + componentData, + opts, + externalResources + ) const fp = file.data.slug + ".html" await emit({ - content: "\n" + render(doc), + content, slug: file.data.slug!, ext: ".html", }) diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx new file mode 100644 index 00000000..ee8f0b9c --- /dev/null +++ b/quartz/plugins/emitters/folderPage.tsx @@ -0,0 +1,77 @@ +import { QuartzEmitterPlugin } from "../types" +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 { FullPageLayout } from "../../cfg" +import path from "path" + +export const FolderPage: QuartzEmitterPlugin = (opts) => { + if (!opts) { + throw new Error("ErrorPage must be initialized with options specifiying the components to use") + } + + const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts + const Header = HeaderConstructor() + const Body = BodyConstructor() + + return { + name: "FolderPage", + getQuartzComponents() { + return [Head, Header, Body, ...header, ...beforeBody, Content, ...left, ...right, Footer] + }, + async emit(_contentDir, cfg, content, resources, emit): Promise { + const fps: string[] = [] + const allFiles = content.map(c => c[1].data) + + const folders: Set = new Set(allFiles.flatMap(data => data.slug ? [path.dirname(data.slug)] : [])) + + // remove special prefixes + folders.delete(".") + folders.delete("tags") + + const folderDescriptions: Record = Object.fromEntries([...folders].map(folder => ([ + folder, defaultProcessedContent({ slug: folder, frontmatter: { title: `Folder: ${folder}`, tags: [] } }) + ]))) + + for (const [tree, file] of content) { + const slug = file.data.slug! + if (folders.has(slug)) { + folderDescriptions[slug] = [tree, file] + } + } + + for (const folder of folders) { + const slug = folder + const externalResources = pageResources(slug, resources) + const [tree, file] = folderDescriptions[folder] + const componentData: QuartzComponentProps = { + fileData: file.data, + externalResources, + cfg, + children: [], + tree, + allFiles + } + + const content = renderPage( + slug, + componentData, + opts, + externalResources + ) + + const fp = file.data.slug + ".html" + await emit({ + content, + slug: file.data.slug!, + ext: ".html", + }) + + fps.push(fp) + } + return fps + } + } +} diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts index 971bf194..ff684d52 100644 --- a/quartz/plugins/emitters/index.ts +++ b/quartz/plugins/emitters/index.ts @@ -1,4 +1,6 @@ export { ContentPage } from './contentPage' +export { TagPage } from './tagPage' +export { FolderPage } from './folderPage' export { ContentIndex } from './contentIndex' export { AliasRedirects } from './aliases' export { CNAME } from './cname' diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx new file mode 100644 index 00000000..1f697159 --- /dev/null +++ b/quartz/plugins/emitters/tagPage.tsx @@ -0,0 +1,74 @@ +import { QuartzEmitterPlugin } from "../types" +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 { FullPageLayout } from "../../cfg" + +export const TagPage: QuartzEmitterPlugin = (opts) => { + if (!opts) { + throw new Error("TagPage must be initialized with options specifiying the components to use") + } + + const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts + const Header = HeaderConstructor() + const Body = BodyConstructor() + + return { + name: "TagPage", + getQuartzComponents() { + return [Head, Header, Body, ...header, ...beforeBody, Content, ...left, ...right, Footer] + }, + async emit(_contentDir, cfg, content, resources, emit): Promise { + const fps: string[] = [] + const allFiles = content.map(c => c[1].data) + + const tags: Set = new Set(allFiles.flatMap(data => data.frontmatter?.tags ?? [])) + const tagDescriptions: Record = Object.fromEntries([...tags].map(tag => ([ + tag, defaultProcessedContent({ slug: `tags/${tag}`, frontmatter: { title: `Tag: ${tag}`, tags: [] } }) + ]))) + + for (const [tree, file] of content) { + const slug = file.data.slug! + if (slug.startsWith("tags/")) { + const tag = slug.slice("tags/".length) + if (tags.has(tag)) { + tagDescriptions[tag] = [tree, file] + } + } + } + + for (const tag of tags) { + const slug = `tags/${tag}` + const externalResources = pageResources(slug, resources) + const [tree, file] = tagDescriptions[tag] + const componentData: QuartzComponentProps = { + fileData: file.data, + externalResources, + cfg, + children: [], + tree, + allFiles + } + + const content = renderPage( + slug, + componentData, + opts, + externalResources + ) + + const fp = file.data.slug + ".html" + await emit({ + content, + slug: file.data.slug!, + ext: ".html", + }) + + fps.push(fp) + } + return fps + } + } +} diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts index 0378b1bb..ae4593f1 100644 --- a/quartz/plugins/index.ts +++ b/quartz/plugins/index.ts @@ -4,8 +4,12 @@ import { StaticResources } from '../resources' import { googleFontHref, joinStyles } from '../theme' import { EmitCallback, PluginTypes } from './types' import styles from '../styles/base.scss' + // @ts-ignore import spaRouterScript from '../components/scripts/spa.inline' +// @ts-ignore +import popoverScript from '../components/scripts/popover.inline' +import popoverStyle from '../components/styles/popover.scss' export type ComponentResources = { css: string[], @@ -57,6 +61,11 @@ export function emitComponentResources(cfg: GlobalConfiguration, resources: Stat ) } + if (cfg.enablePopovers) { + componentResources.afterDOMLoaded.push(popoverScript) + componentResources.css.push(popoverStyle) + } + emit({ slug: "index", ext: ".css", diff --git a/quartz/plugins/vfile.ts b/quartz/plugins/vfile.ts index 9df31929..d3d24d37 100644 --- a/quartz/plugins/vfile.ts +++ b/quartz/plugins/vfile.ts @@ -1,5 +1,12 @@ -import { Node } from 'hast' -import { Data, VFile } from 'vfile/lib' +import { Node, Parent } from 'hast' +import { Data, VFile } from 'vfile' export type QuartzPluginData = Data export type ProcessedContent = [Node, VFile] + +export function defaultProcessedContent(vfileData: Partial): ProcessedContent { + const root: Parent = { type: 'root', children: [] } + const vfile = new VFile("") + vfile.data = vfileData + return [root, vfile] +} diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index fe626017..45e6370c 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -3,9 +3,6 @@ html { scroll-behavior: smooth; - & footer > p { - text-align: center !important; - } } body {