Quartz community main (#14)

* Port to Quartz v5 Bases view plugin

Rewrite the v4 transformer plugin as a v5 Bases view plugin that
registers a 'leaflet-map' view type via the bases-page view registry.

- Add v5 plugin structure: package.json, tsup.config.ts, tsconfig.json
- Add view renderer (src/view.tsx) that builds server-side marker HTML
- Add client-side Leaflet initialization (src/scripts/leaflet-map.inline.ts)
- Add full Leaflet + custom marker styles (src/styles/leaflet-map.scss)
- Add type definitions for view config and marker data (src/types.ts)
- Auto-register view on import via viewRegistry.register()
- Mark @quartz-community/bases-page as external peer dependency
- ESM + DTS build via tsup with inline-script and SCSS loaders

* fix: move bases-page from dependencies to devDependencies

bases-page must only be a peerDependency at runtime so Node resolves
to the host's installed copy. Keeping it as a regular dependency caused
npm to install a private copy in node_modules/, creating a separate
ViewRegistry instance that never communicated with the host plugin.

Now bases-page is a devDependency (needed for TypeScript compilation)
and a peerDependency (resolved at runtime from the host).

* feat: resolve image paths via transformLink for correct link resolution

- Import transformLink and FullSlug from @quartz-community/bases-page
- Use slug, allSlugs, linkResolution props from ViewRendererProps
- Remove stale ambient module declaration that shadowed real package types
- Add @quartz-community/utils and @quartz-community/types as devDependencies

* fix: defer ControlContainer class definition to avoid L is not defined at parse time

* fix: add CDN fallback and error handling for dependency loading

Switch primary CDN from unpkg.com to cdn.jsdelivr.net with unpkg.com as
fallback. Add loadScript() helper that tries multiple URLs sequentially.
Add try/catch around loadDependencies() in the nav listener with early
return when no map elements found, and user-visible error message when
all CDN sources fail. Fixes maps not loading when browser content
blocking (e.g. Firefox Enhanced Tracking Protection) blocks unpkg.com.

* chore: linting

* feat: add render event listener for in-place DOM re-initialization

* fix: update to bases-page changes

* feat: allow plugin views to pass options

* Cleanup typing

* Remove quartz v4plugin files

* Removal of duplicated imports

* Cleanup typing

* Removal of duplicated imports

* chore: commit dist/ and remove prepare script

* Update README.md

* fix: update to bases-page changes

* chore: rebase

* Cleanup typing

* Removal of duplicated imports

* Cleanup typing

* Remove quartz v4plugin files

* Removal of duplicated imports

---------

Co-authored-by: saberzero1 <github@emilebangma.com>
This commit is contained in:
Sara Lilith Slootmaker 2026-03-16 17:41:50 +01:00 committed by GitHub
parent 6b01c731af
commit 74a8afcf1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 246 additions and 208 deletions

View file

@ -12,7 +12,14 @@
},
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"ignorePatterns": ["dist", "node_modules"],
"ignorePatterns": [
"dist",
"node_modules",
"leafletMapPlugin.ts",
"src/build.ts",
"src/plugins",
"src/util"
],
"rules": {
"@typescript-eslint/no-unused-vars": [
"error",

View file

@ -9,6 +9,34 @@ A live demo can be found [here](https://requiae.github.io/quartz-leaflet-map-plu
## How to add it to your quartz
### Quartz v5
Add the plugin to the `plugins` list in your `quartz.config.yaml`:
```yaml
plugins:
- source: github:quartz-community/external-quartz-leaflet-map-plugin
enabled: true
```
This plugin also requires the [bases-page](https://github.com/quartz-community/bases-page) plugin.
### Plugin Options
You can pass options to the plugin via `quartz.config.yaml`:
```yaml
plugins:
- source: github:quartz-community/external-quartz-leaflet-map-plugin
enabled: true
options:
enableCopyTool: true
```
| Option | Type | Default | Description |
| -------------- | ------- | ------- | ------------------------------------------------------------- |
| enableCopyTool | boolean | `false` | Enables the copy tool from the Obsidian Leaflet Bases plugin. |
### Quartz v4
> Quartz v4 does not have a bases implementation. As such it is highly recommended (read 'basically required') to use the `mapName` feature if your vault has multiple maps.

25
dist/index.d.ts vendored
View file

@ -1,6 +1,4 @@
import { ViewTypeRegistration } from '@quartz-community/bases-page';
declare const leafletMapViewRegistration: ViewTypeRegistration;
import { ViewTypeRegistration } from "@quartz-community/bases-page";
interface LeafletMapViewConfig {
mapName?: string;
@ -13,6 +11,9 @@ interface LeafletMapViewConfig {
scale?: number;
unit?: string;
}
interface LeafletMapPluginOptions {
enableCopyTool?: boolean;
}
interface MarkerData {
mapName?: string;
coordinates: string;
@ -21,4 +22,20 @@ interface MarkerData {
minZoom?: number;
}
export { type LeafletMapViewConfig, type MarkerData, leafletMapViewRegistration };
declare const leafletMapViewRegistration: ViewTypeRegistration;
declare function registerLeafletMap(userOpts?: Partial<LeafletMapPluginOptions>): void;
/**
* Called by Quartz's config-loader with merged options from
* `quartz.config.yaml` and `package.json` defaultOptions.
*/
declare function init(options?: Record<string, unknown>): void;
export {
type LeafletMapPluginOptions,
type LeafletMapViewConfig,
type MarkerData,
init,
leafletMapViewRegistration,
registerLeafletMap,
};

199
dist/index.js vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

114
package-lock.json generated
View file

@ -1003,19 +1003,13 @@
},
"node_modules/@quartz-community/bases-page": {
"version": "0.2.0",
"resolved": "git+ssh://git@github.com/quartz-community/bases-page.git#67e3971e6d7dc67ee11ba36e7fe4a16dbb1d19ae",
"resolved": "git+ssh://git@github.com/quartz-community/bases-page.git#7d40bf8ac23b75cd089bfa29d55f0fbe6920c025",
"dev": true,
"license": "MIT",
"dependencies": {
"@quartz-community/types": "github:quartz-community/types",
"@quartz-community/utils": "github:quartz-community/utils",
"hast-util-from-html": "^2.0.3",
"preact-render-to-string": "^6.6.6",
"tsup": "^8.5.0",
"typescript": "^5.9.3",
"unist-util-visit": "^5.1.0",
"vfile": "^6.0.3",
"yaml": "^2.8.1"
"hast-util-from-html": "^2.0.3"
},
"engines": {
"node": ">=22",
@ -1023,7 +1017,11 @@
},
"peerDependencies": {
"@jackyzha0/quartz": "^5.0.0",
"preact": "^10.0.0"
"preact": "^10.0.0",
"preact-render-to-string": "^6.6.6",
"unist-util-visit": "^5.1.0",
"vfile": "^6.0.3",
"yaml": "^2.8.1"
},
"peerDependenciesMeta": {
"@jackyzha0/quartz": {
@ -1031,6 +1029,18 @@
},
"preact": {
"optional": false
},
"preact-render-to-string": {
"optional": true
},
"unist-util-visit": {
"optional": true
},
"vfile": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
@ -3198,16 +3208,6 @@
"url": "https://opencollective.com/preact"
}
},
"node_modules/preact-render-to-string": {
"version": "6.6.6",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.6.6.tgz",
"integrity": "sha512-EfqZJytnjJldV+YaaqhthU2oXsEf5e+6rDv957p+zxAvNfFLQOPfvBOTncscQ+akzu6Wrl7s3Pa0LjUQmWJsGQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"preact": ">=10 || >= 11.0.0-0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -4654,20 +4654,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
@ -4682,37 +4668,6 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
"integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-visit-parents": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@ -4759,6 +4714,21 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-location": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz",
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/vfile-message": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
@ -4818,22 +4788,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"devOptional": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View file

@ -83,7 +83,9 @@
"dependencies": [],
"defaultOrder": 50,
"defaultEnabled": true,
"defaultOptions": {}
"defaultOptions": {
"enableCopyTool": false
}
},
"engines": {
"node": ">=22",

View file

@ -12,9 +12,3 @@ declare module "*.inline.ts" {
const content: string;
export default content;
}
declare global {
interface Window {
addCleanup(fn: () => void): void;
}
}

10
src/globals.d.ts vendored
View file

@ -1,9 +1,7 @@
export declare global {
interface Window {
addCleanup(fn: (...args: unknown[]) => void);
}
export {};
interface ControlOptions {
enableCopyTool: boolean;
declare global {
interface Window {
addCleanup(fn: () => void): void;
}
}

View file

@ -1,7 +1,25 @@
import { viewRegistry } from "@quartz-community/bases-page";
import { leafletMapViewRegistration } from "./view";
import { defaultOptions } from "./types";
import type { LeafletMapPluginOptions } from "./types";
export { leafletMapViewRegistration } from "./view";
export type { LeafletMapViewConfig, MarkerData } from "./types";
export type { LeafletMapViewConfig, MarkerData, LeafletMapPluginOptions } from "./types";
export function registerLeafletMap(userOpts?: Partial<LeafletMapPluginOptions>): void {
const opts = { ...defaultOptions, ...userOpts };
viewRegistry.register({
...leafletMapViewRegistration,
options: opts,
});
}
/**
* Called by Quartz's config-loader with merged options from
* `quartz.config.yaml` and `package.json` defaultOptions.
*/
export function init(options?: Record<string, unknown>): void {
registerLeafletMap(options as Partial<LeafletMapPluginOptions> | undefined);
}
viewRegistry.register(leafletMapViewRegistration);

View file

@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-namespace */
declare const L: typeof import("leaflet");
type LatLng = import("leaflet").LatLng;
@ -28,6 +26,7 @@ interface CreateIconsOptions {
inTemplates?: boolean;
}
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace lucide {
const createElement: (icon: IconNode) => HTMLElement;
const createIcons: ({ icons, nameAttr, attrs, root, inTemplates }?: CreateIconsOptions) => void;
@ -492,9 +491,12 @@ interface ControlContainerOptions extends ControlOptions {
const DefaultControlContainerOptions: ControlContainerOptions = { enableCopyTool: false };
// Lazily define ControlContainer after Leaflet is loaded (L.Control is unavailable at parse time)
let _ControlContainer: typeof L.Control<ControlContainerOptions> | null = null;
type ControlContainerClass = new (options: ControlContainerOptions) => L.Control & {
updateSettings(options: MapDataSet): void;
};
let _ControlContainer: ControlContainerClass | null = null;
function getControlContainerClass() {
function getControlContainerClass(): ControlContainerClass {
if (_ControlContainer) return _ControlContainer;
class ControlContainer extends L.Control {
@ -554,7 +556,7 @@ function getControlContainerClass() {
}
}
_ControlContainer = ControlContainer as unknown as typeof L.Control;
_ControlContainer = ControlContainer as unknown as ControlContainerClass;
return _ControlContainer;
}
@ -614,9 +616,7 @@ async function initialiseMap(
const ControlContainer = getControlContainerClass();
const controls = new ControlContainer({
enableCopyTool: dataset.enableCopyTool === "true",
}) as InstanceType<typeof L.Control<ControlContainerOptions>> & {
updateSettings(options: MapDataSet): void;
};
});
controls.addTo(mapItem);
controls.updateSettings(dataset);
@ -634,7 +634,7 @@ function cleanupMap(mapItem: Map | undefined) {
mapItem?.remove();
}
document.addEventListener("nav", async () => {
async function initializeLeafletMaps() {
const maps: NodeListOf<HTMLElement> = document.querySelectorAll("div.leaflet-map");
if (maps.length === 0) return;
@ -654,6 +654,9 @@ document.addEventListener("nav", async () => {
const mapItem = await initialiseMap(map, markerData);
window.addCleanup(() => cleanupMap(mapItem));
}
});
}
document.addEventListener("nav", initializeLeafletMaps);
document.addEventListener("render", initializeLeafletMaps);
export default "";

View file

@ -10,6 +10,14 @@ export interface LeafletMapViewConfig {
unit?: string;
}
export interface LeafletMapPluginOptions {
enableCopyTool?: boolean;
}
export const defaultOptions: LeafletMapPluginOptions = {
enableCopyTool: false,
};
export interface MarkerData {
mapName?: string;
coordinates: string;

View file

@ -8,7 +8,7 @@ import type { FullSlug } from "@quartz-community/bases-page";
import leafletMapCss from "./styles/leaflet-map.scss";
import leafletMapScript from "./scripts/leaflet-map.inline";
import { DEFAULTS } from "./types";
import type { MarkerData } from "./types";
import type { MarkerData, LeafletMapPluginOptions } from "./types";
type MarkerWithEntry = MarkerData & {
name: string;
@ -36,7 +36,9 @@ const leafletMapRenderer: ViewRenderer = ({
slug,
allSlugs,
linkResolution,
options,
}: ViewRendererProps) => {
const pluginOptions = (options ?? {}) as LeafletMapPluginOptions;
const mapName = getString(view.mapName);
const rawImage = getString(view.image);
@ -96,7 +98,7 @@ const leafletMapRenderer: ViewRenderer = ({
data-zoom-delta={zoomDelta}
data-scale={scale}
data-unit={unit}
data-enable-copy-tool="false"
data-enable-copy-tool={pluginOptions.enableCopyTool ?? false}
>
{filteredMarkers.map((marker) => (
<div