@rettangoli/sites 1.0.0-rc3 → 1.0.0-rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -1
- package/package.json +3 -2
- package/sites/README.md +107 -0
- package/sites/partials/docs/mobile-nav.yaml +7 -0
- package/sites/templates/default/base.yaml +20 -0
- package/sites/templates/docs/documentation.yaml +34 -0
- package/sites/templates/rettangoli-dev/base.yaml +24 -0
- package/sites/templates/rettangoli-dev/documentation.yaml +33 -0
- package/sites/templates/rettangoli-dev/fe-documentation.yaml +33 -0
- package/sites/templates/rettangoli-dev/sites-documentation.yaml +33 -0
- package/sites/templates/rettangoli-dev/vt-documentation.yaml +33 -0
- package/src/builtinTemplateFunctions.js +32 -2
- package/src/cli/build.js +1 -0
- package/src/createSiteBuilder.js +250 -11
- package/src/utils/loadSiteConfig.js +73 -2
- package/templates/default/README.md +1 -1
- package/templates/default/templates/base.yaml +1 -1
- package/templates/default/templates/post.yaml +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,11 @@ markdownit:
|
|
|
69
69
|
fallback: section
|
|
70
70
|
build:
|
|
71
71
|
keepMarkdownFiles: false
|
|
72
|
+
imports:
|
|
73
|
+
templates:
|
|
74
|
+
docs/documentation: https://example.com/templates/docs-documentation.yaml
|
|
75
|
+
partials:
|
|
76
|
+
docs/nav: https://example.com/partials/docs-nav.yaml
|
|
72
77
|
```
|
|
73
78
|
|
|
74
79
|
In the default starter template, CDN runtime scripts are controlled via `data/site.yaml`:
|
|
@@ -87,8 +92,46 @@ Example mappings:
|
|
|
87
92
|
- `pages/index.md` -> `_site/index.html` and `_site/index.md`
|
|
88
93
|
- `pages/docs/intro.md` -> `_site/docs/intro/index.html` and `_site/docs/intro.md`
|
|
89
94
|
|
|
95
|
+
`imports` lets you map aliases to remote YAML files (HTTP/HTTPS only). Use aliases in pages/templates:
|
|
96
|
+
- page frontmatter: `template: docs/documentation`
|
|
97
|
+
- template/page content: `$partial: docs/nav`
|
|
98
|
+
|
|
99
|
+
Imported files are cached on disk under `.rettangoli/sites/imports/{templates|partials}/` (hashed filenames).
|
|
100
|
+
Alias/url/hash mapping is tracked in `.rettangoli/sites/imports/index.yaml`.
|
|
101
|
+
Build is cache-first: if a cached file exists, it is used without a network request.
|
|
102
|
+
|
|
103
|
+
When an alias exists both remotely and locally, local files under `templates/` and `partials/` override the imported one.
|
|
104
|
+
|
|
90
105
|
If you want to publish a manual `llms.txt`, place it in `static/llms.txt`; it will be copied to `_site/llms.txt`.
|
|
91
106
|
|
|
107
|
+
## Pre-published Import Assets
|
|
108
|
+
|
|
109
|
+
`@rettangoli/sites` publishes reusable template/partial YAML assets under `sites/` for URL imports.
|
|
110
|
+
|
|
111
|
+
- Docs template: `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/docs/documentation.yaml`
|
|
112
|
+
- Docs partial: `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/partials/docs/mobile-nav.yaml`
|
|
113
|
+
- Rettangoli.dev shells: `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/*.yaml`
|
|
114
|
+
- Default scaffold base: `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/default/base.yaml`
|
|
115
|
+
|
|
116
|
+
See `sites/README.md` for full alias examples and required data contract.
|
|
117
|
+
|
|
118
|
+
## Template Authoring Pattern
|
|
119
|
+
|
|
120
|
+
Keep base templates as shells with minimal logic:
|
|
121
|
+
|
|
122
|
+
- document root (`html`, `head`, `body`)
|
|
123
|
+
- main content slot (`"${content}"`)
|
|
124
|
+
- stable layout containers
|
|
125
|
+
|
|
126
|
+
Put variant-specific behavior and data wiring in partials instead.
|
|
127
|
+
Partials accept explicit parameters via `$partial`, so they are the preferred place for:
|
|
128
|
+
|
|
129
|
+
- section-specific navigation data
|
|
130
|
+
- conditional UI branches
|
|
131
|
+
- reusable interactive blocks
|
|
132
|
+
|
|
133
|
+
This keeps one template reusable across many page variants and avoids duplicated template files.
|
|
134
|
+
|
|
92
135
|
## Commands
|
|
93
136
|
|
|
94
137
|
```bash
|
|
@@ -121,7 +164,7 @@ Available in YAML templates/pages without extra setup:
|
|
|
121
164
|
|
|
122
165
|
`formatDate` tokens: `YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`.
|
|
123
166
|
`decodeURI`/`decodeURIComponent` return the original input when decoding fails.
|
|
124
|
-
`sort` supports `order` as `asc` or `desc` and returns a new array.
|
|
167
|
+
`sort` supports `order` as `asc` or `desc` (default: `asc`), accepts dot-path keys (for example `data.date`), and returns a new array.
|
|
125
168
|
`md` returns raw rendered HTML from Markdown for template insertion.
|
|
126
169
|
|
|
127
170
|
## Screenshots
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/sites",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc5",
|
|
4
4
|
"description": "Generate static sites using Markdown and YAML for docs, blogs, and marketing sites.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Luciano Hanyon Wu",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
16
16
|
"components",
|
|
17
|
-
"templates"
|
|
17
|
+
"templates",
|
|
18
|
+
"sites"
|
|
18
19
|
],
|
|
19
20
|
"dependencies": {
|
|
20
21
|
"gray-matter": "^4.0.3",
|
package/sites/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Rettangoli Sites Import Assets
|
|
2
|
+
|
|
3
|
+
This folder contains publish-only YAML templates/partials for `@rettangoli/sites` URL imports.
|
|
4
|
+
These files are distribution assets, not `@rettangoli/sites` runtime source code.
|
|
5
|
+
|
|
6
|
+
## Docs Bundle
|
|
7
|
+
|
|
8
|
+
Template URL:
|
|
9
|
+
|
|
10
|
+
`https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/docs/documentation.yaml`
|
|
11
|
+
|
|
12
|
+
Partial URL:
|
|
13
|
+
|
|
14
|
+
`https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/partials/docs/mobile-nav.yaml`
|
|
15
|
+
|
|
16
|
+
`sites.config.yaml` example:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
imports:
|
|
20
|
+
templates:
|
|
21
|
+
docs/documentation: https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/docs/documentation.yaml
|
|
22
|
+
partials:
|
|
23
|
+
docs/mobile-nav: https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/partials/docs/mobile-nav.yaml
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Page frontmatter example:
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
---
|
|
30
|
+
template: docs/documentation
|
|
31
|
+
title: Getting Started
|
|
32
|
+
sidebarId: intro
|
|
33
|
+
---
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Required template data:
|
|
37
|
+
|
|
38
|
+
- `title`
|
|
39
|
+
- `docsLayout.sidebar.header` (object for sidebar header)
|
|
40
|
+
- `docsLayout.sidebar.items` (sidebar items)
|
|
41
|
+
- `docsLayout.assets.stylesheets` (array of stylesheet URLs)
|
|
42
|
+
- `docsLayout.assets.scripts` (array of script URLs)
|
|
43
|
+
|
|
44
|
+
Optional data:
|
|
45
|
+
|
|
46
|
+
- `docsLayout.metaDescription`
|
|
47
|
+
- `docsLayout.canonicalUrl`
|
|
48
|
+
|
|
49
|
+
Create `data/docsLayout.yaml`:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
assets:
|
|
53
|
+
stylesheets:
|
|
54
|
+
- https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc14/dist/themes/base.css
|
|
55
|
+
- https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc14/dist/themes/theme-rtgl-slate.css
|
|
56
|
+
scripts:
|
|
57
|
+
- https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js
|
|
58
|
+
- https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc14/dist/rettangoli-iife-ui.min.js
|
|
59
|
+
|
|
60
|
+
metaDescription: Documentation portal
|
|
61
|
+
canonicalUrl: https://example.com/docs/getting-started/
|
|
62
|
+
|
|
63
|
+
sidebar:
|
|
64
|
+
header:
|
|
65
|
+
label: Docs
|
|
66
|
+
href: /
|
|
67
|
+
items:
|
|
68
|
+
- title: Introduction
|
|
69
|
+
type: groupLabel
|
|
70
|
+
items:
|
|
71
|
+
- id: intro
|
|
72
|
+
title: Introduction
|
|
73
|
+
href: /docs/introduction/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Rettangoli.dev Shell Templates
|
|
77
|
+
|
|
78
|
+
These templates mirror `apps/rettangoli.dev/templates/*` so the app can consume
|
|
79
|
+
them via URL imports instead of local template files.
|
|
80
|
+
|
|
81
|
+
Template URLs:
|
|
82
|
+
|
|
83
|
+
- `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/base.yaml`
|
|
84
|
+
- `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/documentation.yaml`
|
|
85
|
+
- `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/fe-documentation.yaml`
|
|
86
|
+
- `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/sites-documentation.yaml`
|
|
87
|
+
- `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/vt-documentation.yaml`
|
|
88
|
+
|
|
89
|
+
`sites.config.yaml` alias example:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
imports:
|
|
93
|
+
templates:
|
|
94
|
+
base: https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/base.yaml
|
|
95
|
+
documentation: https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/documentation.yaml
|
|
96
|
+
fe-documentation: https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/fe-documentation.yaml
|
|
97
|
+
sites-documentation: https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/sites-documentation.yaml
|
|
98
|
+
vt-documentation: https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/rettangoli-dev/vt-documentation.yaml
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Default Scaffold Base Template
|
|
102
|
+
|
|
103
|
+
This template mirrors `packages/rettangoli-sites/templates/default/templates/base.yaml`.
|
|
104
|
+
|
|
105
|
+
Template URL:
|
|
106
|
+
|
|
107
|
+
- `https://cdn.jsdelivr.net/npm/@rettangoli/sites@<version>/sites/templates/default/base.yaml`
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
- $if docsLayout && docsLayout.sidebar && docsLayout.sidebar.header && docsLayout.sidebar.items:
|
|
2
|
+
- 'rtgl-view hide md-show w=f d=v bgc=bg':
|
|
3
|
+
- 'rtgl-view pos=fix w=f h=56 bgc=bg av=c ph=lg bwb=xs z=10 style="top:0;left:0"':
|
|
4
|
+
- 'a href="${docsLayout.sidebar.header.href}" style="text-decoration:none;display:contents;color:inherit;"':
|
|
5
|
+
- rtgl-text s=md: ${docsLayout.sidebar.header.label}
|
|
6
|
+
- rtgl-view h=56:
|
|
7
|
+
- rtgl-sidebar selected-item-id=${sidebarId} header="${encodeURIComponent(jsonStringify(docsLayout.sidebar.header))}" items="${encodeURIComponent(jsonStringify(docsLayout.sidebar.items))}" w=f:
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
- html lang="en":
|
|
2
|
+
- head:
|
|
3
|
+
- meta charset="utf-8":
|
|
4
|
+
- meta name="viewport" content="width=device-width,initial-scale=1":
|
|
5
|
+
- title: ${title} - ${site.name}
|
|
6
|
+
- link rel="stylesheet" href="/css/theme.css":
|
|
7
|
+
- $if site.assets.loadConstructStyleSheetsPolyfill:
|
|
8
|
+
- script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js":
|
|
9
|
+
- $if site.assets.loadUiFromCdn:
|
|
10
|
+
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/rettangoli-iife-ui.min.js":
|
|
11
|
+
- body.dark:
|
|
12
|
+
- rtgl-view w="f":
|
|
13
|
+
- rtgl-view h="64":
|
|
14
|
+
- rtgl-view w="f" ah="c":
|
|
15
|
+
- rtgl-view w="f" ph="lg" pb="lg" ah="c" md-w="100vw" lg-w="768" w="1024":
|
|
16
|
+
- "${content}"
|
|
17
|
+
- rtgl-view h="30vh":
|
|
18
|
+
- $partial: footer
|
|
19
|
+
- rtgl-view pos="abs" edge="t" ah="c" bgc="bg":
|
|
20
|
+
- $partial: header
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
- html lang="en":
|
|
2
|
+
- head:
|
|
3
|
+
- meta charset="utf-8":
|
|
4
|
+
- meta name="viewport" content="width=device-width,initial-scale=1":
|
|
5
|
+
- title: ${title}
|
|
6
|
+
- $if docsLayout && docsLayout.metaDescription:
|
|
7
|
+
- meta name="description" content="${docsLayout.metaDescription}":
|
|
8
|
+
- $if docsLayout && docsLayout.canonicalUrl:
|
|
9
|
+
- link rel="canonical" href="${docsLayout.canonicalUrl}":
|
|
10
|
+
- $if docsLayout && docsLayout.assets && docsLayout.assets.stylesheets:
|
|
11
|
+
- $for href in docsLayout.assets.stylesheets:
|
|
12
|
+
- link rel="stylesheet" href="${href}":
|
|
13
|
+
- $if docsLayout && docsLayout.assets && docsLayout.assets.scripts:
|
|
14
|
+
- $for src in docsLayout.assets.scripts:
|
|
15
|
+
- script src="${src}":
|
|
16
|
+
|
|
17
|
+
- body.dark:
|
|
18
|
+
- $partial: docs/mobile-nav
|
|
19
|
+
docsLayout: ${docsLayout}
|
|
20
|
+
sidebarId: ${sidebarId}
|
|
21
|
+
|
|
22
|
+
- rtgl-view bgc="bg" w="f" d=h:
|
|
23
|
+
- $if docsLayout && docsLayout.sidebar && docsLayout.sidebar.header && docsLayout.sidebar.items:
|
|
24
|
+
- 'rtgl-view md-hide pos=fix h=100vh bgc=bg style="left: 0"':
|
|
25
|
+
- rtgl-sidebar selected-item-id=${sidebarId} header="${encodeURIComponent(jsonStringify(docsLayout.sidebar.header))}" items="${encodeURIComponent(jsonStringify(docsLayout.sidebar.items))}":
|
|
26
|
+
- rtgl-view md-hide xl-show hide w=272:
|
|
27
|
+
- rtgl-view w="1fg" ah=c:
|
|
28
|
+
- rtgl-view pv="lg" lg-w="100vw" w="720" ph="lg" sv id="content-container":
|
|
29
|
+
- rtgl-view hide md-show h=56:
|
|
30
|
+
- rtgl-text s="h1" mb="lg": ${title}
|
|
31
|
+
- "${content}"
|
|
32
|
+
- rtgl-view h=200:
|
|
33
|
+
- 'rtgl-view xl-hide style="position: fixed; right: 0"':
|
|
34
|
+
- rtgl-page-outline id="page-outline" target-id="content-container" scroll-container-id="window" offset-top="80":
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
- html lang="en":
|
|
2
|
+
- head:
|
|
3
|
+
- $partial: seo1
|
|
4
|
+
page: ${page}
|
|
5
|
+
seo: ${seo}
|
|
6
|
+
title: ${title}
|
|
7
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/base.css":
|
|
8
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/theme-rtgl-slate.css":
|
|
9
|
+
- script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js":
|
|
10
|
+
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/rettangoli-iife-ui.min.js":
|
|
11
|
+
- script src="/public/rtgl-icons.js":
|
|
12
|
+
|
|
13
|
+
- body.dark:
|
|
14
|
+
- rtgl-view:
|
|
15
|
+
- rtgl-view pos="abs" edge="t" ah="c" bgc="bg":
|
|
16
|
+
- $partial: navbar1
|
|
17
|
+
title: ${site.navbar.title}
|
|
18
|
+
cta: ${site.navbar.cta}
|
|
19
|
+
- rtgl-view w=f:
|
|
20
|
+
- rtgl-view h="48":
|
|
21
|
+
- rtgl-view w="f" ah="c":
|
|
22
|
+
- rtgl-view w=f ph="lg" pb="lg" ah=c:
|
|
23
|
+
- "${content}"
|
|
24
|
+
- script src="/public/mobile-nav.js":
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
- html lang="en":
|
|
2
|
+
- head:
|
|
3
|
+
- $partial: seo1
|
|
4
|
+
page: ${page}
|
|
5
|
+
seo: ${seo}
|
|
6
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/base.css":
|
|
7
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/theme-rtgl-slate.css":
|
|
8
|
+
- script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js":
|
|
9
|
+
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/rettangoli-iife-ui.min.js":
|
|
10
|
+
- script src="/public/rtgl-icons.js":
|
|
11
|
+
|
|
12
|
+
- body.dark:
|
|
13
|
+
- $partial: mobile-nav1
|
|
14
|
+
headerLabel: ${docs.header.label}
|
|
15
|
+
headerHref: ${docs.header.href}
|
|
16
|
+
header: ${docs.header}
|
|
17
|
+
items: ${docs.items}
|
|
18
|
+
sidebarId: ${sidebarId}
|
|
19
|
+
|
|
20
|
+
- rtgl-view bgc="bg" w="f" d=h:
|
|
21
|
+
- 'rtgl-view md-hide pos=fix h=100vh bgc=bg style="left: 0"':
|
|
22
|
+
- rtgl-sidebar selected-item-id=${sidebarId} header="${encodeURIComponent(jsonStringify(docs.header))}" items="${encodeURIComponent(jsonStringify(docs.items))}":
|
|
23
|
+
- rtgl-view md-hide xl-show hide w=272:
|
|
24
|
+
- rtgl-view w="1fg" ah=c:
|
|
25
|
+
- rtgl-view pv="lg" lg-w="100vw" w="720" ph="lg" sv id="content-container":
|
|
26
|
+
- rtgl-view hide md-show h=56:
|
|
27
|
+
- rtgl-text s="h1" mb="lg": ${title}
|
|
28
|
+
- "${content}"
|
|
29
|
+
- rtgl-view h=200:
|
|
30
|
+
- 'rtgl-view xl-hide style="position: fixed; right: 0"':
|
|
31
|
+
- rtgl-page-outline id="page-outline" target-id="content-container" scroll-container-id="window" offset-top="80":
|
|
32
|
+
|
|
33
|
+
- script src="/public/mobile-nav.js":
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
- html lang="en":
|
|
2
|
+
- head:
|
|
3
|
+
- $partial: seo1
|
|
4
|
+
page: ${page}
|
|
5
|
+
seo: ${seo}
|
|
6
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/base.css":
|
|
7
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/theme-rtgl-slate.css":
|
|
8
|
+
- script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js":
|
|
9
|
+
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/rettangoli-iife-ui.min.js":
|
|
10
|
+
- script src="/public/rtgl-icons.js":
|
|
11
|
+
|
|
12
|
+
- body.dark:
|
|
13
|
+
- $partial: mobile-nav1
|
|
14
|
+
headerLabel: ${feDocs.header.label}
|
|
15
|
+
headerHref: ${feDocs.header.href}
|
|
16
|
+
header: ${feDocs.header}
|
|
17
|
+
items: ${feDocs.items}
|
|
18
|
+
sidebarId: ${sidebarId}
|
|
19
|
+
|
|
20
|
+
- rtgl-view bgc="bg" w="f" d=h:
|
|
21
|
+
- 'rtgl-view md-hide pos=fix h=100vh bgc=bg style="left: 0"':
|
|
22
|
+
- rtgl-sidebar selected-item-id=${sidebarId} header="${encodeURIComponent(jsonStringify(feDocs.header))}" items="${encodeURIComponent(jsonStringify(feDocs.items))}":
|
|
23
|
+
- rtgl-view md-hide xl-show hide w=272:
|
|
24
|
+
- rtgl-view w="1fg" ah=c:
|
|
25
|
+
- rtgl-view pv="lg" lg-w="100vw" w="720" ph="lg" sv id="content-container":
|
|
26
|
+
- rtgl-view hide md-show h=56:
|
|
27
|
+
- rtgl-text s="h1" mb="lg": ${title}
|
|
28
|
+
- "${content}"
|
|
29
|
+
- rtgl-view h=200:
|
|
30
|
+
- 'rtgl-view xl-hide style="position: fixed; right: 0"':
|
|
31
|
+
- rtgl-page-outline id="page-outline" target-id="content-container" scroll-container-id="window" offset-top="80":
|
|
32
|
+
|
|
33
|
+
- script src="/public/mobile-nav.js":
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
- html lang="en":
|
|
2
|
+
- head:
|
|
3
|
+
- $partial: seo1
|
|
4
|
+
page: ${page}
|
|
5
|
+
seo: ${seo}
|
|
6
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/base.css":
|
|
7
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/theme-rtgl-slate.css":
|
|
8
|
+
- script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js":
|
|
9
|
+
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/rettangoli-iife-ui.min.js":
|
|
10
|
+
- script src="/public/rtgl-icons.js":
|
|
11
|
+
|
|
12
|
+
- body.dark:
|
|
13
|
+
- $partial: mobile-nav1
|
|
14
|
+
headerLabel: ${sitesDocs.header.label}
|
|
15
|
+
headerHref: ${sitesDocs.header.href}
|
|
16
|
+
header: ${sitesDocs.header}
|
|
17
|
+
items: ${sitesDocs.items}
|
|
18
|
+
sidebarId: ${sidebarId}
|
|
19
|
+
|
|
20
|
+
- rtgl-view bgc="bg" w="f" d=h:
|
|
21
|
+
- 'rtgl-view md-hide pos=fix h=100vh bgc=bg style="left: 0"':
|
|
22
|
+
- rtgl-sidebar selected-item-id=${sidebarId} header="${encodeURIComponent(jsonStringify(sitesDocs.header))}" items="${encodeURIComponent(jsonStringify(sitesDocs.items))}":
|
|
23
|
+
- rtgl-view md-hide xl-show hide w=272:
|
|
24
|
+
- rtgl-view w="1fg" ah=c:
|
|
25
|
+
- rtgl-view pv="lg" lg-w="100vw" w="720" ph="lg" sv id="content-container":
|
|
26
|
+
- rtgl-view hide md-show h=56:
|
|
27
|
+
- rtgl-text s="h1" mb="lg": ${title}
|
|
28
|
+
- "${content}"
|
|
29
|
+
- rtgl-view h=200:
|
|
30
|
+
- 'rtgl-view xl-hide style="position: fixed; right: 0"':
|
|
31
|
+
- rtgl-page-outline id="page-outline" target-id="content-container" scroll-container-id="window" offset-top="80":
|
|
32
|
+
|
|
33
|
+
- script src="/public/mobile-nav.js":
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
- html lang="en":
|
|
2
|
+
- head:
|
|
3
|
+
- $partial: seo1
|
|
4
|
+
page: ${page}
|
|
5
|
+
seo: ${seo}
|
|
6
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/base.css":
|
|
7
|
+
- link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/themes/theme-rtgl-slate.css":
|
|
8
|
+
- script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js":
|
|
9
|
+
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/rettangoli-iife-ui.min.js":
|
|
10
|
+
- script src="/public/rtgl-icons.js":
|
|
11
|
+
|
|
12
|
+
- body.dark:
|
|
13
|
+
- $partial: mobile-nav1
|
|
14
|
+
headerLabel: ${vtDocs.header.label}
|
|
15
|
+
headerHref: ${vtDocs.header.href}
|
|
16
|
+
header: ${vtDocs.header}
|
|
17
|
+
items: ${vtDocs.items}
|
|
18
|
+
sidebarId: ${sidebarId}
|
|
19
|
+
|
|
20
|
+
- rtgl-view bgc="bg" w="f" d=h:
|
|
21
|
+
- 'rtgl-view md-hide pos=fix h=100vh bgc=bg style="left: 0"':
|
|
22
|
+
- rtgl-sidebar selected-item-id=${sidebarId} header="${encodeURIComponent(jsonStringify(vtDocs.header))}" items="${encodeURIComponent(jsonStringify(vtDocs.items))}":
|
|
23
|
+
- rtgl-view md-hide xl-show hide w=272:
|
|
24
|
+
- rtgl-view w="1fg" ah=c:
|
|
25
|
+
- rtgl-view pv="lg" lg-w="100vw" w="720" ph="lg" sv id="content-container":
|
|
26
|
+
- rtgl-view hide md-show h=56:
|
|
27
|
+
- rtgl-text s="h1" mb="lg": ${title}
|
|
28
|
+
- "${content}"
|
|
29
|
+
- rtgl-view h=200:
|
|
30
|
+
- 'rtgl-view xl-hide style="position: fixed; right: 0"':
|
|
31
|
+
- rtgl-page-outline id="page-outline" target-id="content-container" scroll-container-id="window" offset-top="80":
|
|
32
|
+
|
|
33
|
+
- script src="/public/mobile-nav.js":
|
|
@@ -75,15 +75,45 @@ function safeDecode(value, decoder) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function readSortValue(item, key, keyParts) {
|
|
79
|
+
if (key == null) {
|
|
80
|
+
return item;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (item == null) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!keyParts) {
|
|
88
|
+
return item?.[key];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (Object.prototype.hasOwnProperty.call(item, key)) {
|
|
92
|
+
return item[key];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let current = item;
|
|
96
|
+
for (const part of keyParts) {
|
|
97
|
+
if (!part) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
current = current?.[part];
|
|
101
|
+
}
|
|
102
|
+
return current;
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
function sortImpl(value, key, order = 'asc') {
|
|
79
106
|
if (!Array.isArray(value)) {
|
|
80
107
|
return [];
|
|
81
108
|
}
|
|
109
|
+
|
|
82
110
|
const normalizedOrder = String(order ?? 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
83
111
|
const factor = normalizedOrder === 'asc' ? 1 : -1;
|
|
112
|
+
const keyParts = typeof key === 'string' && key.includes('.') ? key.split('.') : null;
|
|
113
|
+
|
|
84
114
|
return [...value].sort((left, right) => {
|
|
85
|
-
const leftRaw =
|
|
86
|
-
const rightRaw =
|
|
115
|
+
const leftRaw = readSortValue(left, key, keyParts);
|
|
116
|
+
const rightRaw = readSortValue(right, key, keyParts);
|
|
87
117
|
|
|
88
118
|
if (leftRaw == null && rightRaw == null) return 0;
|
|
89
119
|
if (leftRaw == null) return 1;
|
package/src/cli/build.js
CHANGED
package/src/createSiteBuilder.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { convertToHtml } from 'yahtml';
|
|
2
2
|
import { parseAndRender } from 'jempl';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
4
5
|
import yaml from 'js-yaml';
|
|
5
6
|
import matter from 'gray-matter';
|
|
6
7
|
|
|
@@ -39,6 +40,204 @@ function isObject(item) {
|
|
|
39
40
|
return item && typeof item === 'object' && !Array.isArray(item);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
function parseYamlWithContext(content, contextLabel) {
|
|
44
|
+
try {
|
|
45
|
+
return yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`${contextLabel}: Invalid YAML: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildImportCacheHash(url) {
|
|
52
|
+
return createHash('sha256').update(url).digest('hex');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildImportCachePath(rootDir, importGroup, hash) {
|
|
56
|
+
return path.join(rootDir, '.rettangoli', 'sites', 'imports', importGroup, `${hash}.yaml`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildImportIndexPath(rootDir) {
|
|
60
|
+
return path.join(rootDir, '.rettangoli', 'sites', 'imports', 'index.yaml');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toRelativePath(rootDir, absolutePath) {
|
|
64
|
+
const relativePath = path.relative(rootDir, absolutePath);
|
|
65
|
+
return relativePath.replace(/\\/g, '/');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeImportIndex(rawIndex, indexPath) {
|
|
69
|
+
if (rawIndex == null) {
|
|
70
|
+
return { version: 1, entries: [] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!isObject(rawIndex)) {
|
|
74
|
+
throw new Error(`Invalid import index "${indexPath}": expected a YAML object.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (rawIndex.version !== undefined && rawIndex.version !== 1) {
|
|
78
|
+
throw new Error(`Unsupported import index version "${rawIndex.version}" in "${indexPath}".`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rawEntries = rawIndex.entries ?? [];
|
|
82
|
+
if (!Array.isArray(rawEntries)) {
|
|
83
|
+
throw new Error(`Invalid import index "${indexPath}": expected "entries" to be an array.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const entries = rawEntries.map((entry, index) => {
|
|
87
|
+
if (!isObject(entry)) {
|
|
88
|
+
throw new Error(`Invalid import index "${indexPath}" at entries[${index}]: expected an object.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const normalizedEntry = {
|
|
92
|
+
alias: entry.alias,
|
|
93
|
+
type: entry.type,
|
|
94
|
+
url: entry.url,
|
|
95
|
+
hash: entry.hash,
|
|
96
|
+
path: entry.path
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
for (const [key, value] of Object.entries(normalizedEntry)) {
|
|
100
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
101
|
+
throw new Error(`Invalid import index "${indexPath}" at entries[${index}].${key}: expected a non-empty string.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return normalizedEntry;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return { version: 1, entries };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readImportIndex(fs, rootDir) {
|
|
112
|
+
const indexPath = buildImportIndexPath(rootDir);
|
|
113
|
+
if (!fs.existsSync(indexPath)) {
|
|
114
|
+
return { version: 1, entries: [] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const indexContent = fs.readFileSync(indexPath, 'utf8');
|
|
118
|
+
const parsed = parseYamlWithContext(indexContent, `import index "${indexPath}"`);
|
|
119
|
+
return normalizeImportIndex(parsed, indexPath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function upsertImportIndexEntry(importIndex, entry) {
|
|
123
|
+
const existingIndex = importIndex.entries.findIndex((item) => item.type === entry.type && item.alias === entry.alias);
|
|
124
|
+
if (existingIndex === -1) {
|
|
125
|
+
importIndex.entries.push(entry);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
importIndex.entries[existingIndex] = entry;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeImportIndex(fs, rootDir, importIndex) {
|
|
132
|
+
const indexPath = buildImportIndexPath(rootDir);
|
|
133
|
+
const indexDir = path.dirname(indexPath);
|
|
134
|
+
if (!fs.existsSync(indexDir)) {
|
|
135
|
+
fs.mkdirSync(indexDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const sortedEntries = [...importIndex.entries].sort((a, b) => {
|
|
139
|
+
const left = `${a.type}:${a.alias}`;
|
|
140
|
+
const right = `${b.type}:${b.alias}`;
|
|
141
|
+
return left.localeCompare(right);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const output = {
|
|
145
|
+
version: 1,
|
|
146
|
+
entries: sortedEntries
|
|
147
|
+
};
|
|
148
|
+
fs.writeFileSync(indexPath, yaml.dump(output, { noRefs: true, lineWidth: -1 }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readImportedYamlFromCache(fs, cachePath, aliasLabel) {
|
|
152
|
+
if (!fs.existsSync(cachePath)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const content = fs.readFileSync(cachePath, 'utf8');
|
|
157
|
+
return parseYamlWithContext(content, `${aliasLabel} (cache: ${cachePath})`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function fetchRemoteYaml(url, fetchImpl, aliasLabel) {
|
|
161
|
+
const effectiveFetch = fetchImpl || globalThis.fetch;
|
|
162
|
+
if (typeof effectiveFetch !== 'function') {
|
|
163
|
+
throw new Error(`${aliasLabel}: Remote imports require global fetch support (Node.js 18+).`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const response = await effectiveFetch(url);
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`${aliasLabel}: HTTP ${response.status} ${response.statusText}`.trim());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const content = await response.text();
|
|
172
|
+
const parsed = parseYamlWithContext(content, aliasLabel);
|
|
173
|
+
return { parsed, content };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writeImportedYamlCache(fs, cachePath, content) {
|
|
177
|
+
const cacheDir = path.dirname(cachePath);
|
|
178
|
+
if (!fs.existsSync(cacheDir)) {
|
|
179
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
fs.writeFileSync(cachePath, content);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// Non-fatal: build can still proceed with fetched content.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function loadImportedAliases({
|
|
190
|
+
fs,
|
|
191
|
+
rootDir,
|
|
192
|
+
importMap,
|
|
193
|
+
importGroup,
|
|
194
|
+
typeLabel,
|
|
195
|
+
fetchImpl,
|
|
196
|
+
importIndex
|
|
197
|
+
}) {
|
|
198
|
+
const resolved = {};
|
|
199
|
+
if (!isObject(importMap)) {
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const [alias, url] of Object.entries(importMap)) {
|
|
204
|
+
const aliasLabel = `imported ${typeLabel} "${alias}" from "${url}"`;
|
|
205
|
+
const hash = buildImportCacheHash(url);
|
|
206
|
+
const cachePath = buildImportCachePath(rootDir, importGroup, hash);
|
|
207
|
+
const relativeCachePath = toRelativePath(rootDir, cachePath);
|
|
208
|
+
|
|
209
|
+
const cachedValue = readImportedYamlFromCache(fs, cachePath, aliasLabel);
|
|
210
|
+
if (cachedValue !== null) {
|
|
211
|
+
resolved[alias] = cachedValue;
|
|
212
|
+
upsertImportIndexEntry(importIndex, {
|
|
213
|
+
alias,
|
|
214
|
+
type: typeLabel,
|
|
215
|
+
url,
|
|
216
|
+
hash,
|
|
217
|
+
path: relativeCachePath
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const fetched = await fetchRemoteYaml(url, fetchImpl, aliasLabel);
|
|
224
|
+
writeImportedYamlCache(fs, cachePath, fetched.content);
|
|
225
|
+
resolved[alias] = fetched.parsed;
|
|
226
|
+
upsertImportIndexEntry(importIndex, {
|
|
227
|
+
alias,
|
|
228
|
+
type: typeLabel,
|
|
229
|
+
url,
|
|
230
|
+
hash,
|
|
231
|
+
path: relativeCachePath
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw new Error(`Failed to load ${aliasLabel}: ${error.message}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return resolved;
|
|
239
|
+
}
|
|
240
|
+
|
|
42
241
|
export function createSiteBuilder({
|
|
43
242
|
fs,
|
|
44
243
|
rootDir = '.',
|
|
@@ -46,6 +245,8 @@ export function createSiteBuilder({
|
|
|
46
245
|
md,
|
|
47
246
|
markdown = {},
|
|
48
247
|
keepMarkdownFiles = false,
|
|
248
|
+
imports = {},
|
|
249
|
+
fetchImpl,
|
|
49
250
|
functions = {},
|
|
50
251
|
quiet = false,
|
|
51
252
|
isScreenshotMode = false
|
|
@@ -79,24 +280,62 @@ export function createSiteBuilder({
|
|
|
79
280
|
fs.mkdirSync(outputRootDir, { recursive: true });
|
|
80
281
|
}
|
|
81
282
|
|
|
283
|
+
const importIndex = readImportIndex(fs, rootDir);
|
|
284
|
+
|
|
285
|
+
const importedTemplates = await loadImportedAliases({
|
|
286
|
+
fs,
|
|
287
|
+
rootDir,
|
|
288
|
+
importMap: imports.templates,
|
|
289
|
+
importGroup: 'templates',
|
|
290
|
+
typeLabel: 'template',
|
|
291
|
+
fetchImpl,
|
|
292
|
+
importIndex
|
|
293
|
+
});
|
|
294
|
+
const importedPartials = await loadImportedAliases({
|
|
295
|
+
fs,
|
|
296
|
+
rootDir,
|
|
297
|
+
importMap: imports.partials,
|
|
298
|
+
importGroup: 'partials',
|
|
299
|
+
typeLabel: 'partial',
|
|
300
|
+
fetchImpl,
|
|
301
|
+
importIndex
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (importIndex.entries.length > 0) {
|
|
305
|
+
writeImportIndex(fs, rootDir, importIndex);
|
|
306
|
+
}
|
|
307
|
+
|
|
82
308
|
// Read all partials and create a JSON object
|
|
83
309
|
const partialsDir = path.join(rootDir, 'partials');
|
|
84
|
-
const partials = {};
|
|
310
|
+
const partials = { ...importedPartials };
|
|
85
311
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
312
|
+
function readPartialsRecursively(dir, basePath = '') {
|
|
313
|
+
if (!fs.existsSync(dir)) return;
|
|
314
|
+
|
|
315
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
316
|
+
items.forEach(item => {
|
|
317
|
+
const itemPath = path.join(dir, item.name);
|
|
318
|
+
if (item.isDirectory()) {
|
|
319
|
+
const newBasePath = basePath ? `${basePath}/${item.name}` : item.name;
|
|
320
|
+
readPartialsRecursively(itemPath, newBasePath);
|
|
90
321
|
return;
|
|
91
322
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
323
|
+
|
|
324
|
+
if (!item.isFile() || (!item.name.endsWith('.yaml') && !item.name.endsWith('.yml'))) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const fileContent = fs.readFileSync(itemPath, 'utf8');
|
|
329
|
+
const nameWithoutExt = path.basename(item.name, path.extname(item.name));
|
|
330
|
+
const partialKey = basePath ? `${basePath}/${nameWithoutExt}` : nameWithoutExt;
|
|
331
|
+
partials[partialKey] = yaml.load(fileContent, { schema: yaml.JSON_SCHEMA });
|
|
97
332
|
});
|
|
98
333
|
}
|
|
99
334
|
|
|
335
|
+
if (fs.existsSync(partialsDir)) {
|
|
336
|
+
readPartialsRecursively(partialsDir);
|
|
337
|
+
}
|
|
338
|
+
|
|
100
339
|
// Read all data files and create a JSON object
|
|
101
340
|
const dataDir = path.join(rootDir, 'data');
|
|
102
341
|
const globalData = {};
|
|
@@ -116,7 +355,7 @@ export function createSiteBuilder({
|
|
|
116
355
|
|
|
117
356
|
// Read all templates and create a JSON object
|
|
118
357
|
const templatesDir = path.join(rootDir, 'templates');
|
|
119
|
-
const templates = {};
|
|
358
|
+
const templates = { ...importedTemplates };
|
|
120
359
|
|
|
121
360
|
function readTemplatesRecursively(dir, basePath = '') {
|
|
122
361
|
if (!fs.existsSync(dir)) return;
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
|
|
5
|
-
const ALLOWED_TOP_LEVEL_KEYS = new Set(['markdown', 'markdownit', 'build']);
|
|
5
|
+
const ALLOWED_TOP_LEVEL_KEYS = new Set(['markdown', 'markdownit', 'build', 'imports']);
|
|
6
6
|
const MARKDOWN_BOOLEAN_KEYS = new Set(['html', 'linkify', 'typographer', 'breaks', 'xhtmlOut']);
|
|
7
7
|
const MARKDOWN_STRING_KEYS = new Set(['langPrefix', 'quotes', 'preset']);
|
|
8
8
|
const MARKDOWN_NUMBER_KEYS = new Set(['maxNesting']);
|
|
@@ -27,6 +27,8 @@ const ALLOWED_HEADING_ANCHORS_KEYS = new Set([...HEADING_ANCHORS_BOOLEAN_KEYS, .
|
|
|
27
27
|
const ALLOWED_HEADING_ANCHOR_SLUG_MODES = new Set(['ascii', 'unicode']);
|
|
28
28
|
const BUILD_BOOLEAN_KEYS = new Set(['keepMarkdownFiles']);
|
|
29
29
|
const ALLOWED_BUILD_KEYS = new Set([...BUILD_BOOLEAN_KEYS]);
|
|
30
|
+
const ALLOWED_IMPORT_GROUP_KEYS = new Set(['templates', 'partials']);
|
|
31
|
+
const ALLOWED_IMPORT_PROTOCOLS = new Set(['http:', 'https:']);
|
|
30
32
|
let didWarnLegacyMarkdownKey = false;
|
|
31
33
|
|
|
32
34
|
function isPlainObject(value) {
|
|
@@ -119,6 +121,71 @@ function validateBuildConfig(value, configPath) {
|
|
|
119
121
|
return { ...value };
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
function validateImportUrl(url, configPath, groupKey, alias) {
|
|
125
|
+
if (typeof url !== 'string' || url.trim() === '') {
|
|
126
|
+
throw new Error(`Invalid imports.${groupKey} value for alias "${alias}" in "${configPath}": expected a non-empty URL string.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = new URL(url);
|
|
132
|
+
} catch {
|
|
133
|
+
throw new Error(`Invalid imports.${groupKey} URL for alias "${alias}" in "${configPath}": "${url}" is not a valid URL.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!ALLOWED_IMPORT_PROTOCOLS.has(parsed.protocol)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Invalid imports.${groupKey} URL for alias "${alias}" in "${configPath}": protocol "${parsed.protocol}" is not supported. Allowed protocols: http:, https:.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return parsed.toString();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateImportGroup(value, configPath, groupKey) {
|
|
146
|
+
if (!isPlainObject(value)) {
|
|
147
|
+
throw new Error(`Invalid imports.${groupKey} in "${configPath}": expected an object of alias-to-URL mappings.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const normalized = {};
|
|
151
|
+
|
|
152
|
+
for (const [rawAlias, rawUrl] of Object.entries(value)) {
|
|
153
|
+
const alias = String(rawAlias).trim();
|
|
154
|
+
if (alias === '') {
|
|
155
|
+
throw new Error(`Invalid imports.${groupKey} in "${configPath}": alias keys must be non-empty.`);
|
|
156
|
+
}
|
|
157
|
+
normalized[alias] = validateImportUrl(rawUrl, configPath, groupKey, alias);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return normalized;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateImportsConfig(value, configPath) {
|
|
164
|
+
if (!isPlainObject(value)) {
|
|
165
|
+
throw new Error(`Invalid imports config in "${configPath}": expected an object.`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const normalized = {};
|
|
169
|
+
|
|
170
|
+
for (const key of Object.keys(value)) {
|
|
171
|
+
if (!ALLOWED_IMPORT_GROUP_KEYS.has(key)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Unsupported imports group "${key}" in "${configPath}". Supported groups: ${Array.from(ALLOWED_IMPORT_GROUP_KEYS).join(', ')}.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (value.templates !== undefined) {
|
|
179
|
+
normalized.templates = validateImportGroup(value.templates, configPath, 'templates');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (value.partials !== undefined) {
|
|
183
|
+
normalized.partials = validateImportGroup(value.partials, configPath, 'partials');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return normalized;
|
|
187
|
+
}
|
|
188
|
+
|
|
122
189
|
function validateConfig(rawConfig, configPath) {
|
|
123
190
|
if (rawConfig == null) {
|
|
124
191
|
return {};
|
|
@@ -133,7 +200,7 @@ function validateConfig(rawConfig, configPath) {
|
|
|
133
200
|
for (const key of Object.keys(config)) {
|
|
134
201
|
if (!ALLOWED_TOP_LEVEL_KEYS.has(key)) {
|
|
135
202
|
throw new Error(
|
|
136
|
-
`Unsupported key "${key}" in "${configPath}". Supported keys: markdownit (recommended), markdown (legacy alias), build.`
|
|
203
|
+
`Unsupported key "${key}" in "${configPath}". Supported keys: markdownit (recommended), markdown (legacy alias), build, imports.`
|
|
137
204
|
);
|
|
138
205
|
}
|
|
139
206
|
}
|
|
@@ -221,6 +288,10 @@ function validateConfig(rawConfig, configPath) {
|
|
|
221
288
|
normalizedConfig.build = validateBuildConfig(config.build, configPath);
|
|
222
289
|
}
|
|
223
290
|
|
|
291
|
+
if (config.imports !== undefined) {
|
|
292
|
+
normalizedConfig.imports = validateImportsConfig(config.imports, configPath);
|
|
293
|
+
}
|
|
294
|
+
|
|
224
295
|
return normalizedConfig;
|
|
225
296
|
}
|
|
226
297
|
|
|
@@ -233,7 +233,7 @@ Use these directly in `${...}` expressions:
|
|
|
233
233
|
|
|
234
234
|
Date format tokens: `YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`.
|
|
235
235
|
`decodeURI`/`decodeURIComponent` return the original input when decoding fails.
|
|
236
|
-
`sort` supports `order` as `asc` or `desc` and returns a new array.
|
|
236
|
+
`sort` supports `order` as `asc` or `desc` (default: `asc`), accepts dot-path keys (for example `data.date`), and returns a new array.
|
|
237
237
|
`md` returns raw rendered HTML from Markdown for template insertion.
|
|
238
238
|
|
|
239
239
|
## Static Files
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- $if site.assets.loadConstructStyleSheetsPolyfill:
|
|
8
8
|
- script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js":
|
|
9
9
|
- $if site.assets.loadUiFromCdn:
|
|
10
|
-
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@0.
|
|
10
|
+
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/rettangoli-iife-ui.min.js":
|
|
11
11
|
- body.dark:
|
|
12
12
|
- rtgl-view w="f":
|
|
13
13
|
- rtgl-view h="64":
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- $if site.assets.loadConstructStyleSheetsPolyfill:
|
|
8
8
|
- script src="https://cdn.jsdelivr.net/npm/construct-style-sheets-polyfill@3.1.0/dist/adoptedStyleSheets.min.js":
|
|
9
9
|
- $if site.assets.loadUiFromCdn:
|
|
10
|
-
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@0.
|
|
10
|
+
- script src="https://cdn.jsdelivr.net/npm/@rettangoli/ui@1.0.0-rc13/dist/rettangoli-iife-ui.min.js":
|
|
11
11
|
- body.dark:
|
|
12
12
|
- rtgl-view h="64":
|
|
13
13
|
- rtgl-view w="f" ah="c":
|