From 0006bfc868fc78dda8ea35fb502634ba5c5e001f Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Fri, 13 Feb 2026 19:12:22 +0100 Subject: [PATCH] feat: implement folder page type plugin --- package-lock.json | 566 ++++++++++------------- package.json | 21 +- src/components/ExampleComponent.tsx | 33 -- src/components/FolderContent.tsx | 157 +++++++ src/components/PageList.tsx | 153 ++++++ src/components/index.ts | 5 +- src/components/scripts.d.ts | 4 - src/components/scripts/example.inline.ts | 107 ----- src/components/styles/example.scss | 8 - src/components/styles/listPage.scss | 37 ++ src/emitter.ts | 91 ---- src/filter.ts | 53 --- src/i18n/locales/en-US.ts | 9 +- src/index.ts | 25 +- src/pageType.ts | 83 ++++ src/transformer.ts | 103 ----- src/types.ts | 52 +-- src/util/path.ts | 60 +++ test/emitter.test.ts | 45 -- test/filter.test.ts | 21 - test/helpers.ts | 41 -- test/transformer.test.ts | 22 - 22 files changed, 766 insertions(+), 930 deletions(-) delete mode 100644 src/components/ExampleComponent.tsx create mode 100644 src/components/FolderContent.tsx create mode 100644 src/components/PageList.tsx delete mode 100644 src/components/scripts.d.ts delete mode 100644 src/components/scripts/example.inline.ts delete mode 100644 src/components/styles/example.scss create mode 100644 src/components/styles/listPage.scss delete mode 100644 src/emitter.ts delete mode 100644 src/filter.ts create mode 100644 src/pageType.ts delete mode 100644 src/transformer.ts create mode 100644 src/util/path.ts delete mode 100644 test/emitter.test.ts delete mode 100644 test/filter.test.ts delete mode 100644 test/helpers.ts delete mode 100644 test/transformer.test.ts diff --git a/package-lock.json b/package-lock.json index 6fc560a..63900fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4c507b6..6bf9d14 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/components/ExampleComponent.tsx b/src/components/ExampleComponent.tsx deleted file mode 100644 index d18ee6a..0000000 --- a/src/components/ExampleComponent.tsx +++ /dev/null @@ -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
{fullText}
; - }; - - Component.css = style; - Component.afterDOMLoaded = script; - - return Component; -}) satisfies QuartzComponentConstructor; diff --git a/src/components/FolderContent.tsx b/src/components/FolderContent.tsx new file mode 100644 index 0000000..ba70c05 --- /dev/null +++ b/src/components/FolderContent.tsx @@ -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) => { + 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 ( +
+
{content}
+
+ {options.showFolderCount && ( +

+ {i18n( + (cfg as { locale?: string } | undefined)?.locale ?? "en-US", + ).pages.folderContent.itemsUnderFolder({ + count: allPagesInFolder.length, + })} +

+ )} +
{pageListContent}
+
+
+ ); + }; + + FolderContent.css = concatenateResources(style, PageList.css); + return FolderContent; +}) satisfies QuartzComponentConstructor; diff --git a/src/components/PageList.tsx b/src/components/PageList.tsx new file mode 100644 index 0000000..5fba183 --- /dev/null +++ b/src/components/PageList.tsx @@ -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 ( + + ); +} + +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 ( + + ); +}; + +PageList.css = ` +.section h3 { + margin: 0; +} + +.section > .tags { + margin: 0; +} +`; diff --git a/src/components/index.ts b/src/components/index.ts index f2ca23a..39ab6a4 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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"; diff --git a/src/components/scripts.d.ts b/src/components/scripts.d.ts deleted file mode 100644 index 89df165..0000000 --- a/src/components/scripts.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.inline.ts" { - const content: string; - export default content; -} diff --git a/src/components/scripts/example.inline.ts b/src/components/scripts/example.inline.ts deleted file mode 100644 index 366f8e5..0000000 --- a/src/components/scripts/example.inline.ts +++ /dev/null @@ -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"); - } -}); diff --git a/src/components/styles/example.scss b/src/components/styles/example.scss deleted file mode 100644 index 49b4379..0000000 --- a/src/components/styles/example.scss +++ /dev/null @@ -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; -} diff --git a/src/components/styles/listPage.scss b/src/components/styles/listPage.scss new file mode 100644 index 0000000..89a9b3e --- /dev/null +++ b/src/components/styles/listPage.scss @@ -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; + } +} diff --git a/src/emitter.ts b/src/emitter.ts deleted file mode 100644 index 00d69be..0000000 --- a/src/emitter.ts +++ /dev/null @@ -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> = ( - userOptions?: Partial, -) => { - 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; - } - }, - }; -}; diff --git a/src/filter.ts b/src/filter.ts deleted file mode 100644 index 5d308c6..0000000 --- a/src/filter.ts +++ /dev/null @@ -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> = ( - userOptions?: Partial, -) => { - 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; - }, - }; -}; diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 001f5c2..0d60717 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -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: {}, }; diff --git a/src/index.ts b/src/index.ts index d5c31b9..210586e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/pageType.ts b/src/pageType.ts new file mode 100644 index 0000000..62eed52 --- /dev/null +++ b/src/pageType.ts @@ -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 = (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(); + 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(); + 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, + }; +}; diff --git a/src/transformer.ts b/src/transformer.ts deleted file mode 100644 index 6afeead..0000000 --- a/src/transformer.ts +++ /dev/null @@ -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> = ( - userOptions?: Partial, -) => { - 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: [], - }; - }, - }; -}; diff --git a/src/types.ts b/src/types.ts index f3fac5a..f8765fe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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; - /** Optional hook to transform the emitted manifest JSON string. */ - transformManifest?: (json: string) => string; - /** Add a custom class to the emitted manifest