diff --git a/quartz.config.ts b/quartz.config.ts
index 65539a84d..3a1d43345 100644
--- a/quartz.config.ts
+++ b/quartz.config.ts
@@ -1,21 +1,6 @@
import { QuartzConfig } from "./quartz/cfg"
-import Body from "./quartz/components/Body"
-import Darkmode from "./quartz/components/Darkmode"
-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"
+import * as Component from "./quartz/components"
+import * as Plugin from "./quartz/plugins"
const config: QuartzConfig = {
configuration: {
@@ -54,25 +39,26 @@ const config: QuartzConfig = {
},
plugins: {
transformers: [
- new FrontMatter(),
- new Katex(),
- new Description(),
- new CreatedModifiedDate({
+ new Plugin.FrontMatter(),
+ new Plugin.Description(),
+ new Plugin.TableOfContents({ showByDefault: true }),
+ new Plugin.CreatedModifiedDate({
priority: ['frontmatter', 'filesystem'] // you can add 'git' here for last modified from Git but this makes the build slower
}),
- new SyntaxHighlighting(),
- new GitHubFlavoredMarkdown(),
- new ObsidianFlavoredMarkdown(),
- new ResolveLinks(),
+ new Plugin.GitHubFlavoredMarkdown(),
+ new Plugin.ObsidianFlavoredMarkdown(),
+ new Plugin.ResolveLinks(),
+ new Plugin.SyntaxHighlighting(),
+ new Plugin.Katex(),
],
filters: [
- new RemoveDrafts()
+ new Plugin.RemoveDrafts()
],
emitters: [
- new ContentPage({
- head: Head,
- header: [PageTitle, Spacer, Darkmode],
- body: Body
+ new Plugin.ContentPage({
+ head: Component.Head,
+ header: [Component.PageTitle, Component.Spacer, Component.Darkmode],
+ body: [Component.ArticleTitle, Component.ReadingTime, Component.TableOfContents, Component.Content]
})
]
},
diff --git a/quartz/components/ArticleTitle.tsx b/quartz/components/ArticleTitle.tsx
new file mode 100644
index 000000000..02725c670
--- /dev/null
+++ b/quartz/components/ArticleTitle.tsx
@@ -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
{displayTitle}
+ } else {
+ return null
+ }
+}
diff --git a/quartz/components/Body.tsx b/quartz/components/Body.tsx
index 92e66828a..b8ad34b6b 100644
--- a/quartz/components/Body.tsx
+++ b/quartz/components/Body.tsx
@@ -2,13 +2,8 @@ import clipboardScript from './scripts/clipboard.inline'
import clipboardStyle from './styles/clipboard.scss'
import { QuartzComponentProps } from "./types"
-export default function Body({ fileData, children }: QuartzComponentProps) {
- const title = fileData.frontmatter?.title
- const displayTitle = fileData.slug === "index" ? undefined : title
+export default function Body({ children }: QuartzComponentProps) {
return
-
- {displayTitle &&
{displayTitle}
}
-
{children}
}
diff --git a/quartz/components/Content.tsx b/quartz/components/Content.tsx
new file mode 100644
index 000000000..c010f2a88
--- /dev/null
+++ b/quartz/components/Content.tsx
@@ -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
+}
diff --git a/quartz/components/Header.tsx b/quartz/components/Header.tsx
index 8eb2d70df..8b0686305 100644
--- a/quartz/components/Header.tsx
+++ b/quartz/components/Header.tsx
@@ -1,4 +1,3 @@
-import style from './styles/header.scss'
import { QuartzComponentProps } from "./types"
export default function Header({ children }: QuartzComponentProps) {
@@ -7,4 +6,18 @@ export default function Header({ children }: QuartzComponentProps) {
}
-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;
+}
+`
diff --git a/quartz/components/ReadingTime.tsx b/quartz/components/ReadingTime.tsx
new file mode 100644
index 000000000..39110f997
--- /dev/null
+++ b/quartz/components/ReadingTime.tsx
@@ -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 {words} words, {timeTaken}
+ } else {
+ return null
+ }
+}
+
+ReadingTime.css = `
+.reading-time {
+ margin-top: 0;
+ opacity: 0.5;
+}
+`
diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx
new file mode 100644
index 000000000..8192da42d
--- /dev/null
+++ b/quartz/components/TableOfContents.tsx
@@ -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
+ Table of Contents
+
+
+ } else if (position === 'sidebar') {
+ // TODO
+ }
+}
+
+TableOfContents.css = style
diff --git a/quartz/components/index.ts b/quartz/components/index.ts
new file mode 100644
index 000000000..5fde7c3e3
--- /dev/null
+++ b/quartz/components/index.ts
@@ -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
+}
diff --git a/quartz/components/styles/header.scss b/quartz/components/styles/header.scss
deleted file mode 100644
index c3ea48781..000000000
--- a/quartz/components/styles/header.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-header {
- display: flex;
- flex-direction: row;
- align-items: center;
- margin: 1em 0 2em 0;
- & > h1 {
- margin: 0;
- flex: auto;
- }
-}
diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss
new file mode 100644
index 000000000..33b9cca30
--- /dev/null
+++ b/quartz/components/styles/toc.scss
@@ -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});
+ }
+ }
+}
diff --git a/quartz/components/types.ts b/quartz/components/types.ts
index 8d7a79c1f..93f6a4bfc 100644
--- a/quartz/components/types.ts
+++ b/quartz/components/types.ts
@@ -2,12 +2,15 @@ import { ComponentType, JSX } from "preact"
import { StaticResources } from "../resources"
import { QuartzPluginData } from "../plugins/vfile"
import { GlobalConfiguration } from "../cfg"
+import { Node } from "hast"
export type QuartzComponentProps = {
externalResources: StaticResources
fileData: QuartzPluginData
cfg: GlobalConfiguration
children: QuartzComponent[] | JSX.Element[]
+ tree: Node
+ position?: 'sidebar' | 'header' | 'body'
}
export type QuartzComponent = ComponentType & {
diff --git a/quartz/path.ts b/quartz/path.ts
index aa3870b94..bece77048 100644
--- a/quartz/path.ts
+++ b/quartz/path.ts
@@ -1,7 +1,7 @@
import path from 'path'
import SlugAnchor from 'github-slugger'
-const slugAnchor = new SlugAnchor()
+export const slugAnchor = new SlugAnchor()
function slugSegment(s: string): string {
return s.replace(/\s/g, '-')
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index 2ab914cd6..d44b709d8 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -1,19 +1,18 @@
-import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import { StaticResources } from "../../resources"
import { EmitCallback, QuartzEmitterPlugin } from "../types"
import { ProcessedContent } from "../vfile"
-import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { render } from "preact-render-to-string"
import { GlobalConfiguration } from "../../cfg"
import { QuartzComponent } from "../../components/types"
import { resolveToRoot } from "../../path"
import Header from "../../components/Header"
import { QuartzComponentProps } from "../../components/types"
+import Body from "../../components/Body"
interface Options {
head: QuartzComponent
header: QuartzComponent[],
- body: QuartzComponent
+ body: QuartzComponent[]
}
export class ContentPage extends QuartzEmitterPlugin {
@@ -26,17 +25,14 @@ export class ContentPage extends QuartzEmitterPlugin {
}
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 {
const fps: string[] = []
- const { head: Head, header, body: Body } = this.opts
+ const { head: Head, header, body } = this.opts
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 pageResources: StaticResources = {
css: [baseDir + "/index.css", ...resources.css],
@@ -51,7 +47,8 @@ export class ContentPage extends QuartzEmitterPlugin {
fileData: file.data,
externalResources: pageResources,
cfg,
- children: [content]
+ children: [],
+ tree
}
const doc =
@@ -59,10 +56,10 @@ export class ContentPage extends QuartzEmitterPlugin {
- {header.map(HeaderComponent => )}
+ {header.map(HeaderComponent => )}
- {content}
+ {body.map(BodyComponent => )}
diff --git a/quartz/plugins/transformers/description.ts b/quartz/plugins/transformers/description.ts
index fa597993b..b24dd1c1f 100644
--- a/quartz/plugins/transformers/description.ts
+++ b/quartz/plugins/transformers/description.ts
@@ -15,7 +15,7 @@ export class Description extends QuartzTransformerPlugin {
name = "Description"
opts: Options
- constructor(opts?: Options) {
+ constructor(opts?: Partial) {
super()
this.opts = { ...defaultOptions, ...opts }
}
diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts
index 778faac8b..0baec9e78 100644
--- a/quartz/plugins/transformers/frontmatter.ts
+++ b/quartz/plugins/transformers/frontmatter.ts
@@ -17,7 +17,7 @@ export class FrontMatter extends QuartzTransformerPlugin {
name = "FrontMatter"
opts: Options
- constructor(opts?: Options) {
+ constructor(opts?: Partial) {
super()
this.opts = { ...defaultOptions, ...opts }
}
diff --git a/quartz/plugins/transformers/gfm.ts b/quartz/plugins/transformers/gfm.ts
index dd6bdec31..72f987085 100644
--- a/quartz/plugins/transformers/gfm.ts
+++ b/quartz/plugins/transformers/gfm.ts
@@ -19,7 +19,7 @@ export class GitHubFlavoredMarkdown extends QuartzTransformerPlugin {
name = "GitHubFlavoredMarkdown"
opts: Options
- constructor(opts?: Options) {
+ constructor(opts?: Partial) {
super()
this.opts = { ...defaultOptions, ...opts }
}
diff --git a/quartz/plugins/transformers/index.ts b/quartz/plugins/transformers/index.ts
index 492a98826..51aaa3417 100644
--- a/quartz/plugins/transformers/index.ts
+++ b/quartz/plugins/transformers/index.ts
@@ -6,3 +6,4 @@ export { Description } from './description'
export { ResolveLinks } from './links'
export { ObsidianFlavoredMarkdown } from './ofm'
export { SyntaxHighlighting } from './syntax'
+export { TableOfContents } from './toc'
diff --git a/quartz/plugins/transformers/lastmod.ts b/quartz/plugins/transformers/lastmod.ts
index 95b54556d..ef33afeaf 100644
--- a/quartz/plugins/transformers/lastmod.ts
+++ b/quartz/plugins/transformers/lastmod.ts
@@ -16,7 +16,7 @@ export class CreatedModifiedDate extends QuartzTransformerPlugin {
name = "CreatedModifiedDate"
opts: Options
- constructor(opts?: Options) {
+ constructor(opts?: Partial) {
super()
this.opts = {
...defaultOptions,
diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts
index d8248a0fb..4bcbe8277 100644
--- a/quartz/plugins/transformers/links.ts
+++ b/quartz/plugins/transformers/links.ts
@@ -21,7 +21,7 @@ export class ResolveLinks extends QuartzTransformerPlugin {
name = "LinkProcessing"
opts: Options
- constructor(opts?: Options) {
+ constructor(opts?: Partial) {
super()
this.opts = { ...defaultOptions, ...opts }
}
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index 691a13293..23ed37c97 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -93,7 +93,7 @@ export class ObsidianFlavoredMarkdown extends QuartzTransformerPlugin {
name = "ObsidianFlavoredMarkdown"
opts: Options
- constructor(opts?: Options) {
+ constructor(opts?: Partial) {
super()
this.opts = { ...defaultOptions, ...opts }
}
diff --git a/quartz/plugins/transformers/toc.ts b/quartz/plugins/transformers/toc.ts
new file mode 100644
index 000000000..863e3a171
--- /dev/null
+++ b/quartz/plugins/transformers/toc.ts
@@ -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) {
+ 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[]
+ }
+}
+