From cee2883c0889a65e2786d70eb81932f5ed017e59 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Wed, 26 Jul 2023 04:10:37 +0000
Subject: [PATCH] nested tag support and tag index page

---
 quartz/components/PageList.tsx                |   13 +++
 quartz/plugins/transformers/frontmatter.ts    |    4 
 quartz/path.ts                                |   22 +++++++
 quartz/components/TagList.tsx                 |    5 -
 quartz/plugins/emitters/componentResources.ts |    2 
 content/features/upcoming features.md         |    2 
 content/features/syntax highlighting.md       |    2 
 quartz/plugins/emitters/tagPage.tsx           |   34 ++++++++---
 quartz/plugins/transformers/ofm.ts            |    4 
 quartz/theme.ts                               |    1 
 content/features/Latex.md                     |    5 +
 quartz/components/pages/TagContent.tsx        |   66 ++++++++++++++++++----
 quartz/components/styles/popover.scss         |    2 
 13 files changed, 125 insertions(+), 37 deletions(-)

diff --git a/content/features/Latex.md b/content/features/Latex.md
index 3f523d6..0dbc113 100644
--- a/content/features/Latex.md
+++ b/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
diff --git a/content/features/syntax highlighting.md b/content/features/syntax highlighting.md
index 16d3fa9..68436c2 100644
--- a/content/features/syntax highlighting.md
+++ b/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.
diff --git a/content/features/upcoming features.md b/content/features/upcoming features.md
index 676bb71..dab75c6 100644
--- a/content/features/upcoming features.md
+++ b/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
diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx
index 8ef40c5..7183acb 100644
--- a/quartz/components/PageList.tsx
+++ b/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 ?? []
diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx
index 0bffb0d..6560035 100644
--- a/quartz/components/TagList.tsx
+++ b/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">
diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx
index 6a3d827..2a9dfca 100644
--- a/quartz/components/pages/TagContent.tsx
+++ b/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))
     }
 
-    // @ts-ignore
-    const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: "html" })
     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}`)
   }
 }
 
diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss
index 3616e51..21e6b72 100644
--- a/quartz/components/styles/popover.scss
+++ b/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;
diff --git a/quartz/path.ts b/quartz/path.ts
index 0d3a0c6..fca2c05 100644
--- a/quartz/path.ts
+++ b/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 {
diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts
index 72a8841..69c9dd5 100644
--- a/quartz/plugins/emitters/componentResources.ts
+++ b/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({
diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx
index 69b0180..2154851 100644
--- a/quartz/plugins/emitters/tagPage.tsx
+++ b/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) => [
-          tag,
-          defaultProcessedContent({
-            slug: `tags/${tag}/index` as ServerSlug,
-            frontmatter: { title: `Tag: ${tag}`, tags: [] },
-          }),
-        ]),
+        [...tags].map((tag) => {
+          const title = tag === "" ? "Tag Index" : `Tag: #${tag}`
+          return [
+            tag,
+            defaultProcessedContent({
+              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 = {
diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts
index 29b0b4b..58ae7c7 100644
--- a/quartz/plugins/transformers/frontmatter.ts
+++ b/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 = {
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index 9840ea8..4a45b02 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/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"],
diff --git a/quartz/theme.ts b/quartz/theme.ts
index b01bfdc..8d7b727 100644
--- a/quartz/theme.ts
+++ b/quartz/theme.ts
@@ -60,5 +60,4 @@
   --highlight: ${theme.colors.darkMode.highlight};
 }
 `
-
 }

--
Gitblit v1.10.0