mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
feat: configuration files
This commit is contained in:
parent
108906ca44
commit
47de5cc55e
148
quartz.lock.json
148
quartz.lock.json
@ -4,224 +4,224 @@
|
||||
"explorer": {
|
||||
"source": "github:quartz-community/explorer",
|
||||
"resolved": "https://github.com/quartz-community/explorer.git",
|
||||
"commit": "47b42a06f7a6688393b5790cf062dd2d3a3816a3",
|
||||
"installedAt": "2026-02-14T01:15:32.713Z"
|
||||
"commit": "09452e44a4c0e9b2c1d94e6e7a5dc98137dac230",
|
||||
"installedAt": "2026-02-17T17:28:27.727Z"
|
||||
},
|
||||
"graph": {
|
||||
"source": "github:quartz-community/graph",
|
||||
"resolved": "https://github.com/quartz-community/graph.git",
|
||||
"commit": "6b10d168ff8c2df9ee7d2aee8076b787f79a22ec",
|
||||
"installedAt": "2026-02-14T00:12:10.810Z"
|
||||
"commit": "f61ef4aaad0a560428373508e49efa8bf5d0dbf6",
|
||||
"installedAt": "2026-02-17T17:28:28.934Z"
|
||||
},
|
||||
"search": {
|
||||
"source": "github:quartz-community/search",
|
||||
"resolved": "https://github.com/quartz-community/search.git",
|
||||
"commit": "9939f74a49f5caba98b57d8f24492e89f48979ae",
|
||||
"installedAt": "2026-02-14T00:12:11.259Z"
|
||||
"commit": "5d206baacbad17e99925f61c6263e98a494258d1",
|
||||
"installedAt": "2026-02-17T17:28:29.442Z"
|
||||
},
|
||||
"backlinks": {
|
||||
"source": "github:quartz-community/backlinks",
|
||||
"resolved": "https://github.com/quartz-community/backlinks.git",
|
||||
"commit": "b6c6ec516ede1fe766353e471cba056317347f32",
|
||||
"installedAt": "2026-02-14T01:15:37.731Z"
|
||||
"commit": "8adb778566a3d8425d321d6bbc0b673cba9ec63d",
|
||||
"installedAt": "2026-02-17T17:28:30.109Z"
|
||||
},
|
||||
"table-of-contents": {
|
||||
"source": "github:quartz-community/table-of-contents",
|
||||
"resolved": "https://github.com/quartz-community/table-of-contents.git",
|
||||
"commit": "43aaea2e8a80baaf38c2c633765299d6a03e0f59",
|
||||
"installedAt": "2026-02-14T01:15:39.471Z"
|
||||
"commit": "38e7f0198114e4466cb39ea0c2ebb01e46847d39",
|
||||
"installedAt": "2026-02-17T17:28:30.541Z"
|
||||
},
|
||||
"comments": {
|
||||
"source": "github:quartz-community/comments",
|
||||
"resolved": "https://github.com/quartz-community/comments.git",
|
||||
"commit": "ff4313309b0e56214ef4875e2c0aca2ab0306ab0",
|
||||
"installedAt": "2026-02-14T01:15:41.231Z"
|
||||
"commit": "a2bee0c8f2f899f4f181a8a6f52ef300eb6bb8e4",
|
||||
"installedAt": "2026-02-17T17:28:30.983Z"
|
||||
},
|
||||
"breadcrumbs": {
|
||||
"source": "github:quartz-community/breadcrumbs",
|
||||
"resolved": "https://github.com/quartz-community/breadcrumbs.git",
|
||||
"commit": "25ada82faf132e926603d7d74d3812e59cf9cfa1",
|
||||
"installedAt": "2026-02-14T01:15:42.947Z"
|
||||
"commit": "441c3c474b2a7aeecb483aa4421c7a633458e97d",
|
||||
"installedAt": "2026-02-17T17:28:31.401Z"
|
||||
},
|
||||
"recent-notes": {
|
||||
"source": "github:quartz-community/recent-notes",
|
||||
"resolved": "https://github.com/quartz-community/recent-notes.git",
|
||||
"commit": "3d9ef14936e6ee50c03de8c01ad04006293f7101",
|
||||
"installedAt": "2026-02-14T01:15:44.526Z"
|
||||
"commit": "96d5df9fc6d7e0e8bd3a90856faaa08d2ac3441b",
|
||||
"installedAt": "2026-02-17T17:28:31.827Z"
|
||||
},
|
||||
"latex": {
|
||||
"source": "github:quartz-community/latex",
|
||||
"resolved": "https://github.com/quartz-community/latex.git",
|
||||
"commit": "ae28358f91b9414c367d7f92aef17ddb860f51cd",
|
||||
"installedAt": "2026-02-14T01:15:46.351Z"
|
||||
"commit": "d7d4a8de001ec18289d12b58c8144f27ab72cb06",
|
||||
"installedAt": "2026-02-17T17:28:32.350Z"
|
||||
},
|
||||
"article-title": {
|
||||
"source": "github:quartz-community/article-title",
|
||||
"resolved": "https://github.com/quartz-community/article-title.git",
|
||||
"commit": "32c2c9238f6cc7d031c5f46934fe8823ae0b941b",
|
||||
"installedAt": "2026-02-14T01:15:48.273Z"
|
||||
"commit": "031872e39ba30d8567af63c6a3891b3054432194",
|
||||
"installedAt": "2026-02-17T17:28:32.818Z"
|
||||
},
|
||||
"tag-list": {
|
||||
"source": "github:quartz-community/tag-list",
|
||||
"resolved": "https://github.com/quartz-community/tag-list.git",
|
||||
"commit": "adcb320f51824a1a18f675fb4c106cb64ac3002a",
|
||||
"installedAt": "2026-02-14T01:15:50.018Z"
|
||||
"commit": "a0a30b822447261a7a9bcf96d649ca7525eb3ded",
|
||||
"installedAt": "2026-02-17T17:28:33.366Z"
|
||||
},
|
||||
"page-title": {
|
||||
"source": "github:quartz-community/page-title",
|
||||
"resolved": "https://github.com/quartz-community/page-title.git",
|
||||
"commit": "f93a92587938c51623b13902c2f40d8907746cf3",
|
||||
"installedAt": "2026-02-14T01:15:51.758Z"
|
||||
"commit": "d14bffe11830eff6e4489945324a0dd33d994bf6",
|
||||
"installedAt": "2026-02-17T17:28:33.828Z"
|
||||
},
|
||||
"darkmode": {
|
||||
"source": "github:quartz-community/darkmode",
|
||||
"resolved": "https://github.com/quartz-community/darkmode.git",
|
||||
"commit": "c321b81c7a049987914e8247ba1c7c2251b295a6",
|
||||
"installedAt": "2026-02-14T01:15:53.419Z"
|
||||
"commit": "453e59cf913b2276a5bb6d6bfdc0685304618877",
|
||||
"installedAt": "2026-02-17T17:28:34.395Z"
|
||||
},
|
||||
"reader-mode": {
|
||||
"source": "github:quartz-community/reader-mode",
|
||||
"resolved": "https://github.com/quartz-community/reader-mode.git",
|
||||
"commit": "69b32f805407c3289d5509d52dd66d42f0337f7d",
|
||||
"installedAt": "2026-02-14T01:15:55.038Z"
|
||||
"commit": "d362013e0de040dcd8711e7c6bf68b93a0668bae",
|
||||
"installedAt": "2026-02-17T17:28:34.989Z"
|
||||
},
|
||||
"content-meta": {
|
||||
"source": "github:quartz-community/content-meta",
|
||||
"resolved": "https://github.com/quartz-community/content-meta.git",
|
||||
"commit": "87fa94a83a658396dd9683705a47860b47385933",
|
||||
"installedAt": "2026-02-14T01:15:56.873Z"
|
||||
"commit": "be178b7ab4f521f0ca29051676f207a23b706051",
|
||||
"installedAt": "2026-02-17T17:28:35.416Z"
|
||||
},
|
||||
"footer": {
|
||||
"source": "github:quartz-community/footer",
|
||||
"resolved": "https://github.com/quartz-community/footer.git",
|
||||
"commit": "0fe310de1e7d306e2a7e9db28a536239716dec42",
|
||||
"installedAt": "2026-02-14T01:15:58.525Z"
|
||||
"commit": "eef4ba0af26ea6097543784fc5cfa6820d6b8250",
|
||||
"installedAt": "2026-02-17T17:28:35.903Z"
|
||||
},
|
||||
"content-page": {
|
||||
"source": "github:quartz-community/content-page",
|
||||
"resolved": "https://github.com/quartz-community/content-page.git",
|
||||
"commit": "2cacb9751b07c5b3c2f13cc79c44f76e6726d8c9",
|
||||
"installedAt": "2026-02-14T01:16:00.123Z"
|
||||
"commit": "9975389d442707d8772aebeaba02676169569c60",
|
||||
"installedAt": "2026-02-17T17:28:36.353Z"
|
||||
},
|
||||
"folder-page": {
|
||||
"source": "github:quartz-community/folder-page",
|
||||
"resolved": "https://github.com/quartz-community/folder-page.git",
|
||||
"commit": "cb70580327a83e72517dfc2426f30add46a6d41b",
|
||||
"installedAt": "2026-02-14T01:16:01.754Z"
|
||||
"commit": "630c26711de6a5cb91ba2b81882f4fad91bbbaeb",
|
||||
"installedAt": "2026-02-17T17:28:36.806Z"
|
||||
},
|
||||
"tag-page": {
|
||||
"source": "github:quartz-community/tag-page",
|
||||
"resolved": "https://github.com/quartz-community/tag-page.git",
|
||||
"commit": "49e4a6ce0201295275ca3512bd95b118a42e1a15",
|
||||
"installedAt": "2026-02-14T01:16:03.431Z"
|
||||
"commit": "27ad16e609832805b2e11c7836937706a38caec1",
|
||||
"installedAt": "2026-02-17T17:28:37.377Z"
|
||||
},
|
||||
"created-modified-date": {
|
||||
"source": "github:quartz-community/created-modified-date",
|
||||
"resolved": "https://github.com/quartz-community/created-modified-date.git",
|
||||
"commit": "1b42ce86e4c3e8f8b8906cdb5e7fe8b0f7a4e1c6",
|
||||
"installedAt": "2026-02-14T01:16:05.122Z"
|
||||
"commit": "4d976e88da01d9fa0dbdcd11e756b1b3037b3a49",
|
||||
"installedAt": "2026-02-17T17:28:37.833Z"
|
||||
},
|
||||
"syntax-highlighting": {
|
||||
"source": "github:quartz-community/syntax-highlighting",
|
||||
"resolved": "https://github.com/quartz-community/syntax-highlighting.git",
|
||||
"commit": "92936ff025f068bf4d72950ff58ec2e56f4da607",
|
||||
"installedAt": "2026-02-14T01:16:06.769Z"
|
||||
"commit": "a3ec90046f68e8d8bbbd275f07f606268cb6e001",
|
||||
"installedAt": "2026-02-17T17:28:38.257Z"
|
||||
},
|
||||
"obsidian-flavored-markdown": {
|
||||
"source": "github:quartz-community/obsidian-flavored-markdown",
|
||||
"resolved": "https://github.com/quartz-community/obsidian-flavored-markdown.git",
|
||||
"commit": "b985da8db841cb8a453785608ff0a5feb847a69b",
|
||||
"installedAt": "2026-02-16T21:29:43.859Z"
|
||||
"commit": "a5f45c21cec287e8aa2896b5032fe48962ed9b77",
|
||||
"installedAt": "2026-02-17T17:28:38.693Z"
|
||||
},
|
||||
"github-flavored-markdown": {
|
||||
"source": "github:quartz-community/github-flavored-markdown",
|
||||
"resolved": "https://github.com/quartz-community/github-flavored-markdown.git",
|
||||
"commit": "8885e276caa3d3a298d1bfff286571889ad1e47b",
|
||||
"installedAt": "2026-02-14T01:16:10.497Z"
|
||||
"commit": "7e19794a519debc994e2e99c67b223661b0aac0c",
|
||||
"installedAt": "2026-02-17T17:28:39.147Z"
|
||||
},
|
||||
"crawl-links": {
|
||||
"source": "github:quartz-community/crawl-links",
|
||||
"resolved": "https://github.com/quartz-community/crawl-links.git",
|
||||
"commit": "eb09d4d77fb2aa704e12db157d42fa1c76d81b02",
|
||||
"installedAt": "2026-02-14T01:16:12.154Z"
|
||||
"commit": "3ca88b4ba1a049cc79b3cf1154b771e4eb8f7549",
|
||||
"installedAt": "2026-02-17T17:28:39.629Z"
|
||||
},
|
||||
"description": {
|
||||
"source": "github:quartz-community/description",
|
||||
"resolved": "https://github.com/quartz-community/description.git",
|
||||
"commit": "8b34242c39639c9fdd0c2161ac43871a0895b7f3",
|
||||
"installedAt": "2026-02-14T01:16:13.775Z"
|
||||
"commit": "82f1c95e53acc7521fa6bf46e9ea20018ee28264",
|
||||
"installedAt": "2026-02-17T17:28:40.060Z"
|
||||
},
|
||||
"hard-line-breaks": {
|
||||
"source": "github:quartz-community/hard-line-breaks",
|
||||
"resolved": "https://github.com/quartz-community/hard-line-breaks.git",
|
||||
"commit": "146ff2162a62de2bf6b341d2402aa43e88c9d221",
|
||||
"installedAt": "2026-02-14T01:16:15.617Z"
|
||||
"commit": "b14a477db87e6657440a044e6d2fbf9d943472e6",
|
||||
"installedAt": "2026-02-17T17:28:40.498Z"
|
||||
},
|
||||
"citations": {
|
||||
"source": "github:quartz-community/citations",
|
||||
"resolved": "https://github.com/quartz-community/citations.git",
|
||||
"commit": "90c35ef22561e37a5f2e681b7307477ca2d8f631",
|
||||
"installedAt": "2026-02-14T01:16:17.397Z"
|
||||
"commit": "b5db4ea6fd6f3fc9c1ce37cd9cdba0ca9e74ecc8",
|
||||
"installedAt": "2026-02-17T17:28:40.960Z"
|
||||
},
|
||||
"ox-hugo": {
|
||||
"source": "github:quartz-community/ox-hugo",
|
||||
"resolved": "https://github.com/quartz-community/ox-hugo.git",
|
||||
"commit": "4f0616e3a65b6b93a66a1cbd4948dd26781a37f3",
|
||||
"installedAt": "2026-02-14T01:16:19.224Z"
|
||||
"commit": "d8fdf7c6a54464e960c6b817147968cf07719086",
|
||||
"installedAt": "2026-02-17T17:28:41.395Z"
|
||||
},
|
||||
"roam": {
|
||||
"source": "github:quartz-community/roam",
|
||||
"resolved": "https://github.com/quartz-community/roam.git",
|
||||
"commit": "9bb85bfbd99624ab014ec65bc58fe665fecf1f3b",
|
||||
"installedAt": "2026-02-14T01:16:20.879Z"
|
||||
"commit": "f94276c284edb01e6b4136ec6f7683f29123c54c",
|
||||
"installedAt": "2026-02-17T17:28:41.832Z"
|
||||
},
|
||||
"remove-draft": {
|
||||
"source": "github:quartz-community/remove-draft",
|
||||
"resolved": "https://github.com/quartz-community/remove-draft.git",
|
||||
"commit": "eab9c76ddb279b953a90b6d5bcc65971d488ef7c",
|
||||
"installedAt": "2026-02-14T01:16:22.581Z"
|
||||
"commit": "61744308cf739f63fe351dea8dffd6b7c434440d",
|
||||
"installedAt": "2026-02-17T17:28:42.324Z"
|
||||
},
|
||||
"explicit-publish": {
|
||||
"source": "github:quartz-community/explicit-publish",
|
||||
"resolved": "https://github.com/quartz-community/explicit-publish.git",
|
||||
"commit": "3166770fc61d3698e800fb42285a1abfdf3ad8d9",
|
||||
"installedAt": "2026-02-14T01:16:24.321Z"
|
||||
"commit": "ba19e6f32393fa5fe788dbf39a9487bc8e73247a",
|
||||
"installedAt": "2026-02-17T17:28:42.799Z"
|
||||
},
|
||||
"alias-redirects": {
|
||||
"source": "github:quartz-community/alias-redirects",
|
||||
"resolved": "https://github.com/quartz-community/alias-redirects.git",
|
||||
"commit": "0fd53121bddf87528fa180343e5a0c06dc606945",
|
||||
"installedAt": "2026-02-14T01:16:26.164Z"
|
||||
"commit": "65897957ab8108d015cd4255d42d84df7377c083",
|
||||
"installedAt": "2026-02-17T17:28:43.267Z"
|
||||
},
|
||||
"cname": {
|
||||
"source": "github:quartz-community/cname",
|
||||
"resolved": "https://github.com/quartz-community/cname.git",
|
||||
"commit": "dd2bb729663d63ff745573a811fdea4e346d04c3",
|
||||
"installedAt": "2026-02-14T01:16:28.105Z"
|
||||
"commit": "752470f04410576a767b48094e926d41eef3eb18",
|
||||
"installedAt": "2026-02-17T17:28:43.713Z"
|
||||
},
|
||||
"favicon": {
|
||||
"source": "github:quartz-community/favicon",
|
||||
"resolved": "https://github.com/quartz-community/favicon.git",
|
||||
"commit": "2112d5c5813dccef150fd7a7c00b6da8406c5ada",
|
||||
"installedAt": "2026-02-14T01:16:29.931Z"
|
||||
"commit": "71cfff28b7a41f28618f574e672b2b399c6b781d",
|
||||
"installedAt": "2026-02-17T17:28:44.365Z"
|
||||
},
|
||||
"content-index": {
|
||||
"source": "github:quartz-community/content-index",
|
||||
"resolved": "https://github.com/quartz-community/content-index.git",
|
||||
"commit": "6162a6ae8afdaff97358337a368299dc0dcb3c55",
|
||||
"installedAt": "2026-02-14T01:16:31.670Z"
|
||||
"commit": "2b9c62d659b7f8e9e7cc028a201cae7061224653",
|
||||
"installedAt": "2026-02-17T17:28:44.806Z"
|
||||
},
|
||||
"og-image": {
|
||||
"source": "github:quartz-community/og-image",
|
||||
"resolved": "https://github.com/quartz-community/og-image.git",
|
||||
"commit": "c72ed66e951663a40bd2d0834725090572ffb124",
|
||||
"installedAt": "2026-02-14T01:16:33.273Z"
|
||||
"commit": "9a00809fccb0e1e4b7db4ee6e6eed002952396f4",
|
||||
"installedAt": "2026-02-17T17:28:45.533Z"
|
||||
},
|
||||
"canvas-page": {
|
||||
"source": "github:quartz-community/canvas-page",
|
||||
"resolved": "https://github.com/quartz-community/canvas-page.git",
|
||||
"commit": "505c09999e3ff32a34bc76a0bb398dca837e1a5f",
|
||||
"installedAt": "2026-02-15T00:26:57.162Z"
|
||||
"commit": "683c2da6bb068d3e596cdb3bfcca68007829cd17",
|
||||
"installedAt": "2026-02-17T17:28:46.012Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
318
quartz.plugins.default.json
Normal file
318
quartz.plugins.default.json
Normal file
@ -0,0 +1,318 @@
|
||||
{
|
||||
"$schema": "./quartz/plugins/quartz-plugins.schema.json",
|
||||
"configuration": {
|
||||
"pageTitle": "Quartz 5",
|
||||
"pageTitleSuffix": "",
|
||||
"enableSPA": true,
|
||||
"enablePopovers": true,
|
||||
"analytics": { "provider": "plausible" },
|
||||
"locale": "en-US",
|
||||
"baseUrl": "quartz.jzhao.xyz",
|
||||
"ignorePatterns": ["private", "templates", ".obsidian"],
|
||||
"defaultDateType": "modified",
|
||||
"theme": {
|
||||
"fontOrigin": "googleFonts",
|
||||
"cdnCaching": true,
|
||||
"typography": {
|
||||
"header": "Schibsted Grotesk",
|
||||
"body": "Source Sans Pro",
|
||||
"code": "IBM Plex Mono"
|
||||
},
|
||||
"colors": {
|
||||
"lightMode": {
|
||||
"light": "#faf8f8",
|
||||
"lightgray": "#e5e5e5",
|
||||
"gray": "#b8b8b8",
|
||||
"darkgray": "#4e4e4e",
|
||||
"dark": "#2b2b2b",
|
||||
"secondary": "#284b63",
|
||||
"tertiary": "#84a59d",
|
||||
"highlight": "rgba(143, 159, 169, 0.15)",
|
||||
"textHighlight": "#fff23688"
|
||||
},
|
||||
"darkMode": {
|
||||
"light": "#161618",
|
||||
"lightgray": "#393639",
|
||||
"gray": "#646464",
|
||||
"darkgray": "#d4d4d4",
|
||||
"dark": "#ebebec",
|
||||
"secondary": "#7b97aa",
|
||||
"tertiary": "#84a59d",
|
||||
"highlight": "rgba(143, 159, 169, 0.15)",
|
||||
"textHighlight": "#b3aa0288"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"source": "github:quartz-community/created-modified-date",
|
||||
"enabled": true,
|
||||
"options": { "priority": ["frontmatter", "git", "filesystem"] },
|
||||
"order": 10
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/syntax-highlighting",
|
||||
"enabled": true,
|
||||
"options": {
|
||||
"theme": { "light": "github-light", "dark": "github-dark" },
|
||||
"keepBackground": false
|
||||
},
|
||||
"order": 20
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/obsidian-flavored-markdown",
|
||||
"enabled": true,
|
||||
"options": { "enableInHtmlEmbed": false, "enableCheckbox": true },
|
||||
"order": 30
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/github-flavored-markdown",
|
||||
"enabled": true,
|
||||
"order": 40
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/table-of-contents",
|
||||
"enabled": true,
|
||||
"order": 50
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/crawl-links",
|
||||
"enabled": true,
|
||||
"options": { "markdownLinkResolution": "shortest" },
|
||||
"order": 60
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/description",
|
||||
"enabled": true,
|
||||
"order": 70
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/latex",
|
||||
"enabled": true,
|
||||
"options": { "renderEngine": "katex" },
|
||||
"order": 80
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/citations",
|
||||
"enabled": false,
|
||||
"order": 85
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/hard-line-breaks",
|
||||
"enabled": false,
|
||||
"order": 90
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/ox-hugo",
|
||||
"enabled": false,
|
||||
"order": 91
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/roam",
|
||||
"enabled": false,
|
||||
"order": 92
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/remove-draft",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/explicit-publish",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/alias-redirects",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/content-index",
|
||||
"enabled": true,
|
||||
"options": { "enableSiteMap": true, "enableRSS": true }
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/favicon",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/og-image",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/cname",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/canvas-page",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/content-page",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/folder-page",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/tag-page",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/explorer",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "left",
|
||||
"priority": 50
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/graph",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "right",
|
||||
"priority": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/search",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "left",
|
||||
"priority": 20,
|
||||
"group": "toolbar",
|
||||
"groupOptions": { "grow": true }
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/backlinks",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "right",
|
||||
"priority": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/article-title",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "beforeBody",
|
||||
"priority": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/content-meta",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "beforeBody",
|
||||
"priority": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/tag-list",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "beforeBody",
|
||||
"priority": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/page-title",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "left",
|
||||
"priority": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/darkmode",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "left",
|
||||
"priority": 30,
|
||||
"group": "toolbar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/reader-mode",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "left",
|
||||
"priority": 35,
|
||||
"group": "toolbar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/spacer",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "left",
|
||||
"priority": 15,
|
||||
"display": "mobile-only"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/breadcrumbs",
|
||||
"enabled": true,
|
||||
"layout": {
|
||||
"position": "beforeBody",
|
||||
"priority": 5,
|
||||
"condition": "not-index"
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/comments",
|
||||
"enabled": false,
|
||||
"options": { "provider": "giscus", "options": {} },
|
||||
"layout": {
|
||||
"position": "afterBody",
|
||||
"priority": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/footer",
|
||||
"enabled": true,
|
||||
"options": {
|
||||
"links": {
|
||||
"GitHub": "https://github.com/jackyzha0/quartz",
|
||||
"Discord Community": "https://discord.gg/cRFFHYye7t"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"source": "github:quartz-community/recent-notes",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"layout": {
|
||||
"groups": {
|
||||
"toolbar": {
|
||||
"direction": "row",
|
||||
"gap": "0.5rem"
|
||||
}
|
||||
},
|
||||
"byPageType": {
|
||||
"content": {},
|
||||
"folder": {
|
||||
"exclude": ["reader-mode"],
|
||||
"positions": {
|
||||
"right": []
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
"exclude": ["reader-mode"],
|
||||
"positions": {
|
||||
"right": []
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"positions": {
|
||||
"beforeBody": [],
|
||||
"left": [],
|
||||
"right": []
|
||||
}
|
||||
},
|
||||
"canvas": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import {
|
||||
handleRestore,
|
||||
handleSync,
|
||||
} from "./cli/handlers.js"
|
||||
import { handleMigrate } from "./cli/migrate-handler.js"
|
||||
import {
|
||||
handlePluginInstall as handleGitPluginInstall,
|
||||
handlePluginAdd,
|
||||
@ -15,6 +16,10 @@ import {
|
||||
handlePluginUpdate,
|
||||
handlePluginRestore,
|
||||
handlePluginList,
|
||||
handlePluginEnable,
|
||||
handlePluginDisable,
|
||||
handlePluginConfig,
|
||||
handlePluginCheck,
|
||||
} from "./cli/plugin-git-handlers.js"
|
||||
import {
|
||||
CommonArgv,
|
||||
@ -51,6 +56,9 @@ yargs(hideBin(process.argv))
|
||||
.command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
|
||||
await handleBuild(argv)
|
||||
})
|
||||
.command("migrate", "Migrate old config to quartz.plugins.json", CommonArgv, async () => {
|
||||
await handleMigrate()
|
||||
})
|
||||
.command(
|
||||
"plugin <subcommand>",
|
||||
"Manage Quartz plugins",
|
||||
@ -84,6 +92,39 @@ yargs(hideBin(process.argv))
|
||||
await handlePluginRestore()
|
||||
},
|
||||
)
|
||||
.command(
|
||||
"enable <names..>",
|
||||
"Enable plugins in quartz.plugins.json",
|
||||
CommonArgv,
|
||||
async (argv) => {
|
||||
await handlePluginEnable(argv.names)
|
||||
},
|
||||
)
|
||||
.command(
|
||||
"disable <names..>",
|
||||
"Disable plugins in quartz.plugins.json",
|
||||
CommonArgv,
|
||||
async (argv) => {
|
||||
await handlePluginDisable(argv.names)
|
||||
},
|
||||
)
|
||||
.command(
|
||||
"config <name>",
|
||||
"View or set plugin configuration",
|
||||
{
|
||||
...CommonArgv,
|
||||
set: {
|
||||
string: true,
|
||||
describe: "Set a config value (key=value)",
|
||||
},
|
||||
},
|
||||
async (argv) => {
|
||||
await handlePluginConfig(argv.name, { set: argv.set })
|
||||
},
|
||||
)
|
||||
.command("check", "Check for plugin updates", CommonArgv, async () => {
|
||||
await handlePluginCheck()
|
||||
})
|
||||
.demandCommand(1, "Please specify a plugin subcommand")
|
||||
},
|
||||
async () => {
|
||||
|
||||
@ -6,7 +6,8 @@ import { readFileSync } from "fs"
|
||||
*/
|
||||
export const ORIGIN_NAME = "origin"
|
||||
export const UPSTREAM_NAME = "upstream"
|
||||
export const QUARTZ_SOURCE_BRANCH = "v4"
|
||||
export const QUARTZ_SOURCE_BRANCH = "v5"
|
||||
export const QUARTZ_SOURCE_REPO = "https://github.com/jackyzha0/quartz.git"
|
||||
export const cwd = process.cwd()
|
||||
export const cacheDir = path.join(cwd, ".quartz-cache")
|
||||
export const cacheFile = "./quartz/.quartz-cache/transpiled-build.mjs"
|
||||
|
||||
@ -23,9 +23,11 @@ import {
|
||||
popContentFolder,
|
||||
stashContentFolder,
|
||||
} from "./helpers.js"
|
||||
import { handlePluginRestore, handlePluginCheck } from "./plugin-git-handlers.js"
|
||||
import {
|
||||
UPSTREAM_NAME,
|
||||
QUARTZ_SOURCE_BRANCH,
|
||||
QUARTZ_SOURCE_REPO,
|
||||
ORIGIN_NAME,
|
||||
version,
|
||||
fp,
|
||||
@ -215,14 +217,20 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
|
||||
)
|
||||
await fs.promises.writeFile(configFilePath, configContent)
|
||||
|
||||
const pluginsJsonPath = path.join(cwd, "quartz.plugins.json")
|
||||
const defaultPluginsJsonPath = path.join(cwd, "quartz.plugins.default.json")
|
||||
if (!fs.existsSync(pluginsJsonPath) && fs.existsSync(defaultPluginsJsonPath)) {
|
||||
await fs.promises.copyFile(defaultPluginsJsonPath, pluginsJsonPath)
|
||||
console.log(styleText("green", "Created quartz.plugins.json from defaults"))
|
||||
}
|
||||
|
||||
// setup remote
|
||||
execSync(
|
||||
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
||||
{ stdio: "ignore" },
|
||||
)
|
||||
execSync(`git remote show upstream || git remote add upstream ${QUARTZ_SOURCE_REPO}`, {
|
||||
stdio: "ignore",
|
||||
})
|
||||
|
||||
outro(`You're all set! Not sure what to do next? Try:
|
||||
• Customizing Quartz a bit more by editing \`quartz.config.ts\`
|
||||
• Customizing Quartz a bit more by editing \`quartz.plugins.json\`
|
||||
• Running \`npx quartz build --serve\` to preview your Quartz locally
|
||||
• Hosting your Quartz online (see: https://quartz.jzhao.xyz/hosting)
|
||||
`)
|
||||
@ -494,9 +502,7 @@ export async function handleUpdate(argv) {
|
||||
const contentFolder = resolveContentPath(argv.directory)
|
||||
console.log(`\n${styleText(["bgGreen", "black"], ` Quartz v${version} `)} \n`)
|
||||
console.log("Backing up your content")
|
||||
execSync(
|
||||
`git remote show upstream || git remote add upstream https://github.com/jackyzha0/quartz.git`,
|
||||
)
|
||||
execSync(`git remote show upstream || git remote add upstream ${QUARTZ_SOURCE_REPO}`)
|
||||
await stashContentFolder(contentFolder)
|
||||
console.log(
|
||||
"Pulling updates... you may need to resolve some `git` conflicts if you've made changes to components or plugins.",
|
||||
@ -532,10 +538,18 @@ export async function handleUpdate(argv) {
|
||||
|
||||
const res = spawnSync("npm", ["i"], opts)
|
||||
if (res.status === 0) {
|
||||
console.log(styleText("green", "Done!"))
|
||||
console.log(styleText("green", "Dependencies updated!"))
|
||||
} else {
|
||||
console.log(styleText("red", "An error occurred above while installing dependencies."))
|
||||
}
|
||||
|
||||
console.log("Restoring plugins from lockfile...")
|
||||
await handlePluginRestore()
|
||||
|
||||
console.log("Checking plugin compatibility...")
|
||||
await handlePluginCheck()
|
||||
|
||||
console.log(styleText("green", "Done!"))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -33,7 +33,7 @@ export async function stashContentFolder(contentFolder) {
|
||||
}
|
||||
|
||||
export function gitPull(origin, branch) {
|
||||
const flags = ["--no-rebase", "--autostash", "-s", "recursive", "-X", "ours", "--no-edit"]
|
||||
const flags = ["--no-rebase", "--autostash", "--no-edit"]
|
||||
const out = spawnSync("git", ["pull", ...flags, origin, branch], { stdio: "inherit" })
|
||||
if (out.stderr) {
|
||||
throw new Error(styleText("red", `Error while pulling updates: ${out.stderr}`))
|
||||
|
||||
192
quartz/cli/migrate-handler.js
Normal file
192
quartz/cli/migrate-handler.js
Normal file
@ -0,0 +1,192 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { spawnSync } from "child_process"
|
||||
import { styleText } from "util"
|
||||
|
||||
const CWD = process.cwd()
|
||||
const CONFIG_PATH = path.join(CWD, "quartz.config.ts")
|
||||
const LAYOUT_PATH = path.join(CWD, "quartz.layout.ts")
|
||||
const PLUGINS_JSON_PATH = path.join(CWD, "quartz.plugins.json")
|
||||
const DEFAULT_PLUGINS_JSON_PATH = path.join(CWD, "quartz.plugins.default.json")
|
||||
const LOCKFILE_PATH = path.join(CWD, "quartz.lock.json")
|
||||
const PLUGINS_DIR = path.join(CWD, ".quartz", "plugins")
|
||||
const PACKAGE_JSON_PATH = path.join(CWD, "package.json")
|
||||
|
||||
function readJson(filePath) {
|
||||
if (!fs.existsSync(filePath)) return null
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf-8"))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function hasTsx() {
|
||||
const pkg = readJson(PACKAGE_JSON_PATH)
|
||||
return Boolean(pkg?.devDependencies?.tsx || pkg?.dependencies?.tsx)
|
||||
}
|
||||
|
||||
function extractWithTsx() {
|
||||
const script = `
|
||||
const { default: config } = await import("./quartz.config.ts")
|
||||
const { layout } = await import("./quartz.layout.ts")
|
||||
const result = {
|
||||
configuration: config?.configuration ?? null,
|
||||
layoutInfo: {
|
||||
defaults: {
|
||||
afterBody: Array.isArray(layout?.defaults?.afterBody) ? layout.defaults.afterBody.length : 0,
|
||||
hasFooter: Boolean(layout?.defaults?.footer),
|
||||
},
|
||||
pageTypes: layout?.byPageType ? Object.keys(layout.byPageType) : [],
|
||||
},
|
||||
}
|
||||
console.log(JSON.stringify(result))
|
||||
`
|
||||
|
||||
const res = spawnSync("node", ["--import", "tsx/esm", "--input-type=module", "-e", script], {
|
||||
encoding: "utf-8",
|
||||
cwd: CWD,
|
||||
})
|
||||
|
||||
if (res.error || res.status !== 0) {
|
||||
return { ok: false, error: res.error ?? res.stderr }
|
||||
}
|
||||
|
||||
try {
|
||||
return { ok: true, data: JSON.parse(res.stdout.trim()) }
|
||||
} catch (error) {
|
||||
return { ok: false, error }
|
||||
}
|
||||
}
|
||||
|
||||
function readManifest(pluginDir) {
|
||||
const pkgPath = path.join(pluginDir, "package.json")
|
||||
if (!fs.existsSync(pkgPath)) return null
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
||||
return pkg.quartz ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function ensureLayoutDefaults(layout) {
|
||||
if (!layout.groups) layout.groups = {}
|
||||
if (!layout.groups.toolbar) {
|
||||
layout.groups.toolbar = { direction: "row", gap: "0.5rem" }
|
||||
}
|
||||
if (!layout.byPageType) layout.byPageType = {}
|
||||
if (!layout.byPageType["404"]) {
|
||||
layout.byPageType["404"] = { positions: { beforeBody: [], left: [], right: [] } }
|
||||
} else if (!layout.byPageType["404"].positions) {
|
||||
layout.byPageType["404"].positions = { beforeBody: [], left: [], right: [] }
|
||||
}
|
||||
return layout
|
||||
}
|
||||
|
||||
function buildPluginEntry(name, entry) {
|
||||
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||
const manifest = readManifest(pluginDir)
|
||||
const source = entry?.source ?? `github:quartz-community/${name}`
|
||||
const pluginEntry = {
|
||||
source,
|
||||
enabled: manifest?.defaultEnabled ?? true,
|
||||
options: manifest?.defaultOptions ?? {},
|
||||
order: manifest?.defaultOrder ?? 50,
|
||||
}
|
||||
|
||||
if (manifest?.components) {
|
||||
const component = Object.values(manifest.components).find((comp) => comp?.defaultPosition)
|
||||
if (component?.defaultPosition) {
|
||||
pluginEntry.layout = {
|
||||
position: component.defaultPosition,
|
||||
priority: component.defaultPriority ?? 50,
|
||||
display: "all",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pluginEntry
|
||||
}
|
||||
|
||||
export async function handleMigrate() {
|
||||
console.log(styleText("cyan", "Migrating Quartz configuration..."))
|
||||
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
console.log(styleText("red", "✗ quartz.config.ts not found. Aborting migration."))
|
||||
return
|
||||
}
|
||||
|
||||
if (fs.existsSync(PLUGINS_JSON_PATH)) {
|
||||
console.log(styleText("yellow", "⚠ quartz.plugins.json already exists. Overwriting."))
|
||||
}
|
||||
|
||||
const defaultJson = readJson(DEFAULT_PLUGINS_JSON_PATH)
|
||||
let configuration = defaultJson?.configuration ?? {}
|
||||
let layout = ensureLayoutDefaults(defaultJson?.layout ?? {})
|
||||
let layoutInfo = null
|
||||
|
||||
console.log(styleText("gray", "→ Extracting configuration..."))
|
||||
if (hasTsx()) {
|
||||
const extracted = extractWithTsx()
|
||||
if (extracted.ok) {
|
||||
configuration = extracted.data?.configuration ?? configuration
|
||||
layoutInfo = extracted.data?.layoutInfo ?? null
|
||||
} else {
|
||||
console.log(styleText("yellow", "⚠ Failed to import TS config with tsx. Using defaults."))
|
||||
}
|
||||
} else {
|
||||
console.log(styleText("yellow", "⚠ tsx not found. Using defaults."))
|
||||
}
|
||||
|
||||
if (layoutInfo?.pageTypes?.length) {
|
||||
for (const pageType of layoutInfo.pageTypes) {
|
||||
if (!layout.byPageType[pageType]) {
|
||||
layout.byPageType[pageType] = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(styleText("gray", "→ Reading plugin lockfile..."))
|
||||
const lockfile = readJson(LOCKFILE_PATH)
|
||||
const plugins = []
|
||||
|
||||
if (lockfile?.plugins) {
|
||||
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
||||
plugins.push(buildPluginEntry(name, entry))
|
||||
}
|
||||
} else if (defaultJson?.plugins) {
|
||||
console.log(styleText("yellow", "⚠ quartz.lock.json not found. Using default plugins."))
|
||||
for (const plugin of defaultJson.plugins) {
|
||||
plugins.push(plugin)
|
||||
}
|
||||
} else {
|
||||
console.log(styleText("yellow", "⚠ No lockfile or default plugins found. Writing empty list."))
|
||||
}
|
||||
|
||||
const outputJson = {
|
||||
$schema: "./quartz/plugins/quartz-plugins.schema.json",
|
||||
configuration,
|
||||
plugins,
|
||||
layout,
|
||||
}
|
||||
|
||||
fs.writeFileSync(PLUGINS_JSON_PATH, JSON.stringify(outputJson, null, 2) + "\n")
|
||||
|
||||
const configTemplate =
|
||||
'import { loadQuartzConfig } from "./quartz/plugins/loader/config-loader"\n' +
|
||||
"export default await loadQuartzConfig()\n"
|
||||
const layoutTemplate =
|
||||
'import { loadQuartzLayout } from "./quartz/plugins/loader/config-loader"\n' +
|
||||
"export const layout = await loadQuartzLayout()\n"
|
||||
|
||||
fs.writeFileSync(CONFIG_PATH, configTemplate)
|
||||
fs.writeFileSync(LAYOUT_PATH, layoutTemplate)
|
||||
|
||||
console.log(styleText("green", "✓ Created quartz.plugins.json"))
|
||||
console.log(styleText("green", "✓ Replaced quartz.config.ts"))
|
||||
console.log(styleText("green", "✓ Replaced quartz.layout.ts"))
|
||||
console.log()
|
||||
console.log(styleText("yellow", "⚠ Verify plugin options in quartz.plugins.json"))
|
||||
console.log(styleText("gray", `Plugins migrated: ${plugins.length}`))
|
||||
}
|
||||
@ -5,8 +5,48 @@ import { styleText } from "util"
|
||||
|
||||
const LOCKFILE_PATH = path.join(process.cwd(), "quartz.lock.json")
|
||||
const PLUGINS_DIR = path.join(process.cwd(), ".quartz", "plugins")
|
||||
const PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
|
||||
const INTERNAL_EXPORTS = new Set(["manifest", "default"])
|
||||
|
||||
function readPluginsJson() {
|
||||
if (!fs.existsSync(PLUGINS_JSON_PATH)) return null
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(PLUGINS_JSON_PATH, "utf-8"))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writePluginsJson(data) {
|
||||
fs.writeFileSync(PLUGINS_JSON_PATH, JSON.stringify(data, null, 2) + "\n")
|
||||
}
|
||||
|
||||
function extractPluginName(source) {
|
||||
if (source.startsWith("github:")) {
|
||||
const withoutPrefix = source.replace("github:", "")
|
||||
const [repoPath] = withoutPrefix.split("#")
|
||||
const parts = repoPath.split("/")
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
if (source.startsWith("git+") || source.startsWith("https://")) {
|
||||
const url = source.replace("git+", "")
|
||||
const match = url.match(/\/([^/]+?)(?:\.git)?(?:#|$)/)
|
||||
return match?.[1] ?? source
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
function readManifestFromPackageJson(pluginDir) {
|
||||
const pkgPath = path.join(pluginDir, "package.json")
|
||||
if (!fs.existsSync(pkgPath)) return null
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
||||
return pkg.quartz ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function buildPlugin(pluginDir, name) {
|
||||
try {
|
||||
console.log(styleText("cyan", ` → ${name}: installing dependencies...`))
|
||||
@ -255,7 +295,7 @@ export async function handlePluginAdd(sources) {
|
||||
installedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
addedPlugins.push({ name, pluginDir })
|
||||
addedPlugins.push({ name, pluginDir, source })
|
||||
console.log(styleText("green", `✓ Added ${name}@${commit.slice(0, 7)}`))
|
||||
} catch (error) {
|
||||
console.log(styleText("red", `✗ Failed to add ${source}: ${error}`))
|
||||
@ -274,6 +314,33 @@ export async function handlePluginAdd(sources) {
|
||||
}
|
||||
|
||||
writeLockfile(lockfile)
|
||||
const pluginsJson = readPluginsJson()
|
||||
if (pluginsJson?.plugins) {
|
||||
for (const { pluginDir, source } of addedPlugins) {
|
||||
const manifest = readManifestFromPackageJson(pluginDir)
|
||||
const newEntry = {
|
||||
source,
|
||||
enabled: manifest?.defaultEnabled ?? true,
|
||||
options: manifest?.defaultOptions ?? {},
|
||||
order: manifest?.defaultOrder ?? 50,
|
||||
}
|
||||
|
||||
if (manifest?.components) {
|
||||
const firstComponentKey = Object.keys(manifest.components)[0]
|
||||
const comp = manifest.components[firstComponentKey]
|
||||
if (comp?.defaultPosition) {
|
||||
newEntry.layout = {
|
||||
position: comp.defaultPosition,
|
||||
priority: comp.defaultPriority ?? 50,
|
||||
display: "all",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginsJson.plugins.push(newEntry)
|
||||
}
|
||||
writePluginsJson(pluginsJson)
|
||||
}
|
||||
console.log()
|
||||
console.log(styleText("gray", "Updated quartz.lock.json"))
|
||||
}
|
||||
@ -310,10 +377,174 @@ export async function handlePluginRemove(names) {
|
||||
}
|
||||
|
||||
writeLockfile(lockfile)
|
||||
const pluginsJson = readPluginsJson()
|
||||
if (pluginsJson?.plugins) {
|
||||
pluginsJson.plugins = pluginsJson.plugins.filter(
|
||||
(plugin) =>
|
||||
!names.includes(extractPluginName(plugin.source)) && !names.includes(plugin.source),
|
||||
)
|
||||
writePluginsJson(pluginsJson)
|
||||
}
|
||||
console.log()
|
||||
console.log(styleText("gray", "Updated quartz.lock.json"))
|
||||
}
|
||||
|
||||
export async function handlePluginEnable(names) {
|
||||
const json = readPluginsJson()
|
||||
if (!json) {
|
||||
console.log(styleText("red", "✗ No quartz.plugins.json found. Cannot enable plugins."))
|
||||
return
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
const entry = json.plugins.find(
|
||||
(e) => extractPluginName(e.source) === name || e.source === name,
|
||||
)
|
||||
if (!entry) {
|
||||
console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.plugins.json`))
|
||||
continue
|
||||
}
|
||||
if (entry.enabled) {
|
||||
console.log(styleText("gray", `✓ ${name} is already enabled`))
|
||||
continue
|
||||
}
|
||||
entry.enabled = true
|
||||
console.log(styleText("green", `✓ Enabled ${name}`))
|
||||
}
|
||||
|
||||
writePluginsJson(json)
|
||||
}
|
||||
|
||||
export async function handlePluginDisable(names) {
|
||||
const json = readPluginsJson()
|
||||
if (!json) {
|
||||
console.log(styleText("red", "✗ No quartz.plugins.json found. Cannot disable plugins."))
|
||||
return
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
const entry = json.plugins.find(
|
||||
(e) => extractPluginName(e.source) === name || e.source === name,
|
||||
)
|
||||
if (!entry) {
|
||||
console.log(styleText("yellow", `⚠ Plugin "${name}" not found in quartz.plugins.json`))
|
||||
continue
|
||||
}
|
||||
if (!entry.enabled) {
|
||||
console.log(styleText("gray", `✓ ${name} is already disabled`))
|
||||
continue
|
||||
}
|
||||
entry.enabled = false
|
||||
console.log(styleText("green", `✓ Disabled ${name}`))
|
||||
}
|
||||
|
||||
writePluginsJson(json)
|
||||
}
|
||||
|
||||
export async function handlePluginConfig(name, options = {}) {
|
||||
const json = readPluginsJson()
|
||||
if (!json) {
|
||||
console.log(styleText("red", "✗ No quartz.plugins.json found."))
|
||||
return
|
||||
}
|
||||
|
||||
const entry = json.plugins.find((e) => extractPluginName(e.source) === name || e.source === name)
|
||||
if (!entry) {
|
||||
console.log(styleText("red", `✗ Plugin "${name}" not found in quartz.plugins.json`))
|
||||
return
|
||||
}
|
||||
|
||||
if (options.set) {
|
||||
const eqIndex = options.set.indexOf("=")
|
||||
if (eqIndex === -1) {
|
||||
console.log(styleText("red", "✗ Invalid format. Use: --set key=value"))
|
||||
return
|
||||
}
|
||||
const key = options.set.slice(0, eqIndex)
|
||||
let value = options.set.slice(eqIndex + 1)
|
||||
|
||||
try {
|
||||
value = JSON.parse(value)
|
||||
} catch {}
|
||||
|
||||
if (!entry.options) entry.options = {}
|
||||
entry.options[key] = value
|
||||
writePluginsJson(json)
|
||||
console.log(styleText("green", `✓ Set ${name}.${key} = ${JSON.stringify(value)}`))
|
||||
} else {
|
||||
console.log(styleText("bold", `Plugin: ${name}`))
|
||||
console.log(` Source: ${entry.source}`)
|
||||
console.log(` Enabled: ${entry.enabled}`)
|
||||
console.log(` Order: ${entry.order ?? 50}`)
|
||||
if (entry.options && Object.keys(entry.options).length > 0) {
|
||||
console.log(` Options:`)
|
||||
for (const [k, v] of Object.entries(entry.options)) {
|
||||
console.log(` ${k}: ${JSON.stringify(v)}`)
|
||||
}
|
||||
} else {
|
||||
console.log(` Options: (none)`)
|
||||
}
|
||||
if (entry.layout) {
|
||||
console.log(` Layout:`)
|
||||
for (const [k, v] of Object.entries(entry.layout)) {
|
||||
console.log(` ${k}: ${JSON.stringify(v)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlePluginCheck() {
|
||||
const lockfile = readLockfile()
|
||||
if (!lockfile || Object.keys(lockfile.plugins).length === 0) {
|
||||
console.log(styleText("gray", "No plugins installed"))
|
||||
return
|
||||
}
|
||||
|
||||
console.log(styleText("bold", "Checking for plugin updates...\n"))
|
||||
|
||||
const results = []
|
||||
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
||||
try {
|
||||
const latestCommit = execSync(`git ls-remote ${entry.resolved} HEAD`, {
|
||||
encoding: "utf-8",
|
||||
})
|
||||
.split("\t")[0]
|
||||
.trim()
|
||||
|
||||
const isCurrent = latestCommit === entry.commit
|
||||
results.push({
|
||||
name,
|
||||
installed: entry.commit.slice(0, 7),
|
||||
latest: latestCommit.slice(0, 7),
|
||||
status: isCurrent ? "up to date" : "update available",
|
||||
})
|
||||
} catch {
|
||||
results.push({
|
||||
name,
|
||||
installed: entry.commit.slice(0, 7),
|
||||
latest: "?",
|
||||
status: "check failed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const nameWidth = Math.max(6, ...results.map((r) => r.name.length)) + 2
|
||||
const header = `${"Plugin".padEnd(nameWidth)}${"Installed".padEnd(12)}${"Latest".padEnd(12)}Status`
|
||||
console.log(styleText("bold", header))
|
||||
console.log("─".repeat(header.length))
|
||||
|
||||
for (const r of results) {
|
||||
const color =
|
||||
r.status === "up to date" ? "green" : r.status === "check failed" ? "red" : "yellow"
|
||||
console.log(
|
||||
`${r.name.padEnd(nameWidth)}${r.installed.padEnd(12)}${r.latest.padEnd(12)}${styleText(
|
||||
color,
|
||||
r.status,
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlePluginUpdate(names) {
|
||||
const lockfile = readLockfile()
|
||||
if (!lockfile) {
|
||||
|
||||
33
quartz/plugins/loader/conditions.ts
Normal file
33
quartz/plugins/loader/conditions.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { QuartzComponentProps } from "../../components/types"
|
||||
|
||||
export type ConditionPredicate = (props: QuartzComponentProps) => boolean
|
||||
|
||||
const builtinConditions: Record<string, ConditionPredicate> = {
|
||||
"not-index": (props) => props.fileData.slug !== "index",
|
||||
"has-tags": (props) => {
|
||||
const tags = props.fileData.frontmatter?.tags
|
||||
return Array.isArray(tags) && tags.length > 0
|
||||
},
|
||||
"has-backlinks": (props) => {
|
||||
const backlinks = (props.fileData as Record<string, unknown>).backlinks
|
||||
return Array.isArray(backlinks) && backlinks.length > 0
|
||||
},
|
||||
"has-toc": (props) => {
|
||||
const toc = (props.fileData as Record<string, unknown>).toc
|
||||
return Array.isArray(toc) && toc.length > 0
|
||||
},
|
||||
}
|
||||
|
||||
const customConditions = new Map<string, ConditionPredicate>()
|
||||
|
||||
export function registerCondition(name: string, predicate: ConditionPredicate): void {
|
||||
customConditions.set(name, predicate)
|
||||
}
|
||||
|
||||
export function getCondition(name: string): ConditionPredicate | undefined {
|
||||
return customConditions.get(name) ?? builtinConditions[name]
|
||||
}
|
||||
|
||||
export function getAllConditionNames(): string[] {
|
||||
return [...Object.keys(builtinConditions), ...customConditions.keys()]
|
||||
}
|
||||
680
quartz/plugins/loader/config-loader.ts
Normal file
680
quartz/plugins/loader/config-loader.ts
Normal file
@ -0,0 +1,680 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { styleText } from "util"
|
||||
import { QuartzConfig, GlobalConfiguration, FullPageLayout } from "../../cfg"
|
||||
import { QuartzComponent } from "../../components/types"
|
||||
import { PluginTypes } from "../types"
|
||||
import {
|
||||
PluginManifest,
|
||||
PluginJsonEntry,
|
||||
QuartzPluginsJson,
|
||||
LayoutConfig,
|
||||
PluginLayoutDeclaration,
|
||||
FlexGroupConfig,
|
||||
PluginCategory,
|
||||
} from "./types"
|
||||
import { parsePluginSource, installPlugin, getPluginEntryPoint } from "./gitLoader"
|
||||
import { loadComponentsFromPackage } from "./componentLoader"
|
||||
import { componentRegistry } from "../../components/registry"
|
||||
import { getCondition } from "./conditions"
|
||||
|
||||
const PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
|
||||
|
||||
function readPluginsJson(): QuartzPluginsJson | null {
|
||||
if (!fs.existsSync(PLUGINS_JSON_PATH)) {
|
||||
return null
|
||||
}
|
||||
const raw = fs.readFileSync(PLUGINS_JSON_PATH, "utf-8")
|
||||
return JSON.parse(raw) as QuartzPluginsJson
|
||||
}
|
||||
|
||||
function extractPluginName(source: string): string {
|
||||
if (source.startsWith("github:")) {
|
||||
const withoutPrefix = source.replace("github:", "")
|
||||
const [repoPath] = withoutPrefix.split("#")
|
||||
const parts = repoPath.split("/")
|
||||
return parts[parts.length - 1]
|
||||
}
|
||||
if (source.startsWith("git+") || source.startsWith("https://")) {
|
||||
const url = source.replace("git+", "")
|
||||
const match = url.match(/\/([^/]+?)(?:\.git)?(?:#|$)/)
|
||||
return match?.[1] ?? source
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
interface DependencyValidationResult {
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
function validateDependencies(
|
||||
entries: PluginJsonEntry[],
|
||||
manifests: Map<string, PluginManifest>,
|
||||
): DependencyValidationResult {
|
||||
const errors: string[] = []
|
||||
const warnings: string[] = []
|
||||
|
||||
const sourceToEntry = new Map<string, PluginJsonEntry>()
|
||||
const nameToSource = new Map<string, string>()
|
||||
for (const entry of entries) {
|
||||
sourceToEntry.set(entry.source, entry)
|
||||
nameToSource.set(extractPluginName(entry.source), entry.source)
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.enabled) continue
|
||||
const manifest = manifests.get(entry.source)
|
||||
if (!manifest?.dependencies?.length) continue
|
||||
|
||||
const pluginName = manifest.displayName || extractPluginName(entry.source)
|
||||
const pluginOrder = entry.order ?? manifest.defaultOrder ?? 50
|
||||
|
||||
for (const dep of manifest.dependencies) {
|
||||
const depEntry = sourceToEntry.get(dep)
|
||||
const depName = extractPluginName(dep)
|
||||
|
||||
if (!depEntry) {
|
||||
errors.push(
|
||||
`Plugin "${pluginName}" requires "${depName}". Run: npx quartz plugin add ${dep}`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!depEntry.enabled) {
|
||||
warnings.push(
|
||||
`Plugin "${pluginName}" depends on "${depName}" which is disabled. "${pluginName}" may not function correctly.`,
|
||||
)
|
||||
}
|
||||
|
||||
const depManifest = manifests.get(dep)
|
||||
const depOrder = depEntry.order ?? depManifest?.defaultOrder ?? 50
|
||||
|
||||
if (pluginOrder < depOrder) {
|
||||
errors.push(
|
||||
`Plugin "${pluginName}" (order: ${pluginOrder}) depends on "${depName}" (order: ${depOrder}), ` +
|
||||
`but "${pluginName}" is configured to run first. Either increase "${pluginName}"'s order above ${depOrder} ` +
|
||||
`or decrease "${depName}"'s order below ${pluginOrder}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Circular dependency detection
|
||||
const graph = new Map<string, string[]>()
|
||||
for (const entry of entries) {
|
||||
const manifest = manifests.get(entry.source)
|
||||
if (manifest?.dependencies?.length) {
|
||||
graph.set(entry.source, manifest.dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
const visited = new Set<string>()
|
||||
const inStack = new Set<string>()
|
||||
|
||||
function detectCycle(node: string, pathSoFar: string[]): string[] | null {
|
||||
if (inStack.has(node)) {
|
||||
const cycleStart = pathSoFar.indexOf(node)
|
||||
return pathSoFar.slice(cycleStart).concat(node)
|
||||
}
|
||||
if (visited.has(node)) return null
|
||||
|
||||
visited.add(node)
|
||||
inStack.add(node)
|
||||
|
||||
for (const dep of graph.get(node) ?? []) {
|
||||
const cycle = detectCycle(dep, [...pathSoFar, node])
|
||||
if (cycle) return cycle
|
||||
}
|
||||
|
||||
inStack.delete(node)
|
||||
return null
|
||||
}
|
||||
|
||||
for (const node of graph.keys()) {
|
||||
const cycle = detectCycle(node, [])
|
||||
if (cycle) {
|
||||
const names = cycle.map(extractPluginName)
|
||||
errors.push(`Circular dependency detected: ${names.join(" → ")}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { errors, warnings }
|
||||
}
|
||||
|
||||
async function resolvePluginManifest(source: string): Promise<PluginManifest | null> {
|
||||
try {
|
||||
const gitSpec = parsePluginSource(source)
|
||||
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
||||
const module = await import(entryPoint)
|
||||
return module.manifest ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function readManifestFromPackageJson(source: string): Promise<PluginManifest | null> {
|
||||
try {
|
||||
const gitSpec = parsePluginSource(source)
|
||||
const pluginDir = path.join(process.cwd(), ".quartz", "plugins", gitSpec.name)
|
||||
const pkgPath = path.join(pluginDir, "package.json")
|
||||
if (!fs.existsSync(pkgPath)) return null
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
||||
if (!pkg.quartz) return null
|
||||
|
||||
const q = pkg.quartz
|
||||
return {
|
||||
name: q.name ?? gitSpec.name,
|
||||
displayName: q.displayName ?? q.name ?? gitSpec.name,
|
||||
description: q.description ?? pkg.description ?? "No description",
|
||||
version: q.version ?? pkg.version ?? "1.0.0",
|
||||
author: q.author ?? pkg.author,
|
||||
homepage: q.homepage ?? pkg.homepage,
|
||||
category: q.category,
|
||||
quartzVersion: q.quartzVersion,
|
||||
dependencies: q.dependencies,
|
||||
defaultOrder: q.defaultOrder,
|
||||
defaultEnabled: q.defaultEnabled,
|
||||
defaultOptions: q.defaultOptions,
|
||||
configSchema: q.configSchema,
|
||||
components: q.components,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function getManifest(source: string): Promise<PluginManifest | null> {
|
||||
// Try package.json quartz field first (preferred), then fall back to manifest.ts export
|
||||
return (await readManifestFromPackageJson(source)) ?? (await resolvePluginManifest(source))
|
||||
}
|
||||
|
||||
export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
||||
const json = readPluginsJson()
|
||||
|
||||
if (!json) {
|
||||
// Fallback: import old-style config directly
|
||||
const oldConfig = await import("../../../quartz.config")
|
||||
return oldConfig.default
|
||||
}
|
||||
|
||||
const configuration = json.configuration as unknown as GlobalConfiguration
|
||||
|
||||
const enabledEntries = json.plugins.filter((e) => e.enabled)
|
||||
const manifests = new Map<string, PluginManifest>()
|
||||
|
||||
// Ensure all plugins are installed and collect manifests
|
||||
for (const entry of enabledEntries) {
|
||||
try {
|
||||
const gitSpec = parsePluginSource(entry.source)
|
||||
await installPlugin(gitSpec, { verbose: false })
|
||||
|
||||
const manifest = await getManifest(entry.source)
|
||||
if (manifest) {
|
||||
manifests.set(entry.source, manifest)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
styleText("red", `✗`) +
|
||||
` Failed to install plugin: ${styleText("yellow", entry.source)}\n` +
|
||||
` ${err instanceof Error ? err.message : String(err)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate dependencies
|
||||
const validation = validateDependencies(enabledEntries, manifests)
|
||||
for (const warning of validation.warnings) {
|
||||
console.warn(styleText("yellow", `⚠`) + ` ${warning}`)
|
||||
}
|
||||
if (validation.errors.length > 0) {
|
||||
for (const error of validation.errors) {
|
||||
console.error(styleText("red", `✗`) + ` ${error}`)
|
||||
}
|
||||
throw new Error(
|
||||
`Plugin dependency validation failed with ${validation.errors.length} error(s). See above for details.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Categorize and sort plugins
|
||||
const transformers: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[] = []
|
||||
const filters: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[] = []
|
||||
const emitters: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[] = []
|
||||
const pageTypes: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[] = []
|
||||
|
||||
for (const entry of enabledEntries) {
|
||||
const manifest = manifests.get(entry.source)
|
||||
const category = manifest?.category
|
||||
|
||||
switch (category) {
|
||||
case "transformer":
|
||||
transformers.push({ entry, manifest })
|
||||
break
|
||||
case "filter":
|
||||
filters.push({ entry, manifest })
|
||||
break
|
||||
case "emitter":
|
||||
emitters.push({ entry, manifest })
|
||||
break
|
||||
case "pageType":
|
||||
pageTypes.push({ entry, manifest })
|
||||
break
|
||||
default: {
|
||||
// Try to detect category from the loaded module
|
||||
const gitSpec = parsePluginSource(entry.source)
|
||||
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
||||
try {
|
||||
const module = await import(entryPoint)
|
||||
const detected = detectCategoryFromModule(module)
|
||||
if (detected) {
|
||||
const target = {
|
||||
transformer: transformers,
|
||||
filter: filters,
|
||||
emitter: emitters,
|
||||
pageType: pageTypes,
|
||||
}[detected]
|
||||
target.push({ entry, manifest })
|
||||
} else {
|
||||
console.warn(
|
||||
styleText("yellow", `⚠`) +
|
||||
` Could not determine category for plugin "${extractPluginName(entry.source)}". Skipping.`,
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
console.warn(
|
||||
styleText("yellow", `⚠`) +
|
||||
` Could not load plugin "${extractPluginName(entry.source)}" to detect category. Skipping.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by order within each category
|
||||
const sortByOrder = (
|
||||
a: { entry: PluginJsonEntry; manifest: PluginManifest | undefined },
|
||||
b: { entry: PluginJsonEntry; manifest: PluginManifest | undefined },
|
||||
) => {
|
||||
const orderA = a.entry.order ?? a.manifest?.defaultOrder ?? 50
|
||||
const orderB = b.entry.order ?? b.manifest?.defaultOrder ?? 50
|
||||
return orderA - orderB
|
||||
}
|
||||
|
||||
transformers.sort(sortByOrder)
|
||||
filters.sort(sortByOrder)
|
||||
emitters.sort(sortByOrder)
|
||||
pageTypes.sort(sortByOrder)
|
||||
|
||||
// Instantiate plugins
|
||||
const instantiate = async (
|
||||
items: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[],
|
||||
) => {
|
||||
const instances = []
|
||||
for (const { entry, manifest } of items) {
|
||||
try {
|
||||
const gitSpec = parsePluginSource(entry.source)
|
||||
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
||||
const module = await import(entryPoint)
|
||||
|
||||
// Load components if declared
|
||||
if (manifest?.components && Object.keys(manifest.components).length > 0) {
|
||||
await loadComponentsFromPackage(entryPoint, manifest)
|
||||
}
|
||||
|
||||
const factory = module.default ?? module.plugin
|
||||
if (typeof factory !== "function") {
|
||||
console.warn(
|
||||
styleText("yellow", `⚠`) +
|
||||
` Plugin "${extractPluginName(entry.source)}" has no factory function. Skipping.`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Merge default options with user options
|
||||
const options = { ...manifest?.defaultOptions, ...entry.options }
|
||||
instances.push(factory(Object.keys(options).length > 0 ? options : undefined))
|
||||
} catch (err) {
|
||||
console.error(
|
||||
styleText("red", `✗`) +
|
||||
` Failed to instantiate plugin "${extractPluginName(entry.source)}": ${err instanceof Error ? err.message : String(err)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
return instances
|
||||
}
|
||||
|
||||
// Import built-in plugins
|
||||
const builtinPlugins = await import("../index")
|
||||
const builtinTransformers = [builtinPlugins.FrontMatter()]
|
||||
const builtinEmitters = [
|
||||
builtinPlugins.ComponentResources(),
|
||||
builtinPlugins.Assets(),
|
||||
builtinPlugins.Static(),
|
||||
]
|
||||
const builtinPageTypes = [builtinPlugins.PageTypes.NotFoundPageType()]
|
||||
|
||||
const plugins: PluginTypes = {
|
||||
transformers: [...builtinTransformers, ...(await instantiate(transformers))],
|
||||
filters: await instantiate(filters),
|
||||
emitters: [...builtinEmitters, ...(await instantiate(emitters))],
|
||||
pageTypes: [...(await instantiate(pageTypes)), ...builtinPageTypes],
|
||||
}
|
||||
|
||||
return {
|
||||
configuration,
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
function detectCategoryFromModule(module: unknown): PluginCategory | null {
|
||||
if (!module || typeof module !== "object") return null
|
||||
const mod = module as Record<string, unknown>
|
||||
|
||||
if (typeof mod.default === "function") {
|
||||
// Try to instantiate and inspect
|
||||
try {
|
||||
const instance = (mod.default as Function)()
|
||||
if (instance && typeof instance === "object") {
|
||||
if ("match" in instance && "body" in instance && "layout" in instance) return "pageType"
|
||||
if ("emit" in instance) return "emitter"
|
||||
if ("shouldPublish" in instance) return "filter"
|
||||
if (
|
||||
"textTransform" in instance ||
|
||||
"markdownPlugins" in instance ||
|
||||
"htmlPlugins" in instance
|
||||
)
|
||||
return "transformer"
|
||||
}
|
||||
} catch {
|
||||
// Couldn't instantiate, skip detection
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function loadQuartzLayout(): Promise<{
|
||||
defaults: Partial<FullPageLayout>
|
||||
byPageType: Record<string, Partial<FullPageLayout>>
|
||||
}> {
|
||||
const json = readPluginsJson()
|
||||
|
||||
if (!json) {
|
||||
// Fallback: import old-style layout directly
|
||||
const oldLayout = await import("../../../quartz.layout")
|
||||
return oldLayout.layout
|
||||
}
|
||||
|
||||
const enabledWithLayout = json.plugins.filter((e) => e.enabled && e.layout)
|
||||
const layoutConfig = json.layout ?? {}
|
||||
|
||||
// Build default layout for all page types
|
||||
const defaultLayout = buildLayoutForEntries(enabledWithLayout, layoutConfig)
|
||||
|
||||
// Build per-page-type overrides
|
||||
const byPageType: Record<string, Partial<FullPageLayout>> = {}
|
||||
if (layoutConfig.byPageType) {
|
||||
for (const [pageType, override] of Object.entries(layoutConfig.byPageType)) {
|
||||
let filteredEntries = enabledWithLayout
|
||||
|
||||
// Apply exclusions
|
||||
if (override.exclude?.length) {
|
||||
filteredEntries = filteredEntries.filter((e) => {
|
||||
const name = extractPluginName(e.source)
|
||||
return !override.exclude!.includes(name)
|
||||
})
|
||||
}
|
||||
|
||||
const ptLayout = buildLayoutForEntries(filteredEntries, layoutConfig)
|
||||
|
||||
// Apply position overrides (empty array = clear position)
|
||||
if (override.positions) {
|
||||
for (const [pos, components] of Object.entries(override.positions)) {
|
||||
if (Array.isArray(components) && components.length === 0) {
|
||||
const key = pos as keyof Pick<
|
||||
FullPageLayout,
|
||||
"left" | "right" | "beforeBody" | "afterBody"
|
||||
>
|
||||
if (key in ptLayout) {
|
||||
;(ptLayout as Record<string, unknown>)[key] = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byPageType[pageType] = ptLayout
|
||||
}
|
||||
}
|
||||
|
||||
// Add Head (built-in) and Footer (plugin)
|
||||
const HeadModule = await import("../../components/Head")
|
||||
const head = HeadModule.default()
|
||||
|
||||
// Find footer plugin
|
||||
const footerEntry = json.plugins.find(
|
||||
(e) => e.enabled && extractPluginName(e.source) === "footer",
|
||||
)
|
||||
let footer: QuartzComponent | undefined
|
||||
if (footerEntry) {
|
||||
try {
|
||||
const gitSpec = parsePluginSource(footerEntry.source)
|
||||
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
||||
const module = await import(entryPoint)
|
||||
const factory = module.default ?? module.plugin
|
||||
if (typeof factory === "function") {
|
||||
const options = { ...footerEntry.options }
|
||||
footer = factory(Object.keys(options).length > 0 ? options : undefined)
|
||||
}
|
||||
} catch {
|
||||
// Footer not available
|
||||
}
|
||||
}
|
||||
|
||||
// Apply structural defaults
|
||||
defaultLayout.head = head
|
||||
defaultLayout.header = defaultLayout.header ?? []
|
||||
if (footer) {
|
||||
defaultLayout.footer = footer
|
||||
}
|
||||
|
||||
// Ensure all byPageType entries inherit structural slots
|
||||
for (const pageType of Object.keys(byPageType)) {
|
||||
const pt = byPageType[pageType]
|
||||
if (!pt.head) pt.head = head
|
||||
if (!pt.header) pt.header = []
|
||||
if (footer && !pt.footer) pt.footer = footer
|
||||
}
|
||||
|
||||
return { defaults: defaultLayout, byPageType }
|
||||
}
|
||||
|
||||
function buildLayoutForEntries(
|
||||
entries: PluginJsonEntry[],
|
||||
layoutConfig: LayoutConfig,
|
||||
): Partial<FullPageLayout> {
|
||||
const positions: Record<
|
||||
string,
|
||||
{
|
||||
component: QuartzComponent
|
||||
priority: number
|
||||
group?: string
|
||||
groupOptions?: PluginLayoutDeclaration["groupOptions"]
|
||||
}[]
|
||||
> = {
|
||||
left: [],
|
||||
right: [],
|
||||
beforeBody: [],
|
||||
afterBody: [],
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.layout) continue
|
||||
|
||||
const layout = entry.layout
|
||||
const name = extractPluginName(entry.source)
|
||||
|
||||
// Look up component from registry
|
||||
const registered =
|
||||
componentRegistry.get(name) ?? componentRegistry.get(`${entry.source}/${name}`)
|
||||
if (!registered) {
|
||||
// Try common naming patterns
|
||||
const pascalName = name
|
||||
.split("-")
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join("")
|
||||
const altRegistered = componentRegistry.get(pascalName)
|
||||
if (!altRegistered) continue
|
||||
}
|
||||
|
||||
const reg =
|
||||
registered ??
|
||||
componentRegistry.get(
|
||||
name
|
||||
.split("-")
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join(""),
|
||||
)
|
||||
if (!reg) continue
|
||||
|
||||
let component: QuartzComponent
|
||||
if (typeof reg.component === "function" && !("displayName" in reg.component)) {
|
||||
// It's a constructor, instantiate with options
|
||||
const opts = { ...entry.options }
|
||||
component = (reg.component as Function)(
|
||||
Object.keys(opts).length > 0 ? opts : undefined,
|
||||
) as QuartzComponent
|
||||
} else {
|
||||
component = reg.component as QuartzComponent
|
||||
}
|
||||
|
||||
// Apply display modifier
|
||||
if (layout.display && layout.display !== "all") {
|
||||
component = applyDisplayWrapper(component, layout.display)
|
||||
}
|
||||
|
||||
// Apply condition
|
||||
if (layout.condition) {
|
||||
component = applyConditionWrapper(component, layout.condition)
|
||||
}
|
||||
|
||||
const posArray = positions[layout.position]
|
||||
if (posArray) {
|
||||
posArray.push({
|
||||
component,
|
||||
priority: layout.priority,
|
||||
group: layout.group,
|
||||
groupOptions: layout.groupOptions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by priority and resolve groups
|
||||
const result: Partial<FullPageLayout> = {}
|
||||
|
||||
for (const [position, items] of Object.entries(positions)) {
|
||||
items.sort((a, b) => a.priority - b.priority)
|
||||
|
||||
const resolved = resolveGroups(items, layoutConfig.groups ?? {})
|
||||
const key = position as keyof Pick<
|
||||
FullPageLayout,
|
||||
"left" | "right" | "beforeBody" | "afterBody"
|
||||
>
|
||||
;(result as Record<string, QuartzComponent[]>)[key] = resolved
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function resolveGroups(
|
||||
items: {
|
||||
component: QuartzComponent
|
||||
priority: number
|
||||
group?: string
|
||||
groupOptions?: PluginLayoutDeclaration["groupOptions"]
|
||||
}[],
|
||||
groups: Record<string, FlexGroupConfig>,
|
||||
): QuartzComponent[] {
|
||||
const result: QuartzComponent[] = []
|
||||
const groupedComponents = new Map<
|
||||
string,
|
||||
{ component: QuartzComponent; groupOptions?: PluginLayoutDeclaration["groupOptions"] }[]
|
||||
>()
|
||||
const groupInsertionOrder: { name: string; priority: number }[] = []
|
||||
|
||||
for (const item of items) {
|
||||
if (item.group) {
|
||||
if (!groupedComponents.has(item.group)) {
|
||||
groupedComponents.set(item.group, [])
|
||||
groupInsertionOrder.push({ name: item.group, priority: item.priority })
|
||||
}
|
||||
groupedComponents.get(item.group)!.push({
|
||||
component: item.component,
|
||||
groupOptions: item.groupOptions,
|
||||
})
|
||||
} else {
|
||||
result.push(item.component)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert flex groups at the position of their first member
|
||||
for (const { name: groupName } of groupInsertionOrder) {
|
||||
const members = groupedComponents.get(groupName)!
|
||||
const groupConfig = groups[groupName] ?? {}
|
||||
|
||||
const flexComponents = members.map((m) => ({
|
||||
Component: m.component,
|
||||
grow: m.groupOptions?.grow,
|
||||
shrink: m.groupOptions?.shrink,
|
||||
basis: m.groupOptions?.basis,
|
||||
order: m.groupOptions?.order,
|
||||
align: m.groupOptions?.align,
|
||||
justify: m.groupOptions?.justify,
|
||||
}))
|
||||
|
||||
// Dynamically import Flex to avoid circular dependencies
|
||||
const FlexModule = require("../../components/Flex")
|
||||
const Flex = FlexModule.default as Function
|
||||
const flexComponent = Flex({
|
||||
components: flexComponents,
|
||||
direction: groupConfig.direction ?? "row",
|
||||
wrap: groupConfig.wrap,
|
||||
gap: groupConfig.gap ?? "1rem",
|
||||
}) as QuartzComponent
|
||||
|
||||
result.push(flexComponent)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function applyDisplayWrapper(
|
||||
component: QuartzComponent,
|
||||
display: "mobile-only" | "desktop-only",
|
||||
): QuartzComponent {
|
||||
if (display === "mobile-only") {
|
||||
const MobileOnly = require("../../components/MobileOnly").default as Function
|
||||
return MobileOnly(component) as QuartzComponent
|
||||
} else {
|
||||
const DesktopOnly = require("../../components/DesktopOnly").default as Function
|
||||
return DesktopOnly(component) as QuartzComponent
|
||||
}
|
||||
}
|
||||
|
||||
function applyConditionWrapper(component: QuartzComponent, conditionName: string): QuartzComponent {
|
||||
const predicate = getCondition(conditionName)
|
||||
if (!predicate) {
|
||||
console.warn(
|
||||
styleText("yellow", `⚠`) +
|
||||
` Unknown condition "${conditionName}". Component will always render.`,
|
||||
)
|
||||
return component
|
||||
}
|
||||
|
||||
const ConditionalRender = require("../../components/ConditionalRender").default as Function
|
||||
return ConditionalRender({
|
||||
component,
|
||||
condition: predicate,
|
||||
}) as QuartzComponent
|
||||
}
|
||||
@ -1,2 +1,4 @@
|
||||
export * from "./types"
|
||||
export * from "./index"
|
||||
export * from "./conditions"
|
||||
export { loadQuartzConfig, loadQuartzLayout } from "./config-loader"
|
||||
|
||||
@ -6,6 +6,12 @@ import {
|
||||
} from "../types"
|
||||
import { BuildCtx } from "../../util/ctx"
|
||||
|
||||
export type PluginCategory = "transformer" | "filter" | "emitter" | "pageType"
|
||||
|
||||
export type LayoutPosition = "left" | "right" | "beforeBody" | "afterBody"
|
||||
|
||||
export type LayoutDisplay = "all" | "mobile-only" | "desktop-only"
|
||||
|
||||
/**
|
||||
* Component manifest metadata
|
||||
*/
|
||||
@ -20,7 +26,20 @@ export interface ComponentManifest {
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin manifest metadata for discovery and documentation
|
||||
* Layout defaults for a component declared in a plugin manifest.
|
||||
* These are used as fallback values when no user layout config is specified.
|
||||
*/
|
||||
export interface ComponentLayoutDefaults {
|
||||
displayName: string
|
||||
description?: string
|
||||
defaultPosition?: LayoutPosition
|
||||
defaultPriority?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin manifest metadata for discovery and documentation.
|
||||
*
|
||||
* This corresponds to the `quartz` field in a plugin's `package.json`.
|
||||
*/
|
||||
export interface PluginManifest {
|
||||
name: string
|
||||
@ -30,11 +49,20 @@ export interface PluginManifest {
|
||||
author?: string
|
||||
homepage?: string
|
||||
keywords?: string[]
|
||||
category?: "transformer" | "filter" | "emitter" | "pageType"
|
||||
category?: PluginCategory
|
||||
quartzVersion?: string
|
||||
/** Plugin sources this plugin depends on (e.g., "github:quartz-community/crawl-links") */
|
||||
dependencies?: string[]
|
||||
/** Default numeric execution order (0-100 convention, lower = runs first). Defaults to 50. */
|
||||
defaultOrder?: number
|
||||
/** Whether the plugin is enabled by default on install. Defaults to true. */
|
||||
defaultEnabled?: boolean
|
||||
/** Default options applied when no user options are specified */
|
||||
defaultOptions?: Record<string, unknown>
|
||||
/** JSON Schema for the plugin's options object, used for validation and TUI generation */
|
||||
configSchema?: object
|
||||
/** Components provided by this plugin */
|
||||
components?: Record<string, ComponentManifest>
|
||||
/** Components provided by this plugin, keyed by component export name */
|
||||
components?: Record<string, ComponentManifest & ComponentLayoutDefaults>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,7 +71,7 @@ export interface PluginManifest {
|
||||
export interface LoadedPlugin {
|
||||
plugin: QuartzTransformerPlugin | QuartzFilterPlugin | QuartzEmitterPlugin | QuartzPageTypePlugin
|
||||
manifest: PluginManifest
|
||||
type: "transformer" | "filter" | "emitter" | "pageType"
|
||||
type: PluginCategory
|
||||
source: string
|
||||
}
|
||||
|
||||
@ -91,3 +119,56 @@ export type PluginSpecifier =
|
||||
| string
|
||||
| { name: string; options?: unknown }
|
||||
| { plugin: LoadedPlugin["plugin"]; manifest?: Partial<PluginManifest> }
|
||||
|
||||
/** Layout declaration for a component-providing plugin in quartz.plugins.json */
|
||||
export interface PluginLayoutDeclaration {
|
||||
position: LayoutPosition
|
||||
priority: number
|
||||
display?: LayoutDisplay
|
||||
condition?: string
|
||||
group?: string
|
||||
groupOptions?: {
|
||||
grow?: boolean
|
||||
shrink?: boolean
|
||||
basis?: string
|
||||
order?: number
|
||||
align?: "start" | "end" | "center" | "stretch"
|
||||
justify?: "start" | "end" | "center" | "between" | "around"
|
||||
}
|
||||
}
|
||||
|
||||
/** A single plugin entry in quartz.plugins.json */
|
||||
export interface PluginJsonEntry {
|
||||
source: string
|
||||
enabled: boolean
|
||||
options?: Record<string, unknown>
|
||||
order?: number
|
||||
layout?: PluginLayoutDeclaration
|
||||
}
|
||||
|
||||
/** Flex group configuration in the top-level layout section */
|
||||
export interface FlexGroupConfig {
|
||||
direction?: "row" | "row-reverse" | "column" | "column-reverse"
|
||||
wrap?: "nowrap" | "wrap" | "wrap-reverse"
|
||||
gap?: string
|
||||
}
|
||||
|
||||
/** Per-page-type layout overrides */
|
||||
export interface PageTypeLayoutOverride {
|
||||
exclude?: string[]
|
||||
positions?: Partial<Record<LayoutPosition, PluginLayoutDeclaration[]>>
|
||||
}
|
||||
|
||||
/** Top-level layout section of quartz.plugins.json */
|
||||
export interface LayoutConfig {
|
||||
groups?: Record<string, FlexGroupConfig>
|
||||
byPageType?: Record<string, PageTypeLayoutOverride>
|
||||
}
|
||||
|
||||
/** Root type for quartz.plugins.json */
|
||||
export interface QuartzPluginsJson {
|
||||
$schema?: string
|
||||
configuration: Record<string, unknown>
|
||||
plugins: PluginJsonEntry[]
|
||||
layout?: LayoutConfig
|
||||
}
|
||||
|
||||
321
quartz/plugins/quartz-plugins.schema.json
Normal file
321
quartz/plugins/quartz-plugins.schema.json
Normal file
@ -0,0 +1,321 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Quartz Plugins Configuration Schema",
|
||||
"description": "Schema for validating quartz.plugins.json configuration files",
|
||||
"type": "object",
|
||||
"required": ["configuration", "plugins"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"description": "JSON Schema reference"
|
||||
},
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"required": ["pageTitle", "enableSPA", "locale", "theme"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pageTitle": {
|
||||
"type": "string",
|
||||
"description": "The title of the website"
|
||||
},
|
||||
"pageTitleSuffix": {
|
||||
"type": "string",
|
||||
"description": "Suffix appended to page titles"
|
||||
},
|
||||
"enableSPA": {
|
||||
"type": "boolean",
|
||||
"description": "Enable single-page application mode"
|
||||
},
|
||||
"enablePopovers": {
|
||||
"type": "boolean",
|
||||
"description": "Enable hover popovers for links"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string",
|
||||
"description": "Locale code for the site"
|
||||
},
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"description": "Base URL for the site"
|
||||
},
|
||||
"theme": {
|
||||
"type": "object",
|
||||
"required": ["fontOrigin", "cdnCaching", "typography", "colors"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"fontOrigin": {
|
||||
"type": "string",
|
||||
"enum": ["googleFonts", "local"],
|
||||
"description": "Source of fonts"
|
||||
},
|
||||
"cdnCaching": {
|
||||
"type": "boolean",
|
||||
"description": "Enable CDN caching"
|
||||
},
|
||||
"typography": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"header": {
|
||||
"type": "string",
|
||||
"description": "Font family for headers"
|
||||
},
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "Font family for body text"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Font family for code"
|
||||
}
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"type": "object",
|
||||
"required": ["lightMode", "darkMode"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"lightMode": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"light": {
|
||||
"type": "string",
|
||||
"description": "Light color"
|
||||
},
|
||||
"lightgray": {
|
||||
"type": "string",
|
||||
"description": "Light gray color"
|
||||
},
|
||||
"gray": {
|
||||
"type": "string",
|
||||
"description": "Gray color"
|
||||
},
|
||||
"darkgray": {
|
||||
"type": "string",
|
||||
"description": "Dark gray color"
|
||||
},
|
||||
"dark": {
|
||||
"type": "string",
|
||||
"description": "Dark color"
|
||||
},
|
||||
"secondary": {
|
||||
"type": "string",
|
||||
"description": "Secondary color"
|
||||
},
|
||||
"tertiary": {
|
||||
"type": "string",
|
||||
"description": "Tertiary color"
|
||||
},
|
||||
"highlight": {
|
||||
"type": "string",
|
||||
"description": "Highlight color"
|
||||
},
|
||||
"textHighlight": {
|
||||
"type": "string",
|
||||
"description": "Text highlight color"
|
||||
}
|
||||
}
|
||||
},
|
||||
"darkMode": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"light": {
|
||||
"type": "string",
|
||||
"description": "Light color"
|
||||
},
|
||||
"lightgray": {
|
||||
"type": "string",
|
||||
"description": "Light gray color"
|
||||
},
|
||||
"gray": {
|
||||
"type": "string",
|
||||
"description": "Gray color"
|
||||
},
|
||||
"darkgray": {
|
||||
"type": "string",
|
||||
"description": "Dark gray color"
|
||||
},
|
||||
"dark": {
|
||||
"type": "string",
|
||||
"description": "Dark color"
|
||||
},
|
||||
"secondary": {
|
||||
"type": "string",
|
||||
"description": "Secondary color"
|
||||
},
|
||||
"tertiary": {
|
||||
"type": "string",
|
||||
"description": "Tertiary color"
|
||||
},
|
||||
"highlight": {
|
||||
"type": "string",
|
||||
"description": "Highlight color"
|
||||
},
|
||||
"textHighlight": {
|
||||
"type": "string",
|
||||
"description": "Text highlight color"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"analytics": {
|
||||
"type": "object",
|
||||
"description": "Analytics configuration"
|
||||
},
|
||||
"ignorePatterns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Patterns to ignore during processing"
|
||||
},
|
||||
"defaultDateType": {
|
||||
"type": "string",
|
||||
"enum": ["created", "modified", "published"],
|
||||
"description": "Default date type for pages"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"type": "array",
|
||||
"description": "Array of plugin configurations",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["source", "enabled"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Plugin source path or identifier"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the plugin is enabled"
|
||||
},
|
||||
"order": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"description": "Plugin execution order"
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Plugin-specific options"
|
||||
},
|
||||
"layout": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "string",
|
||||
"enum": ["left", "right", "beforeBody", "afterBody"],
|
||||
"description": "Layout position"
|
||||
},
|
||||
"priority": {
|
||||
"type": "number",
|
||||
"description": "Layout priority"
|
||||
},
|
||||
"display": {
|
||||
"type": "string",
|
||||
"enum": ["all", "mobile-only", "desktop-only"],
|
||||
"description": "Display mode"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string",
|
||||
"description": "Conditional display logic"
|
||||
},
|
||||
"group": {
|
||||
"type": "string",
|
||||
"description": "Layout group name"
|
||||
},
|
||||
"groupOptions": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"grow": {
|
||||
"type": "boolean",
|
||||
"description": "Flex grow"
|
||||
},
|
||||
"shrink": {
|
||||
"type": "boolean",
|
||||
"description": "Flex shrink"
|
||||
},
|
||||
"basis": {
|
||||
"type": "string",
|
||||
"description": "Flex basis"
|
||||
},
|
||||
"order": {
|
||||
"type": "number",
|
||||
"description": "Flex order"
|
||||
},
|
||||
"align": {
|
||||
"type": "string",
|
||||
"description": "Alignment"
|
||||
},
|
||||
"justify": {
|
||||
"type": "string",
|
||||
"description": "Justification"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"description": "Flex direction"
|
||||
},
|
||||
"wrap": {
|
||||
"type": "boolean",
|
||||
"description": "Flex wrap"
|
||||
},
|
||||
"gap": {
|
||||
"type": "string",
|
||||
"description": "Gap between items"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Layout groups configuration"
|
||||
},
|
||||
"byPageType": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"exclude": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Excluded plugins for this page type"
|
||||
},
|
||||
"positions": {
|
||||
"type": "object",
|
||||
"description": "Position overrides for this page type"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Layout configuration by page type"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user