@quartz-community/bases-page 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - `options` field on `ViewTypeRegistration` — community plugins can pass user-supplied configuration (e.g. feature flags) that gets forwarded to every render invocation via `ViewRendererProps.options`.
13
+ - `options` field on `ViewRendererProps` — view renderers receive registration-time options as `Record<string, unknown>`.
14
+ - `slug`, `allSlugs`, and `linkResolution` fields on `ViewRendererProps` — view renderers can now resolve internal links correctly.
15
+ - `css` and `afterDOMLoaded` fields on `ViewTypeRegistration` — community view types can inject per-page styles and client-side scripts.
16
+ - `linkResolution` plugin option (`"absolute" | "relative" | "shortest"`) — controls how internal links are resolved in view renderers, matching the crawl-links plugin setting.
17
+
18
+ ### Fixed
19
+
20
+ - Added `yaml` and `unist-util-visit` to devDependencies (were listed as peer dependencies but missing from devDependencies, causing `tsc --noEmit` failures).
21
+ - Respect the `file.data.unlisted` convention in `resolveBasesEntries`. Pages marked `unlisted: true` are now excluded from every rendered base view (table, list, board, cards, gallery, and any custom view registered via `viewRegistry`), regardless of whether the base's filter expression would otherwise match them. Fixes a privacy leak where encrypted-unlisted and plain-unlisted pages showed up in base views built from queries like `file.ext == "md"` that matched the entire content directory. Both the main entry loop and the internal `fileLookup` used by `.asFile()` method resolution are filtered, so unlisted pages also cannot be dereferenced from formulas on visible pages. The convention is the same one respected by `content-index`, `search`, `backlinks`, `recent-notes`, `folder-page`, and `tag-page`, and is written by `@quartz-community/unlisted-pages` and `@quartz-community/encrypted-pages` (when `unlistWhenEncrypted` or per-page `unlisted: true`/`stealth: true` is set).
22
+
23
+ ## [0.2.0]
24
+
25
+ ### Added
26
+
27
+ - Initial Quartz community plugin template.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quartz Community
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,315 @@
1
+ # @quartz-community/bases-page
2
+
3
+ A page type and component plugin for Quartz v5 that renders Obsidian Bases (.base files) as database-like filtered views. This plugin (v0.2.0) provides a powerful way to visualize and interact with your Obsidian data in a structured format.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx quartz plugin add github:quartz-community/bases-page
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Enable the plugin in your Quartz configuration:
14
+
15
+ ```yaml title="quartz.config.yaml"
16
+ plugins:
17
+ - source: github:quartz-community/bases-page
18
+ enabled: true
19
+ ```
20
+
21
+ ## Interaction with `unlisted` pages
22
+
23
+ `bases-page` respects the `file.data.unlisted` convention. Pages marked `unlisted: true` (by `@quartz-community/unlisted-pages`, or by `@quartz-community/encrypted-pages` via `unlistWhenEncrypted: true` or per-page `unlisted: true` / `stealth: true`) are excluded from every rendered base view — table, list, board, cards, gallery, and any custom view registered via `viewRegistry`. Both the main entry loop and the internal `fileLookup` used by `.asFile()` method resolution skip unlisted pages, so they cannot be dereferenced from formulas on visible pages either.
24
+
25
+ > [!note]
26
+ > Base views are **server-side rendered** HTML baked at build time. They do not update client-side after a visitor successfully decrypts an encrypted page. Graph, explorer, and search all re-hydrate from the patched in-memory content index after decryption and show newly-unlocked pages for the rest of the browser session — base views do not, because they were materialized at build time with unlisted pages already excluded. A visitor who decrypts a revealable encrypted page will see it appear in graph, explorer, and search, but **not** in any base view on the site, until the site is rebuilt with that page listed. This is the same structural limitation that applies to backlinks, recent notes, folder listings, and tag listings.
27
+
28
+ ### TypeScript Override
29
+
30
+ For advanced use cases (e.g. custom view renderers), you can override in TypeScript:
31
+
32
+ ```ts title="quartz.ts (override)"
33
+ import * as ExternalPlugin from "./.quartz/plugins";
34
+
35
+ ExternalPlugin.BasesPage({
36
+ defaultViewType: "table",
37
+ linkResolution: "shortest",
38
+ customViews: {
39
+ myView: ({
40
+ entries,
41
+ view,
42
+ basesData,
43
+ total,
44
+ locale,
45
+ slug,
46
+ allSlugs,
47
+ linkResolution,
48
+ options,
49
+ }) => {
50
+ // return JSX
51
+ },
52
+ },
53
+ });
54
+ ```
55
+
56
+ ## Built-in Views
57
+
58
+ - **Table**: Sortable columns with automatic type rendering for strings, numbers, booleans, arrays, and links. Supports custom column widths and row heights.
59
+ - **List**: A compact list view displaying metadata chips for each entry.
60
+ - **Cards**: A card-based layout with support for an optional image property and configurable card size or aspect ratio.
61
+ - **Gallery**: An image-focused grid layout designed for visual content with configurable card sizes.
62
+ - **Board**: A Kanban-style board that groups entries by a specific property (using boardProperty or groupBy), creating columns for each group value.
63
+
64
+ ## Features
65
+
66
+ - Multiple views per .base file (switchable tabs)
67
+ - Recursive filter trees (and/or/not)
68
+ - Formulas (computed properties via expression engine)
69
+ - Summaries (column aggregations: Count, Average, Min, Max, Sum, Range, Median, Stddev, Earliest, Latest, Checked, Unchecked, Empty, Filled, Unique)
70
+ - Property display configuration
71
+ - Wikilink and Markdown link rendering
72
+ - Sort and grouping
73
+
74
+ ## Expression Engine
75
+
76
+ The expression engine follows a pipeline of source, lexer, Pratt parser, AST, bytecode compiler, and stack interpreter.
77
+
78
+ ### Supported Types
79
+
80
+ - Strings
81
+ - Numbers
82
+ - Booleans
83
+ - Null
84
+ - Lists
85
+
86
+ ### Operators
87
+
88
+ - **Arithmetic**: +, -, \*, /
89
+ - **Comparison**: ==, !=, >, <, >=, <=
90
+ - **Logical**: and / &&, or / ||, not / !
91
+
92
+ ### Property Access
93
+
94
+ - `note.property`: Access a property on the current note.
95
+ - `file.name`: The name of the file.
96
+ - `file.path`: The full path of the file.
97
+ - `file.folder`: The folder containing the file.
98
+ - `file.ext`: The file extension.
99
+ - `file.tags`: A list of tags associated with the file.
100
+ - `file.links`: A list of links in the file.
101
+ - `file.created`: The creation date of the file.
102
+ - `file.modified`: The last modification date of the file.
103
+ - `formula.name`: Access the result of another formula.
104
+
105
+ ### Built-in Global Functions
106
+
107
+ - `if(condition, then, else)`
108
+ - `contains(container, item)`
109
+ - `date(string)`
110
+ - `duration(string)`
111
+ - `now()`
112
+ - `today()`
113
+ - `number(value)`
114
+ - `min(a, b, ...)`
115
+ - `max(a, b, ...)`
116
+ - `list(value)`
117
+ - `link(path, display?)`
118
+ - `image(url)`
119
+ - `icon(name)`
120
+ - `html(string)`
121
+ - `escapeHTML(string)`
122
+ - `file(path)`
123
+
124
+ ### Method Functions
125
+
126
+ **Strings**
127
+
128
+ - `contains(substring)`
129
+ - `startsWith(prefix)`
130
+ - `endsWith(suffix)`
131
+ - `lower()`
132
+ - `upper()`
133
+ - `trim()`
134
+ - `replace(pattern, replacement)`
135
+ - `slice(start, end?)`
136
+ - `isEmpty()`
137
+ - `repeat(count)`
138
+ - `reverse()`
139
+
140
+ **Numbers**
141
+
142
+ - `toFixed(digits)`
143
+ - `round()`
144
+ - `floor()`
145
+ - `ceil()`
146
+ - `abs()`
147
+
148
+ **Dates**
149
+
150
+ - `format(formatString)`
151
+ - `year()`
152
+ - `month()`
153
+ - `day()`
154
+ - `time()`
155
+ - `relative()`
156
+ - `isEmpty()`
157
+
158
+ **Lists**
159
+
160
+ - `sum()`
161
+ - `mean()`
162
+ - `count()`
163
+ - `min()`
164
+ - `max()`
165
+ - `round()`
166
+
167
+ **File**
168
+
169
+ - `hasTag(tag)`
170
+ - `hasLink(link)`
171
+ - `inFolder(folder)`
172
+ - `hasProperty(property)`
173
+
174
+ ## Extending with Custom Views
175
+
176
+ Community extensions can register custom view types using the view registry.
177
+
178
+ ### Registering a View
179
+
180
+ ```typescript
181
+ import { viewRegistry } from "@quartz-community/bases-page/registry";
182
+
183
+ viewRegistry.register({
184
+ id: "my-custom-view",
185
+ name: "My Custom View",
186
+ icon: "star",
187
+ render: (props) => {
188
+ const { entries, view, basesData, total, locale, slug, allSlugs, linkResolution, options } =
189
+ props;
190
+ // Return your React/Preact component
191
+ },
192
+ css: `.my-custom-view { color: red; }`,
193
+ afterDOMLoaded: `console.log("My custom view loaded");`,
194
+ options: { enableFeatureX: true },
195
+ });
196
+ ```
197
+
198
+ ### Interfaces
199
+
200
+ **ViewTypeRegistration**
201
+
202
+ ```typescript
203
+ interface ViewTypeRegistration {
204
+ id: string;
205
+ name: string;
206
+ icon?: string;
207
+ render: ViewRenderer;
208
+ css?: string;
209
+ afterDOMLoaded?: string;
210
+ options?: Record<string, unknown>;
211
+ }
212
+ ```
213
+
214
+ **ViewRendererProps**
215
+
216
+ ```typescript
217
+ interface ViewRendererProps {
218
+ entries: BasesEntry[];
219
+ view: BasesView;
220
+ basesData: BasesData;
221
+ total: number;
222
+ locale: string;
223
+ slug: string;
224
+ allSlugs: string[];
225
+ linkResolution: "absolute" | "relative" | "shortest";
226
+ options?: Record<string, unknown>;
227
+ }
228
+ ```
229
+
230
+ ### Passing Options to a Custom View
231
+
232
+ Community plugins can use the `options` field to accept configuration at registration time. This is useful for feature flags, API keys, or other plugin-level settings.
233
+
234
+ ```typescript
235
+ import { viewRegistry } from "@quartz-community/bases-page/registry";
236
+ import type { LeafletMapPluginOptions } from "./types";
237
+
238
+ const defaultOptions: LeafletMapPluginOptions = {
239
+ enableCopyTool: false,
240
+ };
241
+
242
+ export function registerLeafletMap(userOpts?: Partial<LeafletMapPluginOptions>): void {
243
+ const opts = { ...defaultOptions, ...userOpts };
244
+ viewRegistry.register({
245
+ ...leafletMapViewRegistration,
246
+ options: opts,
247
+ });
248
+ }
249
+ ```
250
+
251
+ In the renderer, these options are available via `props.options` and should be cast to the plugin's options type.
252
+
253
+ ## Compiler API
254
+
255
+ The expression engine is available as a public API for advanced usage. Import from `@quartz-community/bases-page/compiler`.
256
+
257
+ ```typescript
258
+ import {
259
+ compile,
260
+ evaluate,
261
+ evaluateFilter,
262
+ resolvePropertyValue,
263
+ } from "@quartz-community/bases-page/compiler";
264
+
265
+ // Compile an expression to bytecode
266
+ const compiled = compile("note.price * 1.2");
267
+
268
+ // Evaluate an expression in a context
269
+ const result = evaluate("note.price * 1.2", context);
270
+
271
+ // Evaluate a filter node
272
+ const isMatch = evaluateFilter(filterNode, context);
273
+
274
+ // Resolve a property path
275
+ const value = resolvePropertyValue("file.name", context);
276
+ ```
277
+
278
+ ### EvalContext Shape
279
+
280
+ ```typescript
281
+ interface EvalContext {
282
+ note: Record<string, unknown>;
283
+ file: {
284
+ name: string;
285
+ path: string;
286
+ folder: string;
287
+ ext: string;
288
+ tags: string[];
289
+ links: string[];
290
+ created?: string;
291
+ modified?: string;
292
+ };
293
+ formula: Record<string, unknown>;
294
+ }
295
+ ```
296
+
297
+ ## Package Exports
298
+
299
+ - `.`: Main plugin entry (BasesPage, BasesBody, viewRegistry, registerCustomViews, compiler API, types)
300
+ - `./types`: All type definitions
301
+ - `./components`: React/Preact components (BasesBody, ViewSelector)
302
+ - `./registry`: View registry singleton and registerCustomViews helper
303
+ - `./compiler`: Expression engine (compile, evaluate, evaluateFilter, resolvePropertyValue)
304
+
305
+ ## Configuration Options
306
+
307
+ | Option | Type | Default | Description |
308
+ | :---------------- | :--------------------------------------- | :----------- | :--------------------------------------------------------------------------------------------- |
309
+ | `defaultViewType` | `string` | `"table"` | The default view type used when none is specified in the .base file. |
310
+ | `linkResolution` | `"absolute" \| "relative" \| "shortest"` | `"shortest"` | How to resolve internal links in view renderers. Should match your crawl-links plugin setting. |
311
+ | `customViews` | `Record<string, ViewRenderer>` | `{}` | Custom view renderers provided via TypeScript override. |
312
+
313
+ ## License
314
+
315
+ MIT
@@ -0,0 +1,6 @@
1
+ import { QuartzComponent } from '@quartz-community/types';
2
+ import { BasesPageOptions } from './types.js';
3
+
4
+ declare const _default: (opts?: BasesPageOptions) => QuartzComponent;
5
+
6
+ export { _default as _ };
@@ -0,0 +1,44 @@
1
+ import { createRequire } from 'module';
2
+
3
+ createRequire(import.meta.url);
4
+
5
+ // src/registry.ts
6
+ var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@quartz-community/bases-page/viewRegistry");
7
+ var ViewRegistry = class {
8
+ views = /* @__PURE__ */ new Map();
9
+ /**
10
+ * Register a view type. If a view with the same ID already exists it is
11
+ * silently replaced — this lets config-level `customViews` override built-in
12
+ * renderers for the same type.
13
+ */
14
+ register(registration) {
15
+ this.views.set(registration.id, registration);
16
+ }
17
+ /** Look up a registered view type by ID. */
18
+ get(id) {
19
+ return this.views.get(id);
20
+ }
21
+ /** Return all registered view types (insertion order). */
22
+ getAll() {
23
+ return Array.from(this.views.values());
24
+ }
25
+ /** Check whether a view type is registered. */
26
+ has(id) {
27
+ return this.views.has(id);
28
+ }
29
+ /** Remove a view type registration. Returns true if it existed. */
30
+ unregister(id) {
31
+ return this.views.delete(id);
32
+ }
33
+ };
34
+ var g = globalThis;
35
+ var viewRegistry = g[REGISTRY_KEY] ??= new ViewRegistry();
36
+ function registerCustomViews(customs) {
37
+ for (const [id, render] of Object.entries(customs)) {
38
+ viewRegistry.register({ id, name: id, render });
39
+ }
40
+ }
41
+
42
+ export { registerCustomViews, viewRegistry };
43
+ //# sourceMappingURL=chunk-2AUMER56.js.map
44
+ //# sourceMappingURL=chunk-2AUMER56.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/registry.ts"],"names":[],"mappings":";;;;;AAWA,IAAM,YAAA,mBAAe,MAAA,CAAO,GAAA,CAAI,2CAA2C,CAAA;AAqB3E,IAAM,eAAN,MAAmB;AAAA,EACT,KAAA,uBAAY,GAAA,EAAkC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOtD,SAAS,YAAA,EAA0C;AACjD,IAAA,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,YAAA,CAAa,EAAA,EAAI,YAAY,CAAA;AAAA,EAC9C;AAAA;AAAA,EAGA,IAAI,EAAA,EAA8C;AAChD,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,EAAE,CAAA;AAAA,EAC1B;AAAA;AAAA,EAGA,MAAA,GAAiC;AAC/B,IAAA,OAAO,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA;AAAA,EACvC;AAAA;AAAA,EAGA,IAAI,EAAA,EAAqB;AACvB,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,EAAE,CAAA;AAAA,EAC1B;AAAA;AAAA,EAGA,WAAW,EAAA,EAAqB;AAC9B,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,EAAE,CAAA;AAAA,EAC7B;AACF,CAAA;AASA,IAAM,CAAA,GAAI,UAAA;AACH,IAAM,YAAA,GAA8B,CAAA,CAAE,YAAY,CAAA,KAAM,IAAI,YAAA;AAM5D,SAAS,oBAAoB,OAAA,EAA6C;AAC/E,EAAA,KAAA,MAAW,CAAC,EAAA,EAAI,MAAM,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,IAAA,YAAA,CAAa,SAAS,EAAE,EAAA,EAAI,IAAA,EAAM,EAAA,EAAI,QAAQ,CAAA;AAAA,EAChD;AACF","file":"chunk-2AUMER56.js","sourcesContent":["import type { ViewTypeRegistration, ViewRenderer } from \"./types\";\n\n/**\n * Symbol key for the global ViewRegistry singleton.\n *\n * Using a well-known Symbol on `globalThis` guarantees that all copies of this\n * module — whether loaded from different `node_modules` paths or from different\n * tsup entry points — share the exact same registry instance. Without this,\n * community plugins that bundle or install their own copy of bases-page would\n * register views into a private registry that the main bases-page never sees.\n */\nconst REGISTRY_KEY = Symbol.for(\"@quartz-community/bases-page/viewRegistry\");\n\n/**\n * Central registry for Bases view types.\n *\n * Built-in views (table, list, cards, gallery, board) are registered at plugin\n * init time. Community plugins can register additional view types by importing\n * this singleton and calling {@link viewRegistry.register}.\n *\n * @example Community plugin registering a custom view:\n * ```ts\n * import { viewRegistry } from \"@quartz-community/bases-page\";\n *\n * viewRegistry.register({\n * id: \"timeline\",\n * name: \"Timeline\",\n * icon: \"git-branch\",\n * render: ({ entries, view }) => <div class=\"bases-timeline\">...</div>,\n * });\n * ```\n */\nclass ViewRegistry {\n private views = new Map<string, ViewTypeRegistration>();\n\n /**\n * Register a view type. If a view with the same ID already exists it is\n * silently replaced — this lets config-level `customViews` override built-in\n * renderers for the same type.\n */\n register(registration: ViewTypeRegistration): void {\n this.views.set(registration.id, registration);\n }\n\n /** Look up a registered view type by ID. */\n get(id: string): ViewTypeRegistration | undefined {\n return this.views.get(id);\n }\n\n /** Return all registered view types (insertion order). */\n getAll(): ViewTypeRegistration[] {\n return Array.from(this.views.values());\n }\n\n /** Check whether a view type is registered. */\n has(id: string): boolean {\n return this.views.has(id);\n }\n\n /** Remove a view type registration. Returns true if it existed. */\n unregister(id: string): boolean {\n return this.views.delete(id);\n }\n}\n\n/**\n * Singleton view registry instance, stored on `globalThis` via a well-known\n * Symbol so that all copies of this module share the same instance.\n *\n * Community plugins import this to register custom view types.\n * Built-in views are registered by `registerBuiltinViews()` during plugin init.\n */\nconst g = globalThis as unknown as Record<symbol, ViewRegistry>;\nexport const viewRegistry: ViewRegistry = (g[REGISTRY_KEY] ??= new ViewRegistry());\n\n/**\n * Convenience: bulk-register views from a `customViews` config record.\n * Called during plugin init to merge user-provided renderers into the registry.\n */\nexport function registerCustomViews(customs: Record<string, ViewRenderer>): void {\n for (const [id, render] of Object.entries(customs)) {\n viewRegistry.register({ id, name: id, render });\n }\n}\n"]}