feat: implement folder page type plugin

This commit is contained in:
saberzero1 2026-02-13 19:12:22 +01:00
parent e4acfdd2ef
commit 0006bfc868
No known key found for this signature in database
22 changed files with 766 additions and 930 deletions

566
package-lock.json generated
View file

@ -1,22 +1,16 @@
{
"name": "@quartz-community/plugin-template",
"name": "@quartz-community/folder-page",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@quartz-community/plugin-template",
"name": "@quartz-community/folder-page",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@quartz-community/types": "github:quartz-community/types",
"mdast-util-find-and-replace": "^3.0.1",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"hast-util-to-jsx-runtime": "^2.3.6",
"vfile": "^6.0.3"
},
"devDependencies": {
@ -1096,9 +1090,17 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree-jsx": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
"integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
@ -1699,6 +1701,36 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-html4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-legacy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-reference-invalid": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
"integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/check-error": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
@ -1745,6 +1777,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -2131,6 +2173,16 @@
"node": ">=4.0"
}
},
"node_modules/estree-util-is-identifier-name": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
"integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@ -2327,12 +2379,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/github-slugger": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"license": "ISC"
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -2446,23 +2492,37 @@
"node": ">=8"
}
},
"node_modules/hast-util-heading-rank": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz",
"integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==",
"node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
"integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
"@types/estree": "^1.0.0",
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"devlop": "^1.0.0",
"estree-util-is-identifier-name": "^3.0.0",
"hast-util-whitespace": "^3.0.0",
"mdast-util-mdx-expression": "^2.0.0",
"mdast-util-mdx-jsx": "^3.0.0",
"mdast-util-mdxjs-esm": "^2.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0",
"style-to-js": "^1.0.0",
"unist-util-position": "^5.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-string": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz",
"integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==",
"node_modules/hast-util-whitespace": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
@ -2528,6 +2588,46 @@
"dev": true,
"license": "ISC"
},
"node_modules/inline-style-parser": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
"license": "MIT"
},
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
"integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-alphanumerical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
"integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
"license": "MIT",
"dependencies": {
"is-alphabetical": "^2.0.0",
"is-decimal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-decimal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
"integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -2551,6 +2651,16 @@
"node": ">=0.10.0"
}
},
"node_modules/is-hexadecimal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
"integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -2738,44 +2848,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/mdast-util-find-and-replace": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
"integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"escape-string-regexp": "^5.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/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mdast-util-from-markdown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
@ -2800,18 +2872,17 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
"node_modules/mdast-util-mdx-expression": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
"integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
"license": "MIT",
"dependencies": {
"@types/estree-jsx": "^1.0.0",
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-gfm-autolink-literal": "^2.0.0",
"mdast-util-gfm-footnote": "^2.0.0",
"mdast-util-gfm-strikethrough": "^2.0.0",
"mdast-util-gfm-table": "^2.0.0",
"mdast-util-gfm-task-list-item": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
@ -2819,78 +2890,38 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-autolink-literal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
"node_modules/mdast-util-mdx-jsx": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
"integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
"license": "MIT",
"dependencies": {
"@types/estree-jsx": "^1.0.0",
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"@types/unist": "^3.0.0",
"ccount": "^2.0.0",
"devlop": "^1.0.0",
"mdast-util-find-and-replace": "^3.0.0",
"micromark-util-character": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-footnote": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"devlop": "^1.1.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0",
"micromark-util-normalize-identifier": "^2.0.0"
"parse-entities": "^4.0.0",
"stringify-entities": "^4.0.0",
"unist-util-stringify-position": "^4.0.0",
"vfile-message": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-strikethrough": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-table": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"markdown-table": "^3.0.0",
"mdast-util-from-markdown": "^2.0.0",
"mdast-util-to-markdown": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-gfm-task-list-item": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
"node_modules/mdast-util-mdxjs-esm": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
"integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
"license": "MIT",
"dependencies": {
"@types/estree-jsx": "^1.0.0",
"@types/hast": "^3.0.0",
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"mdast-util-from-markdown": "^2.0.0",
@ -3028,127 +3059,6 @@
"micromark-util-types": "^2.0.0"
}
},
"node_modules/micromark-extension-gfm": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
"license": "MIT",
"dependencies": {
"micromark-extension-gfm-autolink-literal": "^2.0.0",
"micromark-extension-gfm-footnote": "^2.0.0",
"micromark-extension-gfm-strikethrough": "^2.0.0",
"micromark-extension-gfm-table": "^2.0.0",
"micromark-extension-gfm-tagfilter": "^2.0.0",
"micromark-extension-gfm-task-list-item": "^2.0.0",
"micromark-util-combine-extensions": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-autolink-literal": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
"license": "MIT",
"dependencies": {
"micromark-util-character": "^2.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-footnote": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-core-commonmark": "^2.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-normalize-identifier": "^2.0.0",
"micromark-util-sanitize-uri": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-strikethrough": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-util-chunked": "^2.0.0",
"micromark-util-classify-character": "^2.0.0",
"micromark-util-resolve-all": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-table": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-tagfilter": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
"license": "MIT",
"dependencies": {
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-extension-gfm-task-list-item": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
"license": "MIT",
"dependencies": {
"devlop": "^1.0.0",
"micromark-factory-space": "^2.0.0",
"micromark-util-character": "^2.0.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-types": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/micromark-factory-destination": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
@ -3692,6 +3602,31 @@
"node": ">=6"
}
},
"node_modules/parse-entities": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
"integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^2.0.0",
"character-entities-legacy": "^3.0.0",
"character-reference-invalid": "^2.0.0",
"decode-named-character-reference": "^1.0.0",
"is-alphanumerical": "^2.0.0",
"is-decimal": "^2.0.0",
"is-hexadecimal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/parse-entities/node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -3901,6 +3836,16 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -3946,72 +3891,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/rehype-slug": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz",
"integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"github-slugger": "^2.0.0",
"hast-util-heading-rank": "^3.0.0",
"hast-util-to-string": "^3.0.0",
"unist-util-visit": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-gfm": "^3.0.0",
"micromark-extension-gfm": "^3.0.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
"integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-from-markdown": "^2.0.0",
"micromark-util-types": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-stringify": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
"integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-to-markdown": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -4192,6 +4071,16 @@
"node": ">=0.10.0"
}
},
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@ -4206,6 +4095,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
"license": "MIT",
"dependencies": {
"character-entities-html4": "^2.0.0",
"character-entities-legacy": "^3.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@ -4232,6 +4135,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-to-js": {
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
"integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
"license": "MIT",
"dependencies": {
"style-to-object": "1.0.14"
}
},
"node_modules/style-to-object": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
"integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
"license": "MIT",
"dependencies": {
"inline-style-parser": "0.2.7"
}
},
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
@ -4594,6 +4515,19 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-position": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
"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",

View file

@ -1,23 +1,20 @@
{
"name": "@quartz-community/plugin-template",
"name": "@quartz-community/folder-page",
"version": "0.1.0",
"description": "Template repository for Quartz community plugins.",
"description": "Folder page type plugin for Quartz.",
"type": "module",
"license": "MIT",
"author": "Quartz Community",
"homepage": "https://quartz.jzhao.xyz",
"repository": {
"type": "git",
"url": "https://github.com/quartz-community/plugin-template"
"url": "https://github.com/quartz-community/folder-page"
},
"keywords": [
"quartz",
"quartz-plugin",
"plugin-template",
"remark",
"rehype",
"mdast",
"hast"
"folder-page",
"page-type"
],
"files": [
"dist",
@ -67,13 +64,7 @@
},
"dependencies": {
"@quartz-community/types": "github:quartz-community/types",
"mdast-util-find-and-replace": "^3.0.1",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"hast-util-to-jsx-runtime": "^2.3.6",
"vfile": "^6.0.3"
},
"devDependencies": {

View file

@ -1,33 +0,0 @@
import type {
QuartzComponent,
QuartzComponentProps,
QuartzComponentConstructor,
} from "@quartz-community/types";
import { classNames } from "../util/lang";
import { i18n } from "../i18n";
import style from "./styles/example.scss";
// @ts-ignore
import script from "./scripts/example.inline.ts";
export interface ExampleComponentOptions {
prefix?: string;
suffix?: string;
className?: string;
}
export default ((opts?: ExampleComponentOptions) => {
const { prefix = "", suffix = "", className = "example-component" } = opts ?? {};
const Component: QuartzComponent = (props: QuartzComponentProps) => {
const frontmatter = props.fileData?.frontmatter as { title?: string } | undefined;
const title = frontmatter?.title ?? "Untitled";
const fullText = `${prefix}${title}${suffix}`;
return <div class={classNames(className)}>{fullText}</div>;
};
Component.css = style;
Component.afterDOMLoaded = script;
return Component;
}) satisfies QuartzComponentConstructor;

View file

@ -0,0 +1,157 @@
import type {
QuartzComponent,
QuartzComponentConstructor,
QuartzComponentProps,
} from "@quartz-community/types";
import { PageList } from "./PageList";
import type { SortFn } from "./PageList";
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
import { jsx, jsxs, Fragment } from "preact/jsx-runtime";
import type { ComponentChildren } from "preact";
import type { Root } from "hast";
import { i18n } from "../i18n";
import style from "./styles/listPage.scss";
interface FolderContentOptions {
showFolderCount: boolean;
showSubfolders: boolean;
sort?: SortFn;
}
const defaultOptions: FolderContentOptions = {
showFolderCount: true,
showSubfolders: true,
};
interface TrieNode {
isFolder: boolean;
children: TrieNode[];
data: unknown;
slug: string;
displayName: string;
findNode(path: string[]): TrieNode | undefined;
}
function concatenateResources(
...resources: (string | string[] | undefined)[]
): string | string[] | undefined {
const result = resources.filter((r): r is string | string[] => r !== undefined).flat();
return result.length === 0 ? undefined : result;
}
export default ((opts?: Partial<FolderContentOptions>) => {
const options: FolderContentOptions = { ...defaultOptions, ...opts };
const FolderContent: QuartzComponent = (props: QuartzComponentProps) => {
const { tree, fileData, cfg } = props;
const ctx = props.ctx as { trie?: TrieNode } | undefined;
const trie = ctx?.trie;
const slug = (fileData as { slug?: string } | undefined)?.slug;
if (!trie || !slug) {
return null;
}
const folder = trie.findNode(slug.split("/"));
if (!folder) {
return null;
}
type PageEntry = {
slug?: string;
dates?: { created: Date; modified: Date; published: Date };
frontmatter?: { title?: string; tags?: string[] };
};
const allPagesInFolder: PageEntry[] =
folder.children
.map((node) => {
const nodeData = node.data as PageEntry | null;
if (nodeData) return nodeData;
if (node.isFolder && options.showSubfolders) {
const getMostRecentDates = (): {
created: Date;
modified: Date;
published: Date;
} => {
let maybeDates: { created: Date; modified: Date; published: Date } | undefined;
for (const child of node.children) {
const childDates = (child.data as { dates?: PageEntry["dates"] } | null)?.dates;
if (childDates) {
if (!maybeDates) {
maybeDates = { ...childDates };
} else {
if (childDates.created > maybeDates.created)
maybeDates.created = childDates.created;
if (childDates.modified > maybeDates.modified)
maybeDates.modified = childDates.modified;
if (childDates.published > maybeDates.published)
maybeDates.published = childDates.published;
}
}
}
return (
maybeDates ?? {
created: new Date(),
modified: new Date(),
published: new Date(),
}
);
};
return {
slug: node.slug,
dates: getMostRecentDates(),
frontmatter: { title: node.displayName, tags: [] },
};
}
return undefined;
})
.filter((page): page is PageEntry => page !== undefined) ?? [];
const cssClasses =
((fileData as { frontmatter?: { cssclasses?: string[] } } | undefined)?.frontmatter
?.cssclasses as string[] | undefined) ?? [];
const classes = cssClasses.join(" ");
const listProps = {
...props,
sort: options.sort,
allFiles: allPagesInFolder,
};
const hastRoot = tree as Root;
const content =
hastRoot.children.length === 0
? (fileData as { description?: unknown } | undefined)?.description
: toJsxRuntime(hastRoot, {
Fragment,
jsx: jsx as any,
jsxs: jsxs as any,
elementAttributeNameCase: "html",
});
const pageListContent = PageList(listProps) as unknown as ComponentChildren;
return (
<div class="popover-hint">
<article class={classes}>{content}</article>
<div class="page-listing">
{options.showFolderCount && (
<p>
{i18n(
(cfg as { locale?: string } | undefined)?.locale ?? "en-US",
).pages.folderContent.itemsUnderFolder({
count: allPagesInFolder.length,
})}
</p>
)}
<div>{pageListContent}</div>
</div>
</div>
);
};
FolderContent.css = concatenateResources(style, PageList.css);
return FolderContent;
}) satisfies QuartzComponentConstructor;

153
src/components/PageList.tsx Normal file
View file

@ -0,0 +1,153 @@
import type { QuartzComponent, QuartzComponentProps } from "@quartz-community/types";
import { resolveRelative, isFolderPath } from "../util/path";
import type { FullSlug } from "../util/path";
interface PageData {
slug?: string;
frontmatter?: { title?: string; tags?: string[] };
dates?: {
created: Date;
modified: Date;
published: Date;
};
[key: string]: unknown;
}
export type SortFn = (f1: PageData, f2: PageData) => number;
function getDate(cfg: unknown, data: PageData): Date | undefined {
const type = (cfg as { defaultDateType?: string } | undefined)?.defaultDateType ?? "created";
return data.dates?.[type as keyof typeof data.dates] as Date | undefined;
}
export function byDateAndAlphabetical(cfg: unknown): SortFn {
return (f1, f2) => {
if (f1.dates && f2.dates) {
return (getDate(cfg, f2)?.getTime() ?? 0) - (getDate(cfg, f1)?.getTime() ?? 0);
} else if (f1.dates && !f2.dates) {
return -1;
} else if (!f1.dates && f2.dates) {
return 1;
}
const f1Title = f1.frontmatter?.title?.toLowerCase() ?? "";
const f2Title = f2.frontmatter?.title?.toLowerCase() ?? "";
return f1Title.localeCompare(f2Title);
};
}
export function byDateAndAlphabeticalFolderFirst(cfg: unknown): SortFn {
return (f1, f2) => {
const f1IsFolder = isFolderPath(f1.slug ?? "");
const f2IsFolder = isFolderPath(f2.slug ?? "");
if (f1IsFolder && !f2IsFolder) return -1;
if (!f1IsFolder && f2IsFolder) return 1;
if (f1.dates && f2.dates) {
return (getDate(cfg, f2)?.getTime() ?? 0) - (getDate(cfg, f1)?.getTime() ?? 0);
} else if (f1.dates && !f2.dates) {
return -1;
} else if (!f1.dates && f2.dates) {
return 1;
}
const f1Title = f1.frontmatter?.title?.toLowerCase() ?? "";
const f2Title = f2.frontmatter?.title?.toLowerCase() ?? "";
return f1Title.localeCompare(f2Title);
};
}
type DateComponentProps = {
date: Date;
locale: string;
};
function DateDisplay({ date, locale }: DateComponentProps) {
return (
<time dateTime={date.toISOString()}>
{date.toLocaleDateString(locale, {
year: "numeric",
month: "short",
day: "2-digit",
})}
</time>
);
}
type PageListProps = {
limit?: number;
sort?: SortFn;
} & QuartzComponentProps;
export const PageList: QuartzComponent = ({
cfg,
fileData,
allFiles,
limit,
sort,
}: PageListProps) => {
const sorter = sort ?? byDateAndAlphabeticalFolderFirst(cfg);
let list = [...(allFiles as PageData[])].sort(sorter);
if (limit) {
list = list.slice(0, limit);
}
const fileSlug = (fileData as { slug?: string } | undefined)?.slug as FullSlug | undefined;
return (
<ul class="section-ul">
{list.map((page) => {
const title = page.frontmatter?.title;
const tags = page.frontmatter?.tags ?? [];
return (
<li class="section-li">
<div class="section">
<p class="meta">
{page.dates && (
<DateDisplay
date={getDate(cfg, page)!}
locale={(cfg as { locale?: string } | undefined)?.locale ?? "en-US"}
/>
)}
</p>
<div class="desc">
<h3>
<a
href={resolveRelative(fileSlug ?? ("" as FullSlug), page.slug as FullSlug)}
class="internal"
>
{title}
</a>
</h3>
</div>
<ul class="tags">
{tags.map((tag) => (
<li>
<a
class="internal tag-link"
href={resolveRelative(
fileSlug ?? ("" as FullSlug),
`tags/${tag}` as unknown as FullSlug,
)}
>
{tag}
</a>
</li>
))}
</ul>
</div>
</li>
);
})}
</ul>
);
};
PageList.css = `
.section h3 {
margin: 0;
}
.section > .tags {
margin: 0;
}
`;

View file

@ -1,2 +1,3 @@
export { default as ExampleComponent } from "./ExampleComponent";
export type { ExampleComponentOptions } from "./ExampleComponent";
export { default as FolderContent } from "./FolderContent";
export { PageList, byDateAndAlphabetical, byDateAndAlphabeticalFolderFirst } from "./PageList";
export type { SortFn } from "./PageList";

View file

@ -1,4 +0,0 @@
declare module "*.inline.ts" {
const content: string;
export default content;
}

View file

@ -1,107 +0,0 @@
// @ts-nocheck
// ============================================================================
// Example Inline Script for Quartz Community Plugin
// ============================================================================
// This file demonstrates patterns commonly used in Quartz plugin client-side code.
// It is bundled as a string and injected via Component.afterDOMLoaded.
//
// Key patterns demonstrated:
// 1. Listening to Quartz navigation events ('nav', 'prenav')
// 2. Fetching content index data
// 3. DOM manipulation with cleanup
// 4. State persistence (localStorage/sessionStorage)
// 5. Keyboard shortcut handling
// 6. Proper event listener cleanup
// ============================================================================
// Helper: Remove all children from an element
function removeAllChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
// Helper: Simplify slug by removing trailing /index
function simplifySlug(slug) {
if (slug.endsWith("/index")) {
return slug.slice(0, -6);
}
return slug;
}
// Helper: Get current page slug from URL
function getCurrentSlug() {
let slug = window.location.pathname;
if (slug.startsWith("/")) slug = slug.slice(1);
if (slug.endsWith("/")) slug = slug.slice(0, -1);
return slug || "index";
}
// Helper: Fetch content index (commonly needed for search, graph, explorer)
async function fetchContentIndex() {
try {
const response = await fetch("/static/contentIndex.json");
const data = await response.json();
// Handle both formats: { "slug": {...} } or { "content": { "slug": {...} } }
return data.content || data;
} catch (error) {
console.error("[Plugin] Error fetching content index:", error);
return null;
}
}
// Main initialization function
function init() {
const components = document.querySelectorAll(".example-component");
if (components.length === 0) return;
// Example: Track cleanup functions for event listeners
const cleanupFns = [];
// Example: Add a keyboard shortcut (Ctrl/Cmd + Shift + E)
function keyboardHandler(e) {
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === "e") {
e.preventDefault();
console.log("[ExampleComponent] Keyboard shortcut triggered!");
// Do something interesting here
}
}
document.addEventListener("keydown", keyboardHandler);
cleanupFns.push(() => document.removeEventListener("keydown", keyboardHandler));
// Example: Click handler with proper cleanup
for (const component of components) {
const clickHandler = () => {
console.log("[ExampleComponent] Clicked!");
};
component.addEventListener("click", clickHandler);
cleanupFns.push(() => component.removeEventListener("click", clickHandler));
}
// Register cleanup with Quartz's cleanup system
if (typeof window !== "undefined" && window.addCleanup) {
window.addCleanup(() => {
cleanupFns.forEach((fn) => fn());
});
}
console.log("[ExampleComponent] Initialized with", components.length, "component(s)");
}
// Listen to Quartz navigation events
// 'nav' fires after page navigation (including initial load)
document.addEventListener("nav", (e) => {
const slug = e.detail?.url || getCurrentSlug();
console.log("[ExampleComponent] Navigation to:", slug);
init();
});
// 'prenav' fires before navigation - use for saving state
document.addEventListener("prenav", () => {
// Example: Save scroll position before navigation
const component = document.querySelector(".example-component");
if (component) {
sessionStorage.setItem("exampleScrollTop", component.scrollTop?.toString() || "0");
}
});

View file

@ -1,8 +0,0 @@
.example-component {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 4px;
font-weight: 600;
display: inline-block;
}

View file

@ -0,0 +1,37 @@
ul.section-ul {
list-style: none;
margin-top: 2em;
padding-left: 0;
}
li.section-li {
margin-bottom: 1em;
& > .section {
display: grid;
grid-template-columns: fit-content(8em) 3fr 1fr;
@media all and (max-width: 600px) {
& > .tags {
display: none;
}
}
& > .desc > h3 > a {
background-color: transparent;
}
& .meta {
margin: 0 1em 0 0;
opacity: 0.6;
}
}
}
.popover .section {
grid-template-columns: fit-content(8em) 1fr !important;
& > .tags {
display: none;
}
}

View file

@ -1,91 +0,0 @@
import path from "node:path";
import fs from "node:fs/promises";
import type {
QuartzEmitterPlugin,
ProcessedContent,
BuildCtx,
FilePath,
FullSlug,
} from "@quartz-community/types";
import type { ExampleEmitterOptions } from "./types";
const defaultOptions: ExampleEmitterOptions = {
manifestSlug: "plugin-manifest",
includeFrontmatter: true,
metadata: {
generator: "Quartz Plugin Template",
},
};
const joinSegments = (...segments: string[]) =>
segments
.filter((segment) => segment.length > 0)
.join("/")
.replace(/\/+/g, "/") as FilePath;
const writeFile = async (
outputDir: string,
slug: FullSlug,
ext: `.${string}` | "",
content: string,
) => {
const outputPath = joinSegments(outputDir, `${slug}${ext}`) as FilePath;
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, content);
return outputPath;
};
/**
* Example emitter that writes a JSON manifest of content metadata.
*/
export const ExampleEmitter: QuartzEmitterPlugin<Partial<ExampleEmitterOptions>> = (
userOptions?: Partial<ExampleEmitterOptions>,
) => {
const options = { ...defaultOptions, ...userOptions };
const emitManifest = async (ctx: BuildCtx, content: ProcessedContent[]) => {
const manifest = {
...options.metadata,
generatedAt: new Date().toISOString(),
pages: content.map(([_tree, vfile]) => {
const frontmatter = (vfile.data?.frontmatter ?? {}) as {
title?: string;
tags?: string[];
[key: string]: unknown;
};
return {
slug: vfile.data?.slug ?? null,
title: frontmatter.title ?? null,
tags: frontmatter.tags ?? null,
filePath: vfile.data?.filePath ?? null,
frontmatter: options.includeFrontmatter ? frontmatter : undefined,
};
}),
};
let json = `${JSON.stringify(manifest, null, 2)}\n`;
if (options.transformManifest) {
json = options.transformManifest(json);
}
const output = await writeFile(
ctx.argv.output,
options.manifestSlug as FullSlug,
".json",
json,
);
return [output];
};
return {
name: "ExampleEmitter",
async emit(ctx, content, _resources) {
return emitManifest(ctx, content);
},
async *partialEmit(ctx, content, _resources, _changeEvents) {
const outputPaths = await emitManifest(ctx, content);
for (const outputPath of outputPaths) {
yield outputPath;
}
},
};
};

View file

@ -1,53 +0,0 @@
import type { QuartzFilterPlugin, ProcessedContent, BuildCtx } from "@quartz-community/types";
import type { ExampleFilterOptions } from "./types";
const defaultOptions: ExampleFilterOptions = {
allowDrafts: false,
excludeTags: ["private"],
excludePathPrefixes: ["_drafts/", "_private/"],
};
const normalizeTag = (tag: unknown) => (typeof tag === "string" ? tag.trim().toLowerCase() : "");
const includesTag = (tags: unknown, excludedTags: string[]) => {
if (!Array.isArray(tags)) {
return false;
}
const normalizedExcluded = excludedTags.map((tag) => tag.toLowerCase());
return tags.some((tag) => normalizedExcluded.includes(normalizeTag(tag)));
};
/**
* Example filter that removes drafts, tagged pages, and excluded path prefixes.
*/
export const ExampleFilter: QuartzFilterPlugin<Partial<ExampleFilterOptions>> = (
userOptions?: Partial<ExampleFilterOptions>,
) => {
const options = { ...defaultOptions, ...userOptions };
return {
name: "ExampleFilter",
shouldPublish(_ctx: BuildCtx, [_tree, vfile]: ProcessedContent) {
const frontmatter = (vfile.data?.frontmatter ?? {}) as {
draft?: boolean | string;
tags?: string[];
};
const isDraft = frontmatter.draft === true || frontmatter.draft === "true";
if (isDraft && !options.allowDrafts) {
return false;
}
if (includesTag(frontmatter.tags, options.excludeTags)) {
return false;
}
const filePath = typeof vfile.data?.filePath === "string" ? vfile.data.filePath : "";
const normalizedPath = filePath.replace(/\\/g, "/");
if (options.excludePathPrefixes.some((prefix) => normalizedPath.startsWith(prefix))) {
return false;
}
return true;
},
};
};

View file

@ -1,7 +1,10 @@
export default {
components: {
example: {
title: "Example",
pages: {
folderContent: {
folder: "Folder",
itemsUnderFolder: ({ count }: { count: number }) =>
count === 1 ? "1 item under this folder." : `${count} items under this folder.`,
},
},
components: {},
};

View file

@ -1,25 +1,18 @@
export { ExampleTransformer } from "./transformer";
export { ExampleFilter } from "./filter";
export { ExampleEmitter } from "./emitter";
export { default as ExampleComponent } from "./components/ExampleComponent";
export type {
ExampleTransformerOptions,
ExampleFilterOptions,
ExampleEmitterOptions,
} from "./types";
export type { ExampleComponentOptions } from "./components/ExampleComponent";
export { FolderPage } from "./pageType";
export { default as FolderContent } from "./components/FolderContent";
export {
PageList,
byDateAndAlphabetical,
byDateAndAlphabeticalFolderFirst,
} from "./components/PageList";
export type { FolderPageOptions } from "./pageType";
export type { SortFn } from "./components/PageList";
// Re-export shared types from @quartz-community/types
export type {
QuartzComponent,
QuartzComponentProps,
QuartzComponentConstructor,
StringResource,
QuartzTransformerPlugin,
QuartzFilterPlugin,
QuartzEmitterPlugin,
QuartzPageTypePlugin,
QuartzPageTypePluginInstance,
PageMatcher,

83
src/pageType.ts Normal file
View file

@ -0,0 +1,83 @@
import type {
QuartzPageTypePlugin,
QuartzComponentConstructor,
PageMatcher,
FullSlug,
VirtualPage,
} from "@quartz-community/types";
import FolderContentComponent from "./components/FolderContent";
import { i18n } from "./i18n";
import { joinSegments } from "./util/path";
import type { SortFn } from "./components/PageList";
import path from "path";
export interface FolderPageOptions {
showFolderCount?: boolean;
showSubfolders?: boolean;
sort?: SortFn;
}
const folderMatcher: PageMatcher = ({ slug }) => {
return slug.endsWith("/index");
};
function getFolders(slug: string): string[] {
let folderName = path.dirname(slug ?? "");
const parentFolderNames = [folderName];
while (folderName !== ".") {
folderName = path.dirname(folderName ?? "");
parentFolderNames.push(folderName);
}
return parentFolderNames;
}
export const FolderPage: QuartzPageTypePlugin<FolderPageOptions> = (opts) => {
const body: QuartzComponentConstructor = () => FolderContentComponent(opts);
return {
name: "FolderPage",
priority: 10,
match: folderMatcher,
generate({ content, cfg }) {
const allFiles = content.map((c) => c[1].data);
const locale = (cfg as { locale?: string } | undefined)?.locale ?? "en-US";
const folders = new Set<string>();
for (const file of allFiles) {
const slug = (file as { slug?: string } | undefined)?.slug;
if (!slug) continue;
const fileFolders = getFolders(slug).filter((f) => f !== "." && f !== "tags");
for (const f of fileFolders) {
folders.add(f);
}
}
const foldersWithIndex = new Set<string>();
for (const [, file] of content) {
const slug = (file.data as { slug?: string } | undefined)?.slug;
if (slug && slug.endsWith("/index")) {
const folder = slug.slice(0, -"/index".length);
foldersWithIndex.add(folder);
}
}
const virtualPages: VirtualPage[] = [];
for (const folder of folders) {
if (foldersWithIndex.has(folder)) continue;
const slug = joinSegments(folder, "index") as unknown as FullSlug;
const title = `${i18n(locale).pages.folderContent.folder}: ${folder}`;
virtualPages.push({
slug,
title,
data: {},
});
}
return virtualPages;
},
layout: "folder",
body,
};
};

View file

@ -1,103 +0,0 @@
import type { PluggableList, Plugin } from "unified";
import type { Root as MdastRoot } from "mdast";
import type { Root as HastRoot, Element } from "hast";
import type { VFile } from "vfile";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import { findAndReplace } from "mdast-util-find-and-replace";
import { visit } from "unist-util-visit";
import type { QuartzTransformerPlugin, BuildCtx } from "@quartz-community/types";
import type { ExampleTransformerOptions } from "./types";
const defaultOptions: ExampleTransformerOptions = {
highlightToken: "==",
headingClass: "example-plugin-heading",
enableGfm: true,
addHeadingSlugs: true,
};
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const remarkHighlightToken = (token: string): Plugin<[], MdastRoot> => {
const escapedToken = escapeRegExp(token);
const pattern = new RegExp(`${escapedToken}([^\n]+?)${escapedToken}`, "g");
return () => (tree: MdastRoot, _file: VFile) => {
findAndReplace(tree, [
[
pattern,
(_match: string, value: string) => ({
type: "strong",
children: [{ type: "text", value }],
}),
],
]);
};
};
const rehypeHeadingClass = (className: string): Plugin<[], HastRoot> => {
return () => (tree: HastRoot, _file: VFile) => {
visit(tree, "element", (node: Element) => {
if (!/^h[1-6]$/.test(node.tagName)) {
return;
}
const existing = node.properties?.className;
const classes: string[] = Array.isArray(existing)
? existing.filter((value): value is string => typeof value === "string")
: typeof existing === "string"
? [existing]
: [];
node.properties = {
...node.properties,
className: [...classes, className],
};
});
};
};
/**
* Example transformer showing remark/rehype usage and resource injection.
*/
export const ExampleTransformer: QuartzTransformerPlugin<Partial<ExampleTransformerOptions>> = (
userOptions?: Partial<ExampleTransformerOptions>,
) => {
const options = { ...defaultOptions, ...userOptions };
return {
name: "ExampleTransformer",
textTransform(_ctx: BuildCtx, src: string) {
return src.endsWith("\n") ? src : `${src}\n`;
},
markdownPlugins(): PluggableList {
const plugins: PluggableList = [remarkHighlightToken(options.highlightToken)];
if (options.enableGfm) {
plugins.unshift(remarkGfm);
}
return plugins;
},
htmlPlugins(): PluggableList {
const plugins: PluggableList = [rehypeHeadingClass(options.headingClass)];
if (options.addHeadingSlugs) {
plugins.unshift(rehypeSlug);
}
return plugins;
},
externalResources() {
return {
css: [
{
content: `.${options.headingClass} { letter-spacing: 0.02em; }`,
inline: true,
},
],
js: [
{
contentType: "inline",
loadTime: "afterDOMReady",
script: "document.documentElement.dataset.exampleTransformer = 'true'",
},
],
additionalHead: [],
};
},
};
};

View file

@ -1,16 +1,7 @@
export type {
BuildCtx,
ChangeEvent,
CSSResource,
JSResource,
ProcessedContent,
QuartzEmitterPlugin,
QuartzEmitterPluginInstance,
QuartzFilterPlugin,
QuartzFilterPluginInstance,
QuartzPluginData,
QuartzTransformerPlugin,
QuartzTransformerPluginInstance,
StaticResources,
PageMatcher,
PageGenerator,
@ -19,44 +10,5 @@ export type {
QuartzPageTypePluginInstance,
} from "@quartz-community/types";
export interface ExampleTransformerOptions {
/** Token used to highlight text, defaults to ==highlight== */
highlightToken: string;
/** Add a CSS class to all headings in the rendered HTML. */
headingClass: string;
/** Enable remark-gfm for tables/task lists. */
enableGfm: boolean;
/** Enable adding slug IDs to headings. */
addHeadingSlugs: boolean;
}
export interface ExampleFilterOptions {
/** Allow pages marked draft: true to publish. */
allowDrafts: boolean;
/** Exclude pages that contain any of these frontmatter tags. */
excludeTags: string[];
/** Exclude paths that start with any of these prefixes (relative to content root). */
excludePathPrefixes: string[];
}
export interface ExampleEmitterOptions {
/** Filename to emit at the site root. */
manifestSlug: string;
/** Whether to include the frontmatter block in the manifest. */
includeFrontmatter: boolean;
/** Extra metadata to write at the top level of the manifest. */
metadata: Record<string, unknown>;
/** Optional hook to transform the emitted manifest JSON string. */
transformManifest?: (json: string) => string;
/** Add a custom class to the emitted manifest <script> tag if used in HTML. */
manifestScriptClass?: string;
}
export interface ExampleComponentOptions {
/** Text to prefix before the title */
prefix?: string;
/** Text to suffix after the title */
suffix?: string;
/** CSS class name to apply */
className?: string;
}
export type { FolderPageOptions } from "./pageType";
export type { SortFn } from "./components/PageList";

60
src/util/path.ts Normal file
View file

@ -0,0 +1,60 @@
export type FullSlug = string & { __brand: "FullSlug" };
export type SimpleSlug = string & { __brand: "SimpleSlug" };
export type RelativeURL = string & { __brand: "relative" };
export function joinSegments(...args: string[]): string {
if (args.length === 0) return "";
let joined = args
.filter((segment) => segment !== "" && segment !== "/")
.map((segment) => stripSlashes(segment))
.join("/");
const first = args[0];
const last = args[args.length - 1];
if (first?.startsWith("/")) joined = "/" + joined;
if (last?.endsWith("/")) joined = joined + "/";
return joined;
}
export function stripSlashes(s: string, onlyStripPrefix?: boolean): string {
if (s.startsWith("/")) s = s.substring(1);
if (!onlyStripPrefix && s.endsWith("/")) s = s.slice(0, -1);
return s;
}
export function simplifySlug(fp: FullSlug): SimpleSlug {
const res = stripSlashes(trimSuffix(fp, "index"), true);
return (res.length === 0 ? "/" : res) as SimpleSlug;
}
export function trimSuffix(s: string, suffix: string): string {
if (endsWith(s, suffix)) s = s.slice(0, -suffix.length);
return s;
}
export function endsWith(s: string, suffix: string): boolean {
return s === suffix || s.endsWith("/" + suffix);
}
export function pathToRoot(slug: FullSlug): RelativeURL {
let rootPath = slug
.split("/")
.filter((x) => x !== "")
.slice(0, -1)
.map((_) => "..")
.join("/");
if (rootPath.length === 0) rootPath = ".";
return rootPath as RelativeURL;
}
export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL {
return joinSegments(pathToRoot(current), simplifySlug(target as FullSlug)) as RelativeURL;
}
export function isFolderPath(fplike: string): boolean {
return (
fplike.endsWith("/") ||
endsWith(fplike, "index") ||
endsWith(fplike, "index.md") ||
endsWith(fplike, "index.html")
);
}

View file

@ -1,45 +0,0 @@
import { describe, expect, it } from "vitest";
import path from "node:path";
import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import { ExampleEmitter } from "../src/emitter";
import { createCtx, createProcessedContent } from "./helpers";
describe("ExampleEmitter", () => {
it("writes a manifest to the output directory", async () => {
const outputDir = await fs.mkdtemp(path.join(tmpdir(), "quartz-plugin-"));
const ctx = createCtx({ argv: { output: outputDir } });
const emitter = ExampleEmitter({ manifestSlug: "manifest" });
const content = [
createProcessedContent({
slug: "hello-world",
filePath: "notes/hello-world.md",
frontmatter: { title: "Hello", tags: ["docs"] },
}),
];
const result = await emitter.emit(ctx, content, {
css: [],
js: [],
additionalHead: [],
});
const outputPaths = Array.isArray(result) ? result : await collectAsync(result);
const outputPath = outputPaths[0];
if (!outputPath) {
throw new Error("Expected emitter to return an output path");
}
const manifest = JSON.parse(await fs.readFile(outputPath, "utf8"));
expect(outputPath).toContain("manifest.json");
expect(manifest.pages[0].slug).toBe("hello-world");
});
});
const collectAsync = async <T>(iterable: AsyncIterable<T>): Promise<T[]> => {
const results: T[] = [];
for await (const item of iterable) {
results.push(item);
}
return results;
};

View file

@ -1,21 +0,0 @@
import { describe, expect, it } from "vitest";
import { ExampleFilter } from "../src/filter";
import { createCtx, createProcessedContent } from "./helpers";
describe("ExampleFilter", () => {
it("filters drafts by default", () => {
const ctx = createCtx();
const filter = ExampleFilter();
const content = createProcessedContent({ frontmatter: { draft: true } });
expect(filter.shouldPublish(ctx, content)).toBe(false);
});
it("allows drafts when configured", () => {
const ctx = createCtx();
const filter = ExampleFilter({ allowDrafts: true });
const content = createProcessedContent({ frontmatter: { draft: true } });
expect(filter.shouldPublish(ctx, content)).toBe(true);
});
});

View file

@ -1,41 +0,0 @@
import type {
BuildCtx,
QuartzConfig,
ProcessedContent,
QuartzPluginData,
} from "@quartz-community/types";
import { VFile } from "vfile";
type BuildCtxOverrides = Omit<Partial<BuildCtx>, "argv"> & {
argv?: Partial<BuildCtx["argv"]>;
};
export const createCtx = (overrides: BuildCtxOverrides = {}): BuildCtx => {
const { argv: argvOverrides, ...rest } = overrides;
const argv: BuildCtx["argv"] = {
directory: "content",
verbose: false,
output: "dist",
serve: false,
watch: false,
port: 0,
wsPort: 0,
...argvOverrides,
};
return {
buildId: "test-build",
argv,
cfg: {} as QuartzConfig,
allSlugs: [],
allFiles: [],
incremental: false,
...rest,
};
};
export const createProcessedContent = (data: Partial<QuartzPluginData> = {}): ProcessedContent => {
const vfile = new VFile("");
vfile.data = data;
return [{ type: "root", children: [] }, vfile];
};

View file

@ -1,22 +0,0 @@
import { describe, expect, it } from "vitest";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkStringify from "remark-stringify";
import { ExampleTransformer } from "../src/transformer";
import { createCtx } from "./helpers";
describe("ExampleTransformer", () => {
it("highlights text wrapped in the token", async () => {
const ctx = createCtx();
const transformer = ExampleTransformer({ highlightToken: "==" });
const plugins = transformer.markdownPlugins?.(ctx) ?? [];
const file = await unified()
.use(remarkParse)
.use(plugins)
.use(remarkStringify)
.process("Hello ==Quartz==");
expect(String(file)).toContain("**Quartz**");
});
});