toc
Jacky Zhao
2023-06-10 b8c011410d6bcd6837f4efd6a3948196a0f7aeea
toc
1 files deleted
7 files added
13 files modified
299 ■■■■ changed files
quartz.config.ts 46 ●●●●● patch | view | raw | blame | history
quartz/components/ArticleTitle.tsx 11 ●●●●● patch | view | raw | blame | history
quartz/components/Body.tsx 7 ●●●● patch | view | raw | blame | history
quartz/components/Content.tsx 9 ●●●●● patch | view | raw | blame | history
quartz/components/Header.tsx 17 ●●●● patch | view | raw | blame | history
quartz/components/ReadingTime.tsx 20 ●●●●● patch | view | raw | blame | history
quartz/components/TableOfContents.tsx 24 ●●●●● patch | view | raw | blame | history
quartz/components/index.ts 19 ●●●●● patch | view | raw | blame | history
quartz/components/styles/header.scss 10 ●●●●● patch | view | raw | blame | history
quartz/components/styles/toc.scss 27 ●●●●● patch | view | raw | blame | history
quartz/components/types.ts 3 ●●●●● patch | view | raw | blame | history
quartz/path.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/emitters/contentPage.tsx 19 ●●●●● patch | view | raw | blame | history
quartz/plugins/transformers/description.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/transformers/frontmatter.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/transformers/gfm.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/transformers/index.ts 1 ●●●● patch | view | raw | blame | history
quartz/plugins/transformers/lastmod.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/transformers/links.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/transformers/ofm.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/transformers/toc.ts 72 ●●●●● patch | view | raw | blame | history
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 @@
  },
  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]
      })
    ]
  },
quartz/components/ArticleTitle.tsx
New 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
  }
}
quartz/components/Body.tsx
@@ -2,13 +2,8 @@
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 <article>
    <div class="top-section">
      {displayTitle && <h1>{displayTitle}</h1>}
    </div>
    {children}
  </article>
}
quartz/components/Content.tsx
New 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
}
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 @@
  </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;
}
`
quartz/components/ReadingTime.tsx
New 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;
}
`
quartz/components/TableOfContents.tsx
New 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
quartz/components/index.ts
New 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
}
quartz/components/styles/header.scss
File was deleted
quartz/components/styles/toc.scss
New 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});
    }
  }
}
quartz/components/types.ts
@@ -2,12 +2,15 @@
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<QuartzPluginData>
  position?: 'sidebar' | 'header' | 'body'
}
export type QuartzComponent = ComponentType<QuartzComponentProps> & {
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, '-')
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 @@
  }
  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[]> {
    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 @@
        fileData: file.data,
        externalResources: pageResources,
        cfg,
        children: [content]
        children: [],
        tree
      }
      const doc = <html>
@@ -59,10 +56,10 @@
        <body>
          <div id="quartz-root" class="page">
            <Header {...componentData} >
              {header.map(HeaderComponent => <HeaderComponent {...componentData}/>)}
              {header.map(HeaderComponent => <HeaderComponent {...componentData} position="header" />)}
            </Header>
            <Body {...componentData}>
              {content}
              {body.map(BodyComponent => <BodyComponent {...componentData } position="body" />)}
            </Body>
          </div>
        </body>
quartz/plugins/transformers/description.ts
@@ -15,7 +15,7 @@
  name = "Description"
  opts: Options
  constructor(opts?: Options) {
  constructor(opts?: Partial<Options>) {
    super()
    this.opts = { ...defaultOptions, ...opts }
  }
quartz/plugins/transformers/frontmatter.ts
@@ -17,7 +17,7 @@
  name = "FrontMatter"
  opts: Options
  constructor(opts?: Options) {
  constructor(opts?: Partial<Options>) {
    super()
    this.opts = { ...defaultOptions, ...opts }
  }
quartz/plugins/transformers/gfm.ts
@@ -19,7 +19,7 @@
  name = "GitHubFlavoredMarkdown"
  opts: Options
  constructor(opts?: Options) {
  constructor(opts?: Partial<Options>) {
    super()
    this.opts = { ...defaultOptions, ...opts }
  }
quartz/plugins/transformers/index.ts
@@ -6,3 +6,4 @@
export { ResolveLinks } from './links'
export { ObsidianFlavoredMarkdown } from './ofm'
export { SyntaxHighlighting } from './syntax'
export { TableOfContents } from './toc'
quartz/plugins/transformers/lastmod.ts
@@ -16,7 +16,7 @@
  name = "CreatedModifiedDate"
  opts: Options
  constructor(opts?: Options) {
  constructor(opts?: Partial<Options>) {
    super()
    this.opts = {
      ...defaultOptions,
quartz/plugins/transformers/links.ts
@@ -21,7 +21,7 @@
  name = "LinkProcessing"
  opts: Options
  constructor(opts?: Options) {
  constructor(opts?: Partial<Options>) {
    super()
    this.opts = { ...defaultOptions, ...opts }
  }
quartz/plugins/transformers/ofm.ts
@@ -93,7 +93,7 @@
  name = "ObsidianFlavoredMarkdown"
  opts: Options
  constructor(opts?: Options) {
  constructor(opts?: Partial<Options>) {
    super()
    this.opts = { ...defaultOptions, ...opts }
  }
quartz/plugins/transformers/toc.ts
New 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[]
  }
}