1 files deleted
11 files added
13 files modified
| | |
| | | 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, |
| | |
| | | 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 |
| | | ] |
| | | }, |
| | | } |
| | |
| | | |
| | | 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}`) |
| | |
| | | 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 |
| | |
| | | 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"> |
| New file |
| | |
| | | interface Props { |
| | | date: Date |
| | | } |
| | | |
| | | export function Date({ date }: Props) { |
| | | const formattedDate = date.toLocaleDateString('en-US', { |
| | | year: "numeric", |
| | | month: "short", |
| | | day: '2-digit' |
| | | }) |
| | | return <>{formattedDate}</> |
| | | } |
| New file |
| | |
| | | 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 |
| New file |
| | |
| | | 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; |
| | | } |
| | | ` |
| | |
| | | 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" |
| | |
| | | import Graph from "./Graph" |
| | | import Backlinks from "./Backlinks" |
| | | import Search from "./Search" |
| | | import Footer from "./Footer" |
| | | |
| | | export { |
| | | ArticleTitle, |
| | | Content, |
| | | TagContent, |
| | | FolderContent, |
| | | Darkmode, |
| | | Head, |
| | | PageTitle, |
| | |
| | | TagList, |
| | | Graph, |
| | | Backlinks, |
| | | Search |
| | | Search, |
| | | Footer |
| | | } |
| New file |
| | |
| | | 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 |
| New file |
| | |
| | | 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 |
| New file |
| | |
| | | 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 |
| New file |
| | |
| | | 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) |
| | | } |
| | |
| | | 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) |
| | |
| | | // 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>) => { |
| | |
| | | |
| | | 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) |
| | | }) |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | border: 1px solid var(--lightgray); |
| | | box-sizing: border-box; |
| | | height: 250px; |
| | | width: 300px; |
| | | margin: 0.5em 0; |
| | | position: relative; |
| | | |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | |
| | | & .highlight { |
| | | color: var(--secondary); |
| | | font-weight: 700; |
| | | } |
| | | |
| | | &:hover, &:focus { |
| | |
| | | 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", |
| | | }) |
| New file |
| | |
| | | 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 |
| | | } |
| | | } |
| | | } |
| | |
| | | export { ContentPage } from './contentPage' |
| | | export { TagPage } from './tagPage' |
| | | export { FolderPage } from './folderPage' |
| | | export { ContentIndex } from './contentIndex' |
| | | export { AliasRedirects } from './aliases' |
| | | export { CNAME } from './cname' |
| New file |
| | |
| | | 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 |
| | | } |
| | | } |
| | | } |
| | |
| | | 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[], |
| | |
| | | ) |
| | | } |
| | | |
| | | if (cfg.enablePopovers) { |
| | | componentResources.afterDOMLoaded.push(popoverScript) |
| | | componentResources.css.push(popoverStyle) |
| | | } |
| | | |
| | | emit({ |
| | | slug: "index", |
| | | ext: ".css", |
| | |
| | | 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] |
| | | } |
| | |
| | | |
| | | html { |
| | | scroll-behavior: smooth; |
| | | & footer > p { |
| | | text-align: center !important; |
| | | } |
| | | } |
| | | |
| | | body { |