tag and folder pages

This commit is contained in:
Jacky Zhao 2023-07-01 00:03:01 -07:00
parent 24348b24a9
commit ba9f243728
25 changed files with 586 additions and 123 deletions

View file

@ -1,7 +1,46 @@
import { QuartzConfig } from "./quartz/cfg" import { PageLayout, QuartzConfig } from "./quartz/cfg"
import * as Component from "./quartz/components" import * as Component from "./quartz/components"
import * as Plugin from "./quartz/plugins" 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 = { const config: QuartzConfig = {
configuration: { configuration: {
enableSPA: true, enableSPA: true,
@ -56,30 +95,22 @@ const config: QuartzConfig = {
emitters: [ emitters: [
Plugin.AliasRedirects(), Plugin.AliasRedirects(),
Plugin.ContentPage({ Plugin.ContentPage({
head: Component.Head(), ...sharedPageComponents,
header: [ ...contentPageLayout,
Component.PageTitle({ title: "🪴 Quartz 4.0" }), pageBody: Component.Content(),
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: []
}), }),
Plugin.ContentIndex(), // you can exclude this if you don't plan on using popovers, graph, or backlinks, Plugin.TagPage({
Plugin.CNAME({ domain: "yoursite.xyz" }) // set this to your final deployed domain ...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
] ]
}, },
} }

View file

@ -57,11 +57,18 @@ export default async function buildQuartz(argv: Argv, version: string) {
if (argv.serve) { if (argv.serve) {
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
console.log(chalk.grey(`[req] ${req.url}`)) let status = 200
return serveHandler(req, res, { const result = await serveHandler(req, res, {
public: output, public: output,
directoryListing: false, 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) server.listen(argv.port)
console.log(`Started a Quartz server listening at http://localhost:${argv.port}`) console.log(`Started a Quartz server listening at http://localhost:${argv.port}`)

View file

@ -1,9 +1,12 @@
import { QuartzComponent } from "./components/types"
import { PluginTypes } from "./plugins/types" import { PluginTypes } from "./plugins/types"
import { Theme } from "./theme" import { Theme } from "./theme"
export interface GlobalConfiguration { export interface GlobalConfiguration {
/** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
enableSPA: boolean, enableSPA: boolean,
/** Whether to display Wikipedia-style popovers when hovering over links */
enablePopovers: boolean,
/** Glob patterns to not search */ /** Glob patterns to not search */
ignorePatterns: string[], ignorePatterns: string[],
theme: Theme theme: Theme
@ -13,3 +16,15 @@ export interface QuartzConfig {
configuration: GlobalConfiguration, configuration: GlobalConfiguration,
plugins: PluginTypes, 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">

View file

@ -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

View file

@ -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}</>
}

View file

@ -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

View file

@ -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;
}
`

View file

@ -1,5 +1,7 @@
import ArticleTitle from "./ArticleTitle" 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 Darkmode from "./Darkmode"
import Head from "./Head" import Head from "./Head"
import PageTitle from "./PageTitle" import PageTitle from "./PageTitle"
@ -10,10 +12,13 @@ import TagList from "./TagList"
import Graph from "./Graph" import Graph from "./Graph"
import Backlinks from "./Backlinks" import Backlinks from "./Backlinks"
import Search from "./Search" import Search from "./Search"
import Footer from "./Footer"
export { export {
ArticleTitle, ArticleTitle,
Content, Content,
TagContent,
FolderContent,
Darkmode, Darkmode,
Head, Head,
PageTitle, PageTitle,
@ -23,5 +28,6 @@ export {
TagList, TagList,
Graph, Graph,
Backlinks, Backlinks,
Search Search,
Footer
} }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
}

View file

@ -13,7 +13,19 @@ type LinkData = {
target: string 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) { async function renderGraph(container: string, slug: string) {
const visited = getVisited()
const graph = document.getElementById(container) const graph = document.getElementById(container)
if (!graph) return if (!graph) return
removeAllChildren(graph) removeAllChildren(graph)
@ -106,7 +118,13 @@ async function renderGraph(container: string, slug: string) {
// calculate radius // calculate radius
const color = (d: NodeData) => { const color = (d: NodeData) => {
const isCurrent = d.id === slug 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>) => { const drag = (simulation: d3.Simulation<NodeData, LinkData>) => {
@ -267,9 +285,15 @@ function renderGlobalGraph() {
document.addEventListener("nav", async (e: unknown) => { document.addEventListener("nav", async (e: unknown) => {
const slug = (e as CustomEventMap["nav"]).detail.url const slug = (e as CustomEventMap["nav"]).detail.url
addToVisited(slug)
await renderGraph("graph-container", slug) await renderGraph("graph-container", slug)
const containerIcon = document.getElementById("global-graph-icon") const containerIcon = document.getElementById("global-graph-icon")
containerIcon?.removeEventListener("click", renderGlobalGraph) containerIcon?.removeEventListener("click", renderGlobalGraph)
containerIcon?.addEventListener("click", renderGlobalGraph) containerIcon?.addEventListener("click", renderGlobalGraph)
}) })
window.addEventListener('resize', async () => {
const slug = document.body.dataset["slug"]!
await renderGraph("graph-container", slug)
})

View file

@ -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;
}
}

View file

@ -9,7 +9,6 @@
border: 1px solid var(--lightgray); border: 1px solid var(--lightgray);
box-sizing: border-box; box-sizing: border-box;
height: 250px; height: 250px;
width: 300px;
margin: 0.5em 0; margin: 0.5em 0;
position: relative; position: relative;

View file

@ -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;
}
}

View file

@ -26,6 +26,7 @@
font-weight: initial; font-weight: initial;
line-height: initial; line-height: initial;
font-size: initial; font-size: initial;
font-family: var(--bodyFont);
border: 1px solid var(--gray); border: 1px solid var(--gray);
background-color: var(--light); background-color: var(--light);
border-radius: 5px; border-radius: 5px;

View file

@ -102,6 +102,7 @@
& .highlight { & .highlight {
color: var(--secondary); color: var(--secondary);
font-weight: 700;
} }
&:hover, &:focus { &:hover, &:focus {

View file

@ -1,90 +1,49 @@
import { JSResourceToScriptElement, StaticResources } from "../../resources"
import { QuartzEmitterPlugin } from "../types" 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 { QuartzComponentProps } from "../../components/types"
import HeaderConstructor from "../../components/Header"
import BodyConstructor from "../../components/Body" import BodyConstructor from "../../components/Body"
import { pageResources, renderPage } from "../../components/renderPage"
import { FullPageLayout } from "../../cfg"
interface Options { export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
head: QuartzComponent
header: QuartzComponent[],
beforeBody: QuartzComponent[],
content: QuartzComponent,
left: QuartzComponent[],
right: QuartzComponent[],
footer: QuartzComponent[],
}
export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
if (!opts) { if (!opts) {
throw new Error("ContentPage must be initialized with options specifiying the components to use") 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 Header = HeaderConstructor()
const Body = BodyConstructor() const Body = BodyConstructor()
return { return {
name: "ContentPage", name: "ContentPage",
getQuartzComponents() { 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[]> { async emit(_contentDir, cfg, content, resources, emit): Promise<string[]> {
const fps: string[] = [] const fps: string[] = []
const allFiles = content.map(c => c[1].data) const allFiles = content.map(c => c[1].data)
for (const [tree, file] of content) { for (const [tree, file] of content) {
const baseDir = resolveToRoot(file.data.slug!) const slug = file.data.slug!
const pageResources: StaticResources = { const externalResources = pageResources(slug, resources)
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 componentData: QuartzComponentProps = { const componentData: QuartzComponentProps = {
fileData: file.data, fileData: file.data,
externalResources: pageResources, externalResources,
cfg, cfg,
children: [], children: [],
tree, tree,
allFiles allFiles
} }
const Content = opts.content const content = renderPage(
const doc = <html> slug,
<Head {...componentData} /> componentData,
<body data-slug={file.data.slug ?? ""}> opts,
<div id="quartz-root" class="page"> externalResources
<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 fp = file.data.slug + ".html" const fp = file.data.slug + ".html"
await emit({ await emit({
content: "<!DOCTYPE html>\n" + render(doc), content,
slug: file.data.slug!, slug: file.data.slug!,
ext: ".html", ext: ".html",
}) })

View file

@ -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
}
}
}

View file

@ -1,4 +1,6 @@
export { ContentPage } from './contentPage' export { ContentPage } from './contentPage'
export { TagPage } from './tagPage'
export { FolderPage } from './folderPage'
export { ContentIndex } from './contentIndex' export { ContentIndex } from './contentIndex'
export { AliasRedirects } from './aliases' export { AliasRedirects } from './aliases'
export { CNAME } from './cname' export { CNAME } from './cname'

View file

@ -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
}
}
}

View file

@ -4,8 +4,12 @@ import { StaticResources } from '../resources'
import { googleFontHref, joinStyles } from '../theme' import { googleFontHref, joinStyles } from '../theme'
import { EmitCallback, PluginTypes } from './types' import { EmitCallback, PluginTypes } from './types'
import styles from '../styles/base.scss' import styles from '../styles/base.scss'
// @ts-ignore // @ts-ignore
import spaRouterScript from '../components/scripts/spa.inline' 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 = { export type ComponentResources = {
css: string[], 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({ emit({
slug: "index", slug: "index",
ext: ".css", ext: ".css",

View file

@ -1,5 +1,12 @@
import { Node } from 'hast' import { Node, Parent } from 'hast'
import { Data, VFile } from 'vfile/lib' import { Data, VFile } from 'vfile'
export type QuartzPluginData = Data export type QuartzPluginData = Data
export type ProcessedContent = [Node<QuartzPluginData>, VFile] 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]
}

View file

@ -3,9 +3,6 @@
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
& footer > p {
text-align: center !important;
}
} }
body { body {