This commit is contained in:
Jacky Zhao 2023-06-09 23:06:02 -07:00
parent b3cbcaf0f5
commit 8c5dd2287a
21 changed files with 233 additions and 66 deletions

View File

@ -1,21 +1,6 @@
import { QuartzConfig } from "./quartz/cfg" import { QuartzConfig } from "./quartz/cfg"
import Body from "./quartz/components/Body" import * as Component from "./quartz/components"
import Darkmode from "./quartz/components/Darkmode" import * as Plugin from "./quartz/plugins"
import Head from "./quartz/components/Head"
import PageTitle from "./quartz/components/PageTitle"
import Spacer from "./quartz/components/Spacer"
import {
ContentPage,
CreatedModifiedDate,
Description,
FrontMatter,
GitHubFlavoredMarkdown,
Katex,
ObsidianFlavoredMarkdown,
RemoveDrafts,
ResolveLinks,
SyntaxHighlighting
} from "./quartz/plugins"
const config: QuartzConfig = { const config: QuartzConfig = {
configuration: { configuration: {
@ -54,25 +39,26 @@ const config: QuartzConfig = {
}, },
plugins: { plugins: {
transformers: [ transformers: [
new FrontMatter(), new Plugin.FrontMatter(),
new Katex(), new Plugin.Description(),
new Description(), new Plugin.TableOfContents({ showByDefault: true }),
new CreatedModifiedDate({ new Plugin.CreatedModifiedDate({
priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower
}), }),
new SyntaxHighlighting(), new Plugin.GitHubFlavoredMarkdown(),
new GitHubFlavoredMarkdown(), new Plugin.ObsidianFlavoredMarkdown(),
new ObsidianFlavoredMarkdown(), new Plugin.ResolveLinks(),
new ResolveLinks(), new Plugin.SyntaxHighlighting(),
new Plugin.Katex(),
], ],
filters: [ filters: [
new RemoveDrafts() new Plugin.RemoveDrafts()
], ],
emitters: [ emitters: [
new ContentPage({ new Plugin.ContentPage({
head: Head, head: Component.Head,
header: [PageTitle, Spacer, Darkmode], header: [Component.PageTitle, Component.Spacer, Component.Darkmode],
body: Body body: [Component.ArticleTitle, Component.ReadingTime, Component.TableOfContents, Component.Content]
}) })
] ]
}, },

View File

@ -0,0 +1,11 @@
import { QuartzComponentProps } from "./types"
export default function ArticleTitle({ fileData }: QuartzComponentProps) {
const title = fileData.frontmatter?.title
const displayTitle = fileData.slug === "index" ? undefined : title
if (displayTitle) {
return <h1>{displayTitle}</h1>
} else {
return null
}
}

View File

@ -2,13 +2,8 @@ import clipboardScript from './scripts/clipboard.inline'
import clipboardStyle from './styles/clipboard.scss' import clipboardStyle from './styles/clipboard.scss'
import { QuartzComponentProps } from "./types" import { QuartzComponentProps } from "./types"
export default function Body({ fileData, children }: QuartzComponentProps) { export default function Body({ children }: QuartzComponentProps) {
const title = fileData.frontmatter?.title
const displayTitle = fileData.slug === "index" ? undefined : title
return <article> return <article>
<div class="top-section">
{displayTitle && <h1>{displayTitle}</h1>}
</div>
{children} {children}
</article> </article>
} }

View File

@ -0,0 +1,9 @@
import { QuartzComponentProps } from "./types"
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
export default function Content({ tree }: QuartzComponentProps) {
// @ts-ignore (preact makes it angry)
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
return content
}

View File

@ -1,4 +1,3 @@
import style from './styles/header.scss'
import { QuartzComponentProps } from "./types" import { QuartzComponentProps } from "./types"
export default function Header({ children }: QuartzComponentProps) { export default function Header({ children }: QuartzComponentProps) {
@ -7,4 +6,18 @@ export default function Header({ children }: QuartzComponentProps) {
</header> </header>
} }
Header.css = style Header.css = `
header {
display: flex;
flex-direction: row;
align-items: center;
margin: 1em 0 2em 0;
& > h1 {
}
}
header > h1 {
margin: 0;
flex: auto;
}
`

View File

@ -0,0 +1,20 @@
import { QuartzComponentProps } from "./types"
import readingTime from "reading-time"
export default function ReadingTime({ fileData }: QuartzComponentProps) {
const text = fileData.text
const isHomePage = fileData.slug === "index"
if (text && !isHomePage) {
const { text: timeTaken, words } = readingTime(text)
return <p class="reading-time">{words} words, {timeTaken}</p>
} else {
return null
}
}
ReadingTime.css = `
.reading-time {
margin-top: 0;
opacity: 0.5;
}
`

View File

@ -0,0 +1,24 @@
import { QuartzComponentProps } from "./types"
import style from "./styles/toc.scss"
export default function TableOfContents({ fileData, position }: QuartzComponentProps) {
if (!fileData.toc) {
return null
}
if (position === 'body') {
// TODO: animate this
return <details className="toc" open>
<summary><h3>Table of Contents</h3></summary>
<ul>
{fileData.toc.map(tocEntry => <li key={tocEntry.slug} className={`depth-${tocEntry.depth}`}>
<a href={`#${tocEntry.slug}`}>{tocEntry.text}</a>
</li>)}
</ul>
</details>
} else if (position === 'sidebar') {
// TODO
}
}
TableOfContents.css = style

View File

@ -0,0 +1,19 @@
import ArticleTitle from "./ArticleTitle"
import Content from "./Content"
import Darkmode from "./Darkmode"
import Head from "./Head"
import PageTitle from "./PageTitle"
import ReadingTime from "./ReadingTime"
import Spacer from "./Spacer"
import TableOfContents from "./TableOfContents"
export {
ArticleTitle,
Content,
Darkmode,
Head,
PageTitle,
ReadingTime,
Spacer,
TableOfContents
}

View File

@ -1,10 +0,0 @@
header {
display: flex;
flex-direction: row;
align-items: center;
margin: 1em 0 2em 0;
& > h1 {
margin: 0;
flex: auto;
}
}

View File

@ -0,0 +1,27 @@
details.toc {
& summary {
cursor: pointer;
&::marker {
color: var(--dark);
}
& > * {
padding-left: 0.25rem;
display: inline-block;
margin: 0;
}
}
& ul {
list-style: none;
margin: 0.5rem 1.25rem;
padding: 0;
}
@for $i from 1 through 6 {
& .depth-#{$i} {
padding-left: calc(1rem * #{$i});
}
}
}

View File

@ -2,12 +2,15 @@ import { ComponentType, JSX } from "preact"
import { StaticResources } from "../resources" import { StaticResources } from "../resources"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { GlobalConfiguration } from "../cfg" import { GlobalConfiguration } from "../cfg"
import { Node } from "hast"
export type QuartzComponentProps = { export type QuartzComponentProps = {
externalResources: StaticResources externalResources: StaticResources
fileData: QuartzPluginData fileData: QuartzPluginData
cfg: GlobalConfiguration cfg: GlobalConfiguration
children: QuartzComponent[] | JSX.Element[] children: QuartzComponent[] | JSX.Element[]
tree: Node<QuartzPluginData>
position?: 'sidebar' | 'header' | 'body'
} }
export type QuartzComponent = ComponentType<QuartzComponentProps> & { export type QuartzComponent = ComponentType<QuartzComponentProps> & {

View File

@ -1,7 +1,7 @@
import path from 'path' import path from 'path'
import SlugAnchor from 'github-slugger' import SlugAnchor from 'github-slugger'
const slugAnchor = new SlugAnchor() export const slugAnchor = new SlugAnchor()
function slugSegment(s: string): string { function slugSegment(s: string): string {
return s.replace(/\s/g, '-') return s.replace(/\s/g, '-')

View File

@ -1,19 +1,18 @@
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import { StaticResources } from "../../resources" import { StaticResources } from "../../resources"
import { EmitCallback, QuartzEmitterPlugin } from "../types" import { EmitCallback, QuartzEmitterPlugin } from "../types"
import { ProcessedContent } from "../vfile" import { ProcessedContent } from "../vfile"
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { render } from "preact-render-to-string" import { render } from "preact-render-to-string"
import { GlobalConfiguration } from "../../cfg" import { GlobalConfiguration } from "../../cfg"
import { QuartzComponent } from "../../components/types" import { QuartzComponent } from "../../components/types"
import { resolveToRoot } from "../../path" import { resolveToRoot } from "../../path"
import Header from "../../components/Header" import Header from "../../components/Header"
import { QuartzComponentProps } from "../../components/types" import { QuartzComponentProps } from "../../components/types"
import Body from "../../components/Body"
interface Options { interface Options {
head: QuartzComponent head: QuartzComponent
header: QuartzComponent[], header: QuartzComponent[],
body: QuartzComponent body: QuartzComponent[]
} }
export class ContentPage extends QuartzEmitterPlugin { export class ContentPage extends QuartzEmitterPlugin {
@ -26,17 +25,14 @@ export class ContentPage extends QuartzEmitterPlugin {
} }
getQuartzComponents(): QuartzComponent[] { getQuartzComponents(): QuartzComponent[] {
return [this.opts.head, Header, ...this.opts.header, this.opts.body] return [this.opts.head, Header, ...this.opts.header, ...this.opts.body]
} }
async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> { async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> {
const fps: string[] = [] const fps: string[] = []
const { head: Head, header, body: Body } = this.opts const { head: Head, header, body } = this.opts
for (const [tree, file] of content) { for (const [tree, file] of content) {
// @ts-ignore (preact makes it angry)
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
const baseDir = resolveToRoot(file.data.slug!) const baseDir = resolveToRoot(file.data.slug!)
const pageResources: StaticResources = { const pageResources: StaticResources = {
css: [baseDir + "/index.css", ...resources.css], css: [baseDir + "/index.css", ...resources.css],
@ -51,7 +47,8 @@ export class ContentPage extends QuartzEmitterPlugin {
fileData: file.data, fileData: file.data,
externalResources: pageResources, externalResources: pageResources,
cfg, cfg,
children: [content] children: [],
tree
} }
const doc = <html> const doc = <html>
@ -59,10 +56,10 @@ export class ContentPage extends QuartzEmitterPlugin {
<body> <body>
<div id="quartz-root" class="page"> <div id="quartz-root" class="page">
<Header {...componentData} > <Header {...componentData} >
{header.map(HeaderComponent => <HeaderComponent {...componentData}/>)} {header.map(HeaderComponent => <HeaderComponent {...componentData} position="header" />)}
</Header> </Header>
<Body {...componentData}> <Body {...componentData}>
{content} {body.map(BodyComponent => <BodyComponent {...componentData } position="body" />)}
</Body> </Body>
</div> </div>
</body> </body>

View File

@ -15,7 +15,7 @@ export class Description extends QuartzTransformerPlugin {
name = "Description" name = "Description"
opts: Options opts: Options
constructor(opts?: Options) { constructor(opts?: Partial<Options>) {
super() super()
this.opts = { ...defaultOptions, ...opts } this.opts = { ...defaultOptions, ...opts }
} }

View File

@ -17,7 +17,7 @@ export class FrontMatter extends QuartzTransformerPlugin {
name = "FrontMatter" name = "FrontMatter"
opts: Options opts: Options
constructor(opts?: Options) { constructor(opts?: Partial<Options>) {
super() super()
this.opts = { ...defaultOptions, ...opts } this.opts = { ...defaultOptions, ...opts }
} }

View File

@ -19,7 +19,7 @@ export class GitHubFlavoredMarkdown extends QuartzTransformerPlugin {
name = "GitHubFlavoredMarkdown" name = "GitHubFlavoredMarkdown"
opts: Options opts: Options
constructor(opts?: Options) { constructor(opts?: Partial<Options>) {
super() super()
this.opts = { ...defaultOptions, ...opts } this.opts = { ...defaultOptions, ...opts }
} }

View File

@ -6,3 +6,4 @@ export { Description } from './description'
export { ResolveLinks } from './links' export { ResolveLinks } from './links'
export { ObsidianFlavoredMarkdown } from './ofm' export { ObsidianFlavoredMarkdown } from './ofm'
export { SyntaxHighlighting } from './syntax' export { SyntaxHighlighting } from './syntax'
export { TableOfContents } from './toc'

View File

@ -16,7 +16,7 @@ export class CreatedModifiedDate extends QuartzTransformerPlugin {
name = "CreatedModifiedDate" name = "CreatedModifiedDate"
opts: Options opts: Options
constructor(opts?: Options) { constructor(opts?: Partial<Options>) {
super() super()
this.opts = { this.opts = {
...defaultOptions, ...defaultOptions,

View File

@ -21,7 +21,7 @@ export class ResolveLinks extends QuartzTransformerPlugin {
name = "LinkProcessing" name = "LinkProcessing"
opts: Options opts: Options
constructor(opts?: Options) { constructor(opts?: Partial<Options>) {
super() super()
this.opts = { ...defaultOptions, ...opts } this.opts = { ...defaultOptions, ...opts }
} }

View File

@ -93,7 +93,7 @@ export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
name = "ObsidianFlavoredMarkdown" name = "ObsidianFlavoredMarkdown"
opts: Options opts: Options
constructor(opts?: Options) { constructor(opts?: Partial<Options>) {
super() super()
this.opts = { ...defaultOptions, ...opts } this.opts = { ...defaultOptions, ...opts }
} }

View File

@ -0,0 +1,72 @@
import { PluggableList } from "unified"
import { QuartzTransformerPlugin } from "../types"
import { Root } from "mdast"
import { visit } from "unist-util-visit"
import { toString } from "mdast-util-to-string"
import { slugAnchor } from "../../path"
export interface Options {
maxDepth: 1 | 2 | 3 | 4 | 5 | 6,
minEntries: 1,
showByDefault: boolean
}
const defaultOptions: Options = {
maxDepth: 3,
minEntries: 1,
showByDefault: true,
}
interface TocEntry {
depth: number,
text: string,
slug: string
}
export class TableOfContents extends QuartzTransformerPlugin {
name = "TableOfContents"
opts: Options
constructor(opts?: Partial<Options>) {
super()
this.opts = { ...defaultOptions, ...opts }
}
markdownPlugins(): PluggableList {
return [() => {
return async (tree: Root, file) => {
const display = file.data.frontmatter?.enableToc ?? this.opts.showByDefault
if (display) {
const toc: TocEntry[] = []
let highestDepth: number = this.opts.maxDepth
visit(tree, 'heading', (node) => {
if (node.depth <= this.opts.maxDepth) {
const text = toString(node)
highestDepth = Math.min(highestDepth, node.depth)
toc.push({
depth: node.depth,
text,
slug: slugAnchor.slug(text)
})
}
})
if (toc.length > this.opts.minEntries) {
file.data.toc = toc.map(entry => ({ ...entry, depth: entry.depth - highestDepth }))
}
}
}
}]
}
htmlPlugins(): PluggableList {
return []
}
}
declare module 'vfile' {
interface DataMap {
toc: TocEntry[]
}
}