mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-22 05:55:42 -05:00
fix(links): virtual page links
This commit is contained in:
parent
da7e62a1d8
commit
ed3ff89568
21
docs/features/Bases.md
Normal file
21
docs/features/Bases.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Bases Support
|
||||
tags:
|
||||
- component
|
||||
---
|
||||
|
||||
Quartz supports rendering [Obsidian Bases](https://obsidian.md/changelog/2025-04-15-desktop-v1.8.0/) (`.base` files) as interactive database-like views. Bases files define queries over your vault's notes and display the results in configurable views such as tables, lists, cards, galleries, and boards.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
Bases support is provided by the [[BasesPage]] plugin. See the plugin page for configuration options, built-in views, the expression engine, and how to extend with custom views.
|
||||
|
||||
## Demo
|
||||
|
||||
![[navigation.base]]
|
||||
|
||||
## Customization
|
||||
|
||||
- Install: `npx quartz plugin add github:quartz-community/bases-page`
|
||||
- Source: [`quartz-community/bases-page`](https://github.com/quartz-community/bases-page)
|
||||
21
docs/features/Canvas.md
Normal file
21
docs/features/Canvas.md
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
title: Canvas Support
|
||||
tags:
|
||||
- component
|
||||
---
|
||||
|
||||
Quartz supports rendering [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files as interactive, pannable and zoomable canvas pages. This brings your Obsidian canvas files to the web, preserving text nodes, file references, link nodes, group nodes, and edges with full visual fidelity.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
Canvas support is provided by the [[CanvasPage]] plugin. See the plugin page for configuration options and a full list of supported features.
|
||||
|
||||
## Demo
|
||||
|
||||
![[CanvasPage.canvas]]
|
||||
|
||||
## Customization
|
||||
|
||||
- Install: `npx quartz plugin add github:quartz-community/canvas-page`
|
||||
- Source: [`quartz-community/canvas-page`](https://github.com/quartz-community/canvas-page)
|
||||
@ -15,6 +15,8 @@ Quartz comes with a wide variety of features out of the box. Most features are p
|
||||
- [[OxHugo compatibility]] — Support for ox-hugo Markdown
|
||||
- [[Roam Research compatibility]] — Support for Roam Research syntax
|
||||
- [[Citations]] — Academic citation support
|
||||
- [[Canvas]] — Render Obsidian Canvas files as interactive pages
|
||||
- [[Bases]] — Database-like views for your notes (tables, cards, galleries, and more)
|
||||
|
||||
## Navigation & Discovery
|
||||
|
||||
|
||||
@ -7,10 +7,6 @@ tags:
|
||||
|
||||
This plugin provides support for [Obsidian Bases](https://obsidian.md/changelog/2025-04-15-desktop-v1.8.0/) (`.base` files) in Quartz. It reads `.base` files from your vault, resolves matching notes based on the query definition, and renders them as interactive database-like views with support for tables, lists, cards, and maps.
|
||||
|
||||
See [[navigation.base]] for a live demo showcasing all supported features.
|
||||
|
||||
![[navigation.base]]
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
|
||||
@ -6,10 +6,6 @@ tags:
|
||||
|
||||
This plugin is a page type plugin that renders [JSON Canvas](https://jsoncanvas.org) (`.canvas`) files as interactive, pannable and zoomable canvas pages. It supports the full [JSON Canvas 1.0 spec](https://jsoncanvas.org/spec/1.0/), including text nodes with Markdown rendering, file nodes that link to other pages in your vault, link nodes for external URLs, and group nodes for visual organization. Edges between nodes are rendered as SVG paths with optional labels, arrow markers, and colors.
|
||||
|
||||
See [[CanvasPage.canvas]] for a live demo showcasing all supported features.
|
||||
|
||||
![[canvasPage.canvas]]
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[configuration#Plugins|Configuration]] page.
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { parseMarkdown } from "./processors/parse"
|
||||
import { filterContent } from "./processors/filter"
|
||||
import { emitContent } from "./processors/emit"
|
||||
import cfg from "../quartz"
|
||||
import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
|
||||
import { FilePath, FullSlug, joinSegments, slugifyFilePath } from "./util/path"
|
||||
import chokidar from "chokidar"
|
||||
import { ProcessedContent } from "./plugins/vfile"
|
||||
import { Argv, BuildCtx } from "./util/ctx"
|
||||
@ -19,9 +19,40 @@ import { options } from "./util/sourcemap"
|
||||
import { Mutex } from "async-mutex"
|
||||
import { getStaticResourcesFromPlugins } from "./plugins"
|
||||
import { randomIdNonSecure } from "./util/random"
|
||||
import { ChangeEvent } from "./plugins/types"
|
||||
import { ChangeEvent, QuartzPageTypePluginInstance } from "./plugins/types"
|
||||
import { minimatch } from "minimatch"
|
||||
|
||||
function getPageTypeExtensions(ctx: BuildCtx): Set<string> {
|
||||
const extensions = new Set<string>()
|
||||
const pageTypes = (ctx.cfg.plugins.pageTypes ?? []) as unknown as QuartzPageTypePluginInstance[]
|
||||
for (const pt of pageTypes) {
|
||||
if (pt.fileExtensions) {
|
||||
for (const ext of pt.fileExtensions) {
|
||||
extensions.add(ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
return extensions
|
||||
}
|
||||
|
||||
// For files whose extensions are handled by PageType plugins (e.g. .canvas, .base),
|
||||
// add extension-stripped slug aliases so that wikilink resolution (CrawlLinks) maps
|
||||
// `![[file.canvas]]` to the virtual-page slug `file` instead of the raw `file.canvas`.
|
||||
function addVirtualPageSlugAliases(allSlugs: FullSlug[], extensions: Set<string>): FullSlug[] {
|
||||
const extra: FullSlug[] = []
|
||||
for (const slug of allSlugs) {
|
||||
for (const ext of extensions) {
|
||||
if (slug.endsWith(ext)) {
|
||||
const stripped = slug.slice(0, -ext.length) as FullSlug
|
||||
if (!allSlugs.includes(stripped) && !extra.includes(stripped)) {
|
||||
extra.push(stripped)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
type ContentMap = Map<
|
||||
FilePath,
|
||||
| {
|
||||
@ -83,6 +114,14 @@ async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) {
|
||||
ctx.allFiles = allFiles
|
||||
ctx.allSlugs = allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
||||
|
||||
// Add extension-stripped slug aliases for PageType-registered extensions
|
||||
// so that wikilinks like ![[file.canvas]] resolve to virtual page slugs
|
||||
const ptExtensions = getPageTypeExtensions(ctx)
|
||||
if (ptExtensions.size > 0) {
|
||||
const aliases = addVirtualPageSlugAliases(ctx.allSlugs, ptExtensions)
|
||||
ctx.allSlugs.push(...aliases)
|
||||
}
|
||||
|
||||
const parsedFiles = await parseMarkdown(ctx, filePaths)
|
||||
const filteredContent = filterContent(ctx, parsedFiles)
|
||||
|
||||
@ -257,6 +296,13 @@ async function rebuild(changes: ChangeEvent[], clientRefresh: () => void, buildD
|
||||
// update allFiles and then allSlugs with the consistent view of content map
|
||||
ctx.allFiles = Array.from(contentMap.keys())
|
||||
ctx.allSlugs = ctx.allFiles.map((fp) => slugifyFilePath(fp as FilePath))
|
||||
|
||||
// Add extension-stripped slug aliases for PageType-registered extensions
|
||||
const ptExtensions = getPageTypeExtensions(ctx)
|
||||
if (ptExtensions.size > 0) {
|
||||
const aliases = addVirtualPageSlugAliases(ctx.allSlugs, ptExtensions)
|
||||
ctx.allSlugs.push(...aliases)
|
||||
}
|
||||
let processedFiles = filterContent(
|
||||
ctx,
|
||||
Array.from(contentMap.values())
|
||||
|
||||
@ -107,25 +107,18 @@ export const PageTypeDispatcher: QuartzEmitterPlugin<Partial<DispatcherOptions>>
|
||||
// Ensure trie is available for components that need folder hierarchy (e.g. FolderContent)
|
||||
ctx.trie ??= trieFromAllFiles(allFiles)
|
||||
|
||||
for (const [tree, file] of content) {
|
||||
const slug = file.data.slug!
|
||||
const fileData = file.data
|
||||
|
||||
for (const pt of pageTypes) {
|
||||
if (pt.match({ slug, fileData, cfg })) {
|
||||
const layout = resolveLayout(pt, defaults, byPageType)
|
||||
yield emitPage(ctx, slug, tree, fileData, allFiles, layout, resources)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 1: Generate all virtual pages first so their data is available in allFiles
|
||||
// for transclude resolution in renderPage (e.g. ![[file.canvas]], ![[file.base]])
|
||||
const virtualEntries: Array<{
|
||||
tree: ProcessedContent[0]
|
||||
vfile: ProcessedContent[1]
|
||||
layout: FullPageLayout
|
||||
vpSlug: FullSlug
|
||||
}> = []
|
||||
for (const pt of pageTypes) {
|
||||
if (!pt.generate) continue
|
||||
|
||||
const virtualPages = pt.generate({ content, cfg, ctx })
|
||||
const layout = resolveLayout(pt, defaults, byPageType)
|
||||
|
||||
for (const vp of virtualPages) {
|
||||
const vpSlug = vp.slug as FullSlug
|
||||
const vpRelativePath = (vpSlug + ".md") as FilePath
|
||||
@ -135,16 +128,41 @@ export const PageTypeDispatcher: QuartzEmitterPlugin<Partial<DispatcherOptions>>
|
||||
frontmatter: { title: vp.title, tags: [] },
|
||||
...vp.data,
|
||||
})
|
||||
|
||||
// Expose virtual pages on ctx so other emitters (e.g. ContentIndex) can include them.
|
||||
// Skip special pages like 404 that shouldn't appear in content listings.
|
||||
if (vpSlug !== "404") {
|
||||
ctx.virtualPages.push([tree, vfile])
|
||||
}
|
||||
|
||||
yield emitPage(ctx, vpSlug, tree, vfile.data, allFiles, layout, resources)
|
||||
virtualEntries.push({ tree, vfile, layout, vpSlug })
|
||||
}
|
||||
}
|
||||
|
||||
// Merge virtual page data into allFiles so renderPage can resolve transcludes
|
||||
const allFilesWithVirtual = [...allFiles, ...virtualEntries.map((ve) => ve.vfile.data)]
|
||||
|
||||
// Phase 2: Emit regular pages (with virtual page data available for transclusion)
|
||||
for (const [tree, file] of content) {
|
||||
const slug = file.data.slug!
|
||||
const fileData = file.data
|
||||
for (const pt of pageTypes) {
|
||||
if (pt.match({ slug, fileData, cfg })) {
|
||||
const layout = resolveLayout(pt, defaults, byPageType)
|
||||
yield emitPage(ctx, slug, tree, fileData, allFilesWithVirtual, layout, resources)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Emit virtual pages
|
||||
for (const ve of virtualEntries) {
|
||||
yield emitPage(
|
||||
ctx,
|
||||
ve.vpSlug,
|
||||
ve.tree,
|
||||
ve.vfile.data,
|
||||
allFilesWithVirtual,
|
||||
ve.layout,
|
||||
resources,
|
||||
)
|
||||
}
|
||||
},
|
||||
async *partialEmit(ctx, content, resources, changeEvents) {
|
||||
const pageTypes = [...getPageTypes(ctx)].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
|
||||
@ -162,26 +180,17 @@ export const PageTypeDispatcher: QuartzEmitterPlugin<Partial<DispatcherOptions>>
|
||||
}
|
||||
}
|
||||
|
||||
for (const [tree, file] of content) {
|
||||
const slug = file.data.slug!
|
||||
if (!changedSlugs.has(slug)) continue
|
||||
|
||||
const fileData = file.data
|
||||
for (const pt of pageTypes) {
|
||||
if (pt.match({ slug, fileData, cfg })) {
|
||||
const layout = resolveLayout(pt, defaults, byPageType)
|
||||
yield emitPage(ctx, slug, tree, fileData, allFiles, layout, resources)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 1: Generate all virtual pages first so their data is available in allFiles
|
||||
const virtualEntries: Array<{
|
||||
tree: ProcessedContent[0]
|
||||
vfile: ProcessedContent[1]
|
||||
layout: FullPageLayout
|
||||
vpSlug: FullSlug
|
||||
}> = []
|
||||
for (const pt of pageTypes) {
|
||||
if (!pt.generate) continue
|
||||
|
||||
const virtualPages = pt.generate({ content, cfg, ctx })
|
||||
const layout = resolveLayout(pt, defaults, byPageType)
|
||||
|
||||
for (const vp of virtualPages) {
|
||||
const vpSlug = vp.slug as FullSlug
|
||||
const vpRelativePath = (vpSlug + ".md") as FilePath
|
||||
@ -191,16 +200,43 @@ export const PageTypeDispatcher: QuartzEmitterPlugin<Partial<DispatcherOptions>>
|
||||
frontmatter: { title: vp.title, tags: [] },
|
||||
...vp.data,
|
||||
})
|
||||
|
||||
// Expose virtual pages on ctx so other emitters (e.g. ContentIndex) can include them.
|
||||
// Skip special pages like 404 that shouldn't appear in content listings.
|
||||
if (vpSlug !== "404") {
|
||||
ctx.virtualPages.push([tree, vfile])
|
||||
}
|
||||
|
||||
yield emitPage(ctx, vpSlug, tree, vfile.data, allFiles, layout, resources)
|
||||
virtualEntries.push({ tree, vfile, layout, vpSlug })
|
||||
}
|
||||
}
|
||||
|
||||
// Merge virtual page data into allFiles for transclude resolution
|
||||
const allFilesWithVirtual = [...allFiles, ...virtualEntries.map((ve) => ve.vfile.data)]
|
||||
|
||||
// Phase 2: Emit changed regular pages
|
||||
for (const [tree, file] of content) {
|
||||
const slug = file.data.slug!
|
||||
if (!changedSlugs.has(slug)) continue
|
||||
|
||||
const fileData = file.data
|
||||
for (const pt of pageTypes) {
|
||||
if (pt.match({ slug, fileData, cfg })) {
|
||||
const layout = resolveLayout(pt, defaults, byPageType)
|
||||
yield emitPage(ctx, slug, tree, fileData, allFilesWithVirtual, layout, resources)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Emit virtual pages
|
||||
for (const ve of virtualEntries) {
|
||||
yield emitPage(
|
||||
ctx,
|
||||
ve.vpSlug,
|
||||
ve.tree,
|
||||
ve.vfile.data,
|
||||
allFilesWithVirtual,
|
||||
ve.layout,
|
||||
resources,
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user