From ba9f243728cab171f86b40b9d50db485af272a39 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Sat, 01 Jul 2023 07:03:01 +0000
Subject: [PATCH] tag and folder pages
---
quartz/components/Footer.tsx | 27 ++
quartz/components/index.ts | 10
quartz/components/PageList.tsx | 53 ++++
quartz/cfg.ts | 15 +
quartz/components/styles/graph.scss | 1
quartz/components/styles/listPage.scss | 36 +++
quartz/components/scripts/graph.inline.ts | 26 ++
quartz/build.ts | 11
quartz/plugins/index.ts | 9
quartz/plugins/vfile.ts | 11
quartz/components/pages/Content.tsx | 11
quartz/plugins/emitters/tagPage.tsx | 74 ++++++
quartz/components/renderPage.tsx | 63 +++++
quartz.config.ts | 79 ++++--
quartz/styles/base.scss | 3
quartz/components/styles/search.scss | 1
quartz/components/Date.tsx | 12 +
/dev/null | 31 --
quartz/plugins/emitters/folderPage.tsx | 77 ++++++
quartz/components/pages/TagContent.tsx | 33 ++
quartz/components/styles/popover.scss | 1
quartz/plugins/emitters/index.ts | 2
quartz/plugins/emitters/contentPage.tsx | 73 +----
quartz/components/pages/FolderContent.tsx | 37 +++
quartz/components/styles/footer.scss | 13 +
25 files changed, 586 insertions(+), 123 deletions(-)
diff --git a/quartz.config.ts b/quartz.config.ts
index bd7a81d..0f2ca8d 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 @@
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 b96bf01..45595e7 100644
--- a/quartz/build.ts
+++ b/quartz/build.ts
@@ -57,11 +57,18 @@
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 e1cf3af..bb097c9 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 @@
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<FullPageLayout, "beforeBody" | "left" | "right">
diff --git a/quartz/components/Content.tsx b/quartz/components/Content.tsx
deleted file mode 100644
index 0bcab1e..0000000
--- 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<Options>) => {
- function Content({ tree }: QuartzComponentProps) {
- // @ts-ignore (preact makes it angry)
- const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
- return <article>{content}</article>
- }
-
- 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 0000000..7ea6ad4
--- /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 0000000..4229f9d
--- /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<string, string>
+}
+
+export default ((opts?: Options) => {
+ function Footer() {
+ const year = new Date().getFullYear()
+ const name = opts?.authorName ?? "someone"
+ const links = opts?.links ?? []
+ return <>
+ <hr />
+ <footer>
+ <p>Made by {name} using <a>Quartz</a>, © {year}</p>
+ <ul>{Object.entries(links).map(([text, link]) => <li>
+ <a href={link}>{text}</a>
+ </li>)}</ul>
+ </footer>
+ </>
+ }
+
+ Footer.css = style
+ return Footer
+}) satisfies QuartzComponentConstructor
diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx
new file mode 100644
index 0000000..e5d8dfb
--- /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 <ul class="section-ul">
+ {allFiles.sort(byDateAndAlphabetical).map(page => {
+ const title = page.frontmatter?.title
+ const pageSlug = page.slug!
+ const tags = page.frontmatter?.tags ?? []
+ return <li class="section-li">
+ <div class="section">
+ {page.dates && <p class="meta">
+ <Date date={page.dates.modified} />
+ </p>}
+ <div class="desc">
+ <h3><a href={stripIndex(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3>
+ </div>
+ <div class="spacer"></div>
+ <ul class="tags">
+ {tags.map(tag => <li><a href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)}
+ </ul>
+ </div>
+ </li>
+ })}
+ </ul>
+}
+
+PageList.css = `
+.section h3 {
+ margin: 0;
+}
+`
diff --git a/quartz/components/index.ts b/quartz/components/index.ts
index 61df101..ed0c668 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 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 @@
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 0000000..7856d6e
--- /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 <article>{content}</article>
+}
+
+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 0000000..4806843
--- /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 <div>
+ <article>{content}</article>
+ <div>
+ <PageList {...listProps} />
+ </div>
+ </div>
+}
+
+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 0000000..e7e5f6d
--- /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 <div>
+ <article>{content}</article>
+ <div>
+ <PageList {...listProps} />
+ </div>
+ </div>
+ } 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 0000000..0e0f4c0
--- /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 = <html>
+ <Head {...componentData} />
+ <body data-slug={slug}>
+ <div id="quartz-root" class="page">
+ <Header {...componentData} >
+ {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
+ </Header>
+ <div class="popover-hint">
+ {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
+ </div>
+ <Body {...componentData}>
+ <div class="left">
+ {left.map(BodyComponent => <BodyComponent {...componentData} />)}
+ </div>
+ <div class="center popover-hint">
+ <Content {...componentData} />
+ </div>
+ <div class="right">
+ {right.map(BodyComponent => <BodyComponent {...componentData} />)}
+ </div>
+ </Body>
+ <Footer {...componentData} />
+ </div>
+ </body>
+ {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
+ </html>
+
+ return "<!DOCTYPE html>\n" + render(doc)
+}
diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts
index 4ff2dfe..27e9a81 100644
--- a/quartz/components/scripts/graph.inline.ts
+++ b/quartz/components/scripts/graph.inline.ts
@@ -13,7 +13,19 @@
target: string
}
+const localStorageKey = "graph-visited"
+function getVisited(): Set<string> {
+ 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 @@
// 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<NodeData, LinkData>) => {
@@ -267,9 +285,15 @@
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 0000000..d104e50
--- /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 76323bb..244f2e4 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 0000000..a5d0a91
--- /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 f95dc7b..5ae09fe 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 bac584b..32d5744 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 03bc0ff..576821a 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<Options> = (opts) => {
+export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (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<string[]> {
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 = <html>
- <Head {...componentData} />
- <body data-slug={file.data.slug ?? ""}>
- <div id="quartz-root" class="page">
- <Header {...componentData} >
- {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
- </Header>
- <div class="popover-hint">
- {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
- </div>
- <Body {...componentData}>
- <div class="left">
- {left.map(BodyComponent => <BodyComponent {...componentData} />)}
- </div>
- <div class="center popover-hint">
- <Content {...componentData} />
- </div>
- <div class="right">
- {right.map(BodyComponent => <BodyComponent {...componentData} />)}
- </div>
- </Body>
-
- </div>
- </body>
- {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
- </html>
+ const content = renderPage(
+ slug,
+ componentData,
+ opts,
+ externalResources
+ )
const fp = file.data.slug + ".html"
await emit({
- content: "<!DOCTYPE html>\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 0000000..ee8f0b9
--- /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<FullPageLayout> = (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<string[]> {
+ const fps: string[] = []
+ const allFiles = content.map(c => c[1].data)
+
+ const folders: Set<string> = new Set(allFiles.flatMap(data => data.slug ? [path.dirname(data.slug)] : []))
+
+ // remove special prefixes
+ folders.delete(".")
+ folders.delete("tags")
+
+ const folderDescriptions: Record<string, ProcessedContent> = 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 971bf19..ff684d5 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 0000000..1f69715
--- /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<FullPageLayout> = (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<string[]> {
+ const fps: string[] = []
+ const allFiles = content.map(c => c[1].data)
+
+ const tags: Set<string> = new Set(allFiles.flatMap(data => data.frontmatter?.tags ?? []))
+ const tagDescriptions: Record<string, ProcessedContent> = 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 0378b1b..ae4593f 100644
--- a/quartz/plugins/index.ts
+++ b/quartz/plugins/index.ts
@@ -4,8 +4,12 @@
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 @@
)
}
+ 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 9df3192..d3d24d3 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<QuartzPluginData>, VFile]
+
+export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): 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 fe62601..45e6370 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 {
--
Gitblit v1.10.0