nested tag support and tag index page
| | |
| | | --- |
| | | tags: |
| | | - plugin/transformer |
| | | --- |
| | | |
| | | Quartz uses [Katex](https://katex.org/) by default to typeset both inline and block math expressions at build time. |
| | | |
| | | ## Formatting |
| | |
| | | --- |
| | | 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. |
| | |
| | | |
| | | ## 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 |
| | |
| | | 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 ?? [] |
| | |
| | | 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 |
| | |
| | | <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"> |
| | |
| | | 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}`) |
| | | } |
| | | } |
| | | |
| | |
| | | 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; |
| | |
| | | import { slug as slugAnchor } from "github-slugger" |
| | | import { slug } from "github-slugger" |
| | | import { trace } from "./trace" |
| | | |
| | | // Quartz Paths |
| | |
| | | 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 { |
| | |
| | | content: transform({ |
| | | filename: "index.css", |
| | | code: Buffer.from(stylesheet), |
| | | minify: true |
| | | minify: true, |
| | | }).code.toString(), |
| | | }), |
| | | emit({ |
| | |
| | | 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) { |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | 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 = { |
| | |
| | | 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" |
| | |
| | | } |
| | | |
| | | // 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 = { |
| | |
| | | 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 |
| | |
| | | |
| | | return { |
| | | type: "link", |
| | | url: base + `/tags/${slugAnchor(tag)}`, |
| | | url: base + `/tags/${slugTag(tag)}`, |
| | | data: { |
| | | hProperties: { |
| | | className: ["tag-link"], |
| | |
| | | --highlight: ${theme.colors.darkMode.highlight}; |
| | | } |
| | | ` |
| | | |
| | | } |