Jacky Zhao
2023-07-26 cee2883c0889a65e2786d70eb81932f5ed017e59
nested tag support and tag index page
13 files modified
152 ■■■■ changed files
content/features/Latex.md 5 ●●●●● patch | view | raw | blame | history
content/features/syntax highlighting.md 2 ●●●●● patch | view | raw | blame | history
content/features/upcoming features.md 2 ●●●●● patch | view | raw | blame | history
quartz/components/PageList.tsx 13 ●●●● patch | view | raw | blame | history
quartz/components/TagList.tsx 5 ●●●●● patch | view | raw | blame | history
quartz/components/pages/TagContent.tsx 62 ●●●● patch | view | raw | blame | history
quartz/components/styles/popover.scss 2 ●●● patch | view | raw | blame | history
quartz/path.ts 22 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/componentResources.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/emitters/tagPage.tsx 28 ●●●● patch | view | raw | blame | history
quartz/plugins/transformers/frontmatter.ts 4 ●●●● patch | view | raw | blame | history
quartz/plugins/transformers/ofm.ts 4 ●●●● patch | view | raw | blame | history
quartz/theme.ts 1 ●●●● patch | view | raw | blame | history
content/features/Latex.md
@@ -1,3 +1,8 @@
---
tags:
  - plugin/transformer
---
Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time.
## Formatting
content/features/syntax highlighting.md
@@ -1,5 +1,7 @@
---
title: Syntax Highlighting
tags:
  - plugin/transformer
---
Syntax highlighting in Quartz is completely done at build-time. This means that Quartz only ships pre-calculated CSS to highlight the right words so there is no heavy client-side bundle that does the syntax highlighting.
content/features/upcoming features.md
@@ -4,9 +4,7 @@
## high priority
- local fonts
- images in same folder are broken on shortest path mode
- https://help.obsidian.md/Editing+and+formatting/Tags#Nested+tags nested tags?? and big tag listing
- watch mode for config/source code
- https://help.obsidian.md/Editing+and+formatting/Basic+formatting+syntax#Task+lists task list styling
- publish metadata https://help.obsidian.md/Editing+and+formatting/Metadata#Metadata+for+Obsidian+Publish
quartz/components/PageList.tsx
@@ -20,11 +20,20 @@
  return f1Title.localeCompare(f2Title)
}
export function PageList({ fileData, allFiles }: QuartzComponentProps) {
type Props = {
  limit?: number
} & QuartzComponentProps
export function PageList({ fileData, allFiles, limit }: Props) {
  const slug = canonicalizeServer(fileData.slug!)
  let list = allFiles.sort(byDateAndAlphabetical)
  if (limit) {
    list = list.slice(0, limit)
  }
  return (
    <ul class="section-ul">
      {allFiles.sort(byDateAndAlphabetical).map((page) => {
      {list.map((page) => {
        const title = page.frontmatter?.title
        const pageSlug = canonicalizeServer(page.slug!)
        const tags = page.frontmatter?.tags ?? []
quartz/components/TagList.tsx
@@ -1,6 +1,5 @@
import { canonicalizeServer, pathToRoot } from "../path"
import { canonicalizeServer, pathToRoot, slugTag } from "../path"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { slug as slugAnchor } from "github-slugger"
function TagList({ fileData }: QuartzComponentProps) {
  const tags = fileData.frontmatter?.tags
@@ -11,7 +10,7 @@
      <ul class="tags">
        {tags.map((tag) => {
          const display = `#${tag}`
          const linkDest = baseDir + `/tags/${slugAnchor(tag)}`
          const linkDest = baseDir + `/tags/${slugTag(tag)}`
          return (
            <li>
              <a href={linkDest} class="internal tag-link">
quartz/components/pages/TagContent.tsx
@@ -3,33 +3,75 @@
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import style from "../styles/listPage.scss"
import { PageList } from "../PageList"
import { ServerSlug, canonicalizeServer } from "../../path"
import { ServerSlug, canonicalizeServer, getAllSegmentPrefixes } from "../../path"
import { QuartzPluginData } from "../../plugins/vfile"
const numPages = 10
function TagContent(props: QuartzComponentProps) {
  const { tree, fileData, allFiles } = props
  const slug = fileData.slug
  if (slug?.startsWith("tags/")) {
    const tag = canonicalizeServer(slug.slice("tags/".length) as ServerSlug)
    const allPagesWithTag = allFiles.filter((file) => (file.frontmatter?.tags ?? []).includes(tag))
    const listProps = {
      ...props,
      allFiles: allPagesWithTag,
  if (!slug?.startsWith("tags/")) {
    throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
    }
  const tag = canonicalizeServer(slug.slice("tags/".length) as ServerSlug)
  const allPagesWithTag = (tag: string) =>
    allFiles.filter((file) =>
      (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
    )
    // @ts-ignore
    const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
  if (tag === "") {
    const tags = [...new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))]
    const tagItemMap: Map<string, QuartzPluginData[]> = new Map()
    for (const tag of tags) {
      tagItemMap.set(tag, allPagesWithTag(tag))
    }
    return (
      <div class="popover-hint">
        <article>{content}</article>
        <p>{allPagesWithTag.length} items with this tag.</p>
        <p>Found {tags.length} total tags.</p>
        <div>
          {tags.map((tag) => {
            const pages = tagItemMap.get(tag)!
            const listProps = {
              ...props,
              allFiles: pages,
            }
            return (
              <div>
                <h2>
                  <a class="internal tag-link" href={`./tags/${tag}`}>
                    #{tag}
                  </a>
                </h2>
                <p>{pages.length} items with this tag. {pages.length > numPages && `Showing first ${numPages}.`}</p>
                <PageList limit={numPages} {...listProps} />
              </div>
            )
          })}
        </div>
      </div>
    )
  } else {
    const pages = allPagesWithTag(tag)
    const listProps = {
      ...props,
      allFiles: pages,
    }
    return (
      <div class="popover-hint">
        <article>{content}</article>
        <p>{pages.length} items with this tag.</p>
        <div>
          <PageList {...listProps} />
        </div>
      </div>
    )
  } else {
    throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
  }
}
quartz/components/styles/popover.scss
@@ -24,7 +24,7 @@
    position: relative;
    width: 30rem;
    max-height: 20rem;
    padding: 0 1rem 2rem 1rem;
    padding: 0 1rem 1rem 1rem;
    font-weight: initial;
    line-height: normal;
    font-size: initial;
quartz/path.ts
@@ -1,4 +1,4 @@
import { slug as slugAnchor } from "github-slugger"
import { slug } from "github-slugger"
import { trace } from "./trace"
// Quartz Paths
@@ -197,10 +197,30 @@
  return [fp, anchor]
}
export function slugAnchor(anchor: string) {
  return slug(anchor)
}
export function slugTag(tag: string) {
  return tag
    .split("/")
    .map((tagSegment) => slug(tagSegment))
    .join("/")
}
export function joinSegments(...args: string[]): string {
  return args.filter((segment) => segment !== "").join("/")
}
export function getAllSegmentPrefixes(tags: string): string[] {
  const segments = tags.split("/")
  const results: string[] = []
  for (let i = 0; i < segments.length; i++) {
    results.push(segments.slice(0, i + 1).join("/"))
  }
  return results
}
export const QUARTZ = "quartz"
function _canonicalize(fp: string): string {
quartz/plugins/emitters/componentResources.ts
@@ -160,7 +160,7 @@
          content: transform({
            filename: "index.css",
            code: Buffer.from(stylesheet),
            minify: true
            minify: true,
          }).code.toString(),
        }),
        emit({
quartz/plugins/emitters/tagPage.tsx
@@ -5,7 +5,13 @@
import { pageResources, renderPage } from "../../components/renderPage"
import { ProcessedContent, defaultProcessedContent } from "../vfile"
import { FullPageLayout } from "../../cfg"
import { CanonicalSlug, FilePath, ServerSlug, joinSegments } from "../../path"
import {
  CanonicalSlug,
  FilePath,
  ServerSlug,
  getAllSegmentPrefixes,
  joinSegments,
} from "../../path"
export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
  if (!opts) {
@@ -26,15 +32,23 @@
      const allFiles = content.map((c) => c[1].data)
      const cfg = ctx.cfg.configuration
      const tags: Set<string> = new Set(allFiles.flatMap((data) => data.frontmatter?.tags ?? []))
      const tags: Set<string> = new Set(
        allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
      )
      // add base tag
      tags.add("")
      const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
        [...tags].map((tag) => [
        [...tags].map((tag) => {
          const title = tag === "" ? "Tag Index" : `Tag: #${tag}`
          return [
          tag,
          defaultProcessedContent({
            slug: `tags/${tag}/index` as ServerSlug,
            frontmatter: { title: `Tag: ${tag}`, tags: [] },
              slug: joinSegments("tags", tag, "index") as ServerSlug,
              frontmatter: { title, tags: [] },
          }),
        ]),
          ]
        }),
      )
      for (const [tree, file] of content) {
@@ -48,7 +62,7 @@
      }
      for (const tag of tags) {
        const slug = `tags/${tag}/index` as CanonicalSlug
        const slug = joinSegments("tags", tag) as CanonicalSlug
        const externalResources = pageResources(slug, resources)
        const [tree, file] = tagDescriptions[tag]
        const componentData: QuartzComponentProps = {
quartz/plugins/transformers/frontmatter.ts
@@ -2,7 +2,7 @@
import remarkFrontmatter from "remark-frontmatter"
import { QuartzTransformerPlugin } from "../types"
import yaml from "js-yaml"
import { slug as slugAnchor } from "github-slugger"
import { slugTag } from "../../path"
export interface Options {
  language: "yaml" | "toml"
@@ -43,7 +43,7 @@
            }
            // slug them all!!
            data.tags = data.tags?.map((tag: string) => slugAnchor(tag)) ?? []
            data.tags = data.tags?.map((tag: string) => slugTag(tag)) ?? []
            // fill in frontmatter
            file.data.frontmatter = {
quartz/plugins/transformers/ofm.ts
@@ -9,7 +9,7 @@
import { JSResource } from "../../resources"
// @ts-ignore
import calloutScript from "../../components/scripts/callout.inline.ts"
import { FilePath, canonicalizeServer, pathToRoot, slugifyFilePath } from "../../path"
import { FilePath, canonicalizeServer, pathToRoot, slugTag, slugifyFilePath } from "../../path"
export interface Options {
  comments: boolean
@@ -337,7 +337,7 @@
              return {
                type: "link",
                url: base + `/tags/${slugAnchor(tag)}`,
                url: base + `/tags/${slugTag(tag)}`,
                data: {
                  hProperties: {
                    className: ["tag-link"],
quartz/theme.ts
@@ -60,5 +60,4 @@
  --highlight: ${theme.colors.darkMode.highlight};
}
`
}