domma-cms 0.30.0 → 0.32.1
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/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +4 -4
- package/admin/js/lib/themes.js +1 -1
- package/admin/js/templates/effects.html +752 -752
- package/admin/js/templates/forms.html +17 -17
- package/admin/js/templates/my-profile.html +17 -17
- package/admin/js/templates/role-editor.html +70 -70
- package/admin/js/templates/roles.html +10 -10
- package/admin/js/views/settings.js +1 -1
- package/bin/lib/config-merge.js +44 -44
- package/config/site.json +1 -0
- package/package.json +2 -2
- package/plugins/surveys/admin/templates/audience.html +47 -0
- package/plugins/surveys/admin/templates/results.html +58 -0
- package/plugins/surveys/admin/templates/survey-editor.html +141 -0
- package/plugins/surveys/admin/templates/surveys.html +25 -0
- package/plugins/surveys/admin/views/audience.js +301 -0
- package/plugins/surveys/admin/views/results.js +172 -0
- package/plugins/surveys/admin/views/survey-editor.js +211 -0
- package/plugins/surveys/admin/views/surveys.js +161 -0
- package/plugins/surveys/collections/survey-contacts/schema.json +13 -0
- package/plugins/surveys/collections/survey-groups/schema.json +11 -0
- package/plugins/surveys/collections/survey-invites/schema.json +16 -0
- package/plugins/surveys/collections/surveys/schema.json +23 -0
- package/plugins/surveys/config.js +8 -0
- package/plugins/surveys/plugin.js +169 -0
- package/plugins/surveys/plugin.json +65 -0
- package/plugins/surveys/public/dist-shared.mjs +1 -0
- package/plugins/surveys/public/survey.css +1 -0
- package/plugins/surveys/public/survey.mjs +1 -0
- package/plugins/surveys/templates/survey-page.html +63 -0
- package/server/middleware/auth.js +253 -253
- package/server/routes/api/auth.js +309 -309
- package/server/routes/api/navigation.js +42 -42
- package/server/routes/api/settings.js +141 -141
- package/server/routes/docs-public.js +43 -0
- package/server/routes/public.js +2 -2
- package/server/server.js +13 -1
- package/{scripts/migrate-docs.js → server/services/docs.js} +204 -126
- package/server/services/email.js +167 -167
- package/server/services/userProfiles.js +199 -199
- package/server/services/users.js +302 -302
- package/config/connections.json.bak +0 -9
|
@@ -1,26 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* the Domma Docs project. Reads each admin HTML template, strips the admin-only
|
|
4
|
-
* view header, wraps the body in page frontmatter tagged `project: domma-docs`,
|
|
5
|
-
* and writes content/pages/domma-docs/<path>.md. Re-runnable (overwrites).
|
|
2
|
+
* Domma Docs — request-time renderer
|
|
6
3
|
*
|
|
7
|
-
*
|
|
4
|
+
* Serves the Domma CMS handbook at /domma-docs/* directly from the admin doc
|
|
5
|
+
* HTML templates (admin/js/templates/docs/*.html) at request time. There is no
|
|
6
|
+
* baked content: a domma-cms version bump ships new templates and the docs
|
|
7
|
+
* update everywhere automatically — no porter, no per-site provisioning, no
|
|
8
|
+
* staleness. (Replaces the one-shot scripts/migrate-docs.js bake.)
|
|
9
|
+
*
|
|
10
|
+
* The publish switch is the `domma-docs` project's enabled flag, enforced by
|
|
11
|
+
* the route (server/routes/docs-public.js); this service only renders.
|
|
12
|
+
*
|
|
13
|
+
* Pipeline per leaf page: read template → strip admin chrome → rewrite links →
|
|
14
|
+
* extract inline <style> → dedent → escape code brackets → structure body into
|
|
15
|
+
* native Domma shortcodes ([card]/[tabs]/[accordion]) → wrap with hero, nav,
|
|
16
|
+
* breadcrumb and prev/next → parseMarkdown → renderPage. Section landing pages
|
|
17
|
+
* (/domma-docs and the Usage/Tutorials/API indexes) are synthesized from the
|
|
18
|
+
* MAP so the nav never links to a 404.
|
|
8
19
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
20
|
+
import {readFile} from 'node:fs/promises';
|
|
21
|
+
import {fileURLToPath} from 'node:url';
|
|
22
|
+
import {dirname, join, resolve} from 'node:path';
|
|
11
23
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
import {parseMarkdown} from './markdown.js';
|
|
25
|
+
import {renderPage} from './renderer.js';
|
|
26
|
+
import * as cache from './cache/index.js';
|
|
15
27
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
28
|
+
// import.meta.url-relative (NOT cwd-relative): the templates live beside the
|
|
29
|
+
// engine's admin dir in both the npm-package layout and each child site's
|
|
30
|
+
// physical copy. server/services/docs.js → ../../admin/js/templates/docs.
|
|
31
|
+
const TPL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../..', 'admin/js/templates/docs');
|
|
32
|
+
|
|
33
|
+
// Functional nav chrome only (breadcrumb + prev/next + section nav). Everything
|
|
34
|
+
// else (hero, cards, tabs, accordion, buttons) is a native Domma component, so
|
|
35
|
+
// it follows the theme. Injected as an inline <style> per docs page at render
|
|
36
|
+
// time — no dependency on content/custom.css.
|
|
24
37
|
const DOCS_BODY_CSS = `.doc-breadcrumb { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; max-width: 80ch; margin: 0 auto 1.25rem; font-size: .85rem; color: var(--dm-text-muted); }
|
|
25
38
|
.doc-breadcrumb a { color: var(--dm-primary); text-decoration: none; }
|
|
26
39
|
.doc-breadcrumb a:hover { text-decoration: underline; }
|
|
@@ -29,11 +42,7 @@ const DOCS_BODY_CSS = `.doc-breadcrumb { display: flex; flex-wrap: wrap; gap: .5
|
|
|
29
42
|
.doc-pagenav .btn-secondary { margin-inline: auto; }
|
|
30
43
|
.doc-nav { display: flex; flex-wrap: wrap; gap: .4rem; justify-content: center; max-width: 80ch; margin: 0 auto 1.75rem; padding: .5rem; border: 1px solid var(--dm-card-border, var(--dm-border)); border-radius: var(--dm-radius-lg, 14px); background: var(--dm-card-bg, var(--dm-surface)); }`;
|
|
31
44
|
|
|
32
|
-
//
|
|
33
|
-
// (deduped) and written to custom.css with the base styles.
|
|
34
|
-
const collectedStyles = new Set();
|
|
35
|
-
|
|
36
|
-
// template basename → {path (URL tail = file path), title}
|
|
45
|
+
// template basename → [url tail (= file path), title]
|
|
37
46
|
const MAP = [
|
|
38
47
|
['usage-pages', 'usage/pages', 'Pages'],
|
|
39
48
|
['usage-media', 'usage/media', 'Media'],
|
|
@@ -66,18 +75,9 @@ const MAP = [
|
|
|
66
75
|
['api-actions', 'api/actions', 'Actions API']
|
|
67
76
|
];
|
|
68
77
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
function stripHeader(html) {
|
|
73
|
-
return html.replace(/<div class="view-header">[\s\S]*?<\/div>\s*/, '');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// The admin templates link with admin-SPA hash routes (#/…) and stale
|
|
77
|
-
// /resources/… paths, none of which resolve on the public site. Rewrite them:
|
|
78
|
-
// doc-to-doc links → public /domma-docs/… paths; admin-feature links → the
|
|
79
|
-
// admin SPA (/admin#/…); resources links with a doc equivalent → that doc; the
|
|
80
|
-
// rest of /resources/… (no equivalent) are unwrapped to plain text.
|
|
78
|
+
// Admin-SPA hash routes and stale /resources/… paths don't resolve on the
|
|
79
|
+
// public site — rewrite doc-to-doc links to /domma-docs/…, admin-feature links
|
|
80
|
+
// to the admin SPA, and unwrap the rest.
|
|
81
81
|
const LINK_MAP = {
|
|
82
82
|
'#/docs/components-walkthrough': '/domma-docs/components/walkthrough',
|
|
83
83
|
'#/docs/components-rules': '/domma-docs/components/rules',
|
|
@@ -96,6 +96,39 @@ const LINK_MAP = {
|
|
|
96
96
|
'/resources/shortcodes': '/domma-docs/usage/shortcodes',
|
|
97
97
|
'/resources/components': '/domma-docs/components'
|
|
98
98
|
};
|
|
99
|
+
|
|
100
|
+
// The four top-level doc sections — rendered as a persistent nav on every page.
|
|
101
|
+
const SECTIONS = [
|
|
102
|
+
{label: 'Usage', href: '/domma-docs/usage'},
|
|
103
|
+
{label: 'Tutorials', href: '/domma-docs/tutorials'},
|
|
104
|
+
{label: 'Components', href: '/domma-docs/components'},
|
|
105
|
+
{label: 'API Reference', href: '/domma-docs/api'}
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// Section landing pages keyed by url tail → section label. The Components
|
|
109
|
+
// landing is the Components Reference leaf (MAP tail 'components'), so it is
|
|
110
|
+
// NOT synthesized here.
|
|
111
|
+
const LANDINGS = {
|
|
112
|
+
'': 'Home',
|
|
113
|
+
'usage': 'Usage',
|
|
114
|
+
'tutorials': 'Tutorials',
|
|
115
|
+
'api': 'API Reference'
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ── pure transform helpers (relocated from scripts/migrate-docs.js) ──────────
|
|
119
|
+
|
|
120
|
+
function escText(s) {
|
|
121
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
122
|
+
}
|
|
123
|
+
function heroAttr(s) {
|
|
124
|
+
return String(s).replace(/"/g, '"');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Drop the admin-only header block (<div class="view-header"> … </div>).
|
|
128
|
+
function stripHeader(html) {
|
|
129
|
+
return html.replace(/<div class="view-header">[\s\S]*?<\/div>\s*/, '');
|
|
130
|
+
}
|
|
131
|
+
|
|
99
132
|
function rewriteLinks(html) {
|
|
100
133
|
let out = html;
|
|
101
134
|
for (const [from, to] of Object.entries(LINK_MAP)) {
|
|
@@ -106,22 +139,22 @@ function rewriteLinks(html) {
|
|
|
106
139
|
return out;
|
|
107
140
|
}
|
|
108
141
|
|
|
109
|
-
// Pull every inline <style> block out of the body
|
|
110
|
-
//
|
|
142
|
+
// Pull every inline <style> block out of the body, returning the cleaned HTML
|
|
143
|
+
// plus the extracted (deduped) CSS — collected PER CALL (no shared module state,
|
|
144
|
+
// which would leak across requests in a long-lived server).
|
|
111
145
|
function stripStyleBlocks(html) {
|
|
112
|
-
|
|
146
|
+
const styles = new Set();
|
|
147
|
+
const out = html.replace(/<style\b[^>]*>([\s\S]*?)<\/style>\s*/gi, (_m, css) => {
|
|
113
148
|
const trimmed = css.trim();
|
|
114
|
-
if (trimmed)
|
|
149
|
+
if (trimmed) styles.add(trimmed);
|
|
115
150
|
return '';
|
|
116
151
|
});
|
|
152
|
+
return {html: out, styles: [...styles]};
|
|
117
153
|
}
|
|
118
154
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
// line OUTSIDE <pre> blocks so each structural tag starts at column 0 and marked
|
|
123
|
-
// passes it through as raw HTML. Inside <pre>, whitespace is significant (code
|
|
124
|
-
// samples) and is preserved verbatim.
|
|
155
|
+
// Markdown treats any line indented 4+ spaces as a code block. Strip leading
|
|
156
|
+
// indentation OUTSIDE <pre> so structural HTML passes through as raw HTML;
|
|
157
|
+
// inside <pre> whitespace is significant and preserved.
|
|
125
158
|
function dedentOutsidePre(html) {
|
|
126
159
|
const parts = html.split(/(<pre\b[^>]*>[\s\S]*?<\/pre>)/gi);
|
|
127
160
|
return parts.map((part, i) =>
|
|
@@ -129,59 +162,16 @@ function dedentOutsidePre(html) {
|
|
|
129
162
|
).join('');
|
|
130
163
|
}
|
|
131
164
|
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// so examples like [grid cols="N"]...[/grid] in table cells would be expanded
|
|
135
|
-
// as real shortcodes. Escape square brackets inside every <pre> and <code>
|
|
136
|
-
// region so all examples render literally on the public page.
|
|
165
|
+
// Escape square brackets inside <pre> and <code> so shortcode examples render
|
|
166
|
+
// literally instead of being expanded by the render pipeline.
|
|
137
167
|
function escapeCodeBrackets(html) {
|
|
138
168
|
const esc = (region) => region.replace(/\[/g, '[').replace(/\]/g, ']');
|
|
139
|
-
html = html.replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/gi, esc);
|
|
140
|
-
html = html.replace(/<code\b[^>]*>[\s\S]*?<\/code>/gi, esc);
|
|
169
|
+
html = html.replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/gi, esc);
|
|
170
|
+
html = html.replace(/<code\b[^>]*>[\s\S]*?<\/code>/gi, esc);
|
|
141
171
|
return html;
|
|
142
172
|
}
|
|
143
173
|
|
|
144
|
-
|
|
145
|
-
return `---\ntitle: ${title}\nlayout: default\nstatus: published\nvisibility: public\nproject: domma-docs\n---\n\n`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// The four top-level doc sections — rendered as a persistent nav on every page.
|
|
149
|
-
const SECTIONS = [
|
|
150
|
-
{label: 'Usage', href: '/domma-docs/usage'},
|
|
151
|
-
{label: 'Tutorials', href: '/domma-docs/tutorials'},
|
|
152
|
-
{label: 'Components', href: '/domma-docs/components'},
|
|
153
|
-
{label: 'API Reference', href: '/domma-docs/api'}
|
|
154
|
-
];
|
|
155
|
-
|
|
156
|
-
// Persistent section nav (Home + the four sections, current one highlighted).
|
|
157
|
-
// Rendered just under the hero on every doc page so navigation is always present.
|
|
158
|
-
function docNav(activeLabel) {
|
|
159
|
-
const links = [{label: 'Home', href: '/domma-docs'}, ...SECTIONS].map(s => {
|
|
160
|
-
const active = s.label === activeLabel;
|
|
161
|
-
return `<a class="btn btn-sm ${active ? 'btn-primary' : 'btn-ghost'}" href="${s.href}">${escText(s.label)}</a>`;
|
|
162
|
-
}).join('');
|
|
163
|
-
return `<nav class="doc-nav">${links}</nav>`;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Section label + landing URL for the hero eyebrow, derived from the page path.
|
|
167
|
-
function sectionFor(path) {
|
|
168
|
-
if (path.startsWith('usage/')) return {label: 'Usage', href: '/domma-docs/usage'};
|
|
169
|
-
if (path.startsWith('tutorials/')) return {label: 'Tutorials', href: '/domma-docs/tutorials'};
|
|
170
|
-
if (path.startsWith('api/')) return {label: 'API Reference', href: '/domma-docs/api'};
|
|
171
|
-
if (path === 'components' || path.startsWith('components/')) return {label: 'Components', href: '/domma-docs/components'};
|
|
172
|
-
return {label: 'Documentation', href: '/domma-docs'};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function heroAttr(s) {
|
|
176
|
-
return String(s).replace(/"/g, '"');
|
|
177
|
-
}
|
|
178
|
-
function escText(s) {
|
|
179
|
-
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Every template wraps its content in a consistent .row > .col-12 > .docs-body
|
|
183
|
-
// shell (opening at the top, three closing </div> at the very end). Strip it to
|
|
184
|
-
// get the bare inner content so we can split it into sections.
|
|
174
|
+
// Strip the consistent .row > .col-12 > .docs-body shell to get the bare inner.
|
|
185
175
|
function unwrapDocsBody(html) {
|
|
186
176
|
return html.trim()
|
|
187
177
|
.replace(/^<div class="row">\s*<div class="col-12">\s*<div class="docs-body">/, '')
|
|
@@ -189,7 +179,7 @@ function unwrapDocsBody(html) {
|
|
|
189
179
|
.trim();
|
|
190
180
|
}
|
|
191
181
|
|
|
192
|
-
// Plain-text heading title, safe
|
|
182
|
+
// Plain-text heading title, safe as a shortcode attribute value.
|
|
193
183
|
function headingText(inner) {
|
|
194
184
|
return inner.replace(/<[^>]+>/g, '')
|
|
195
185
|
.replace(/&/g, '&').replace(/—/g, '—').replace(/–/g, '–')
|
|
@@ -210,9 +200,7 @@ function splitSections(inner, level) {
|
|
|
210
200
|
return {preamble, sections};
|
|
211
201
|
}
|
|
212
202
|
|
|
213
|
-
//
|
|
214
|
-
// scannable reference); 2–6 → tabs (first pane shown, rest a click away); <2 →
|
|
215
|
-
// a single card. Any preamble before the first heading leads in its own card.
|
|
203
|
+
// ≥7 sections → accordion; 2–6 → tabs; <2 → a single card.
|
|
216
204
|
function structureBody(body) {
|
|
217
205
|
const inner = unwrapDocsBody(body);
|
|
218
206
|
const level = /<h2\b/i.test(inner) ? 2 : 3;
|
|
@@ -227,8 +215,26 @@ function structureBody(body) {
|
|
|
227
215
|
sections.map(s => `[item title="${s.title}"]\n${s.body}\n[/item]`).join('\n') + '\n[/accordion]';
|
|
228
216
|
}
|
|
229
217
|
|
|
230
|
-
//
|
|
231
|
-
|
|
218
|
+
// Persistent section nav (Home + the four sections, current one highlighted).
|
|
219
|
+
function docNav(activeLabel) {
|
|
220
|
+
const links = [{label: 'Home', href: '/domma-docs'}, ...SECTIONS].map(s => {
|
|
221
|
+
const active = s.label === activeLabel;
|
|
222
|
+
return `<a class="btn btn-sm ${active ? 'btn-primary' : 'btn-ghost'}" href="${s.href}">${escText(s.label)}</a>`;
|
|
223
|
+
}).join('');
|
|
224
|
+
return `<nav class="doc-nav">${links}</nav>`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Section label + landing URL for the hero eyebrow, derived from the page path.
|
|
228
|
+
function sectionFor(path) {
|
|
229
|
+
if (path.startsWith('usage/')) return {label: 'Usage', href: '/domma-docs/usage'};
|
|
230
|
+
if (path.startsWith('tutorials/')) return {label: 'Tutorials', href: '/domma-docs/tutorials'};
|
|
231
|
+
if (path.startsWith('api/')) return {label: 'API Reference', href: '/domma-docs/api'};
|
|
232
|
+
if (path === 'components' || path.startsWith('components/')) return {label: 'Components', href: '/domma-docs/components'};
|
|
233
|
+
return {label: 'Documentation', href: '/domma-docs'};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Assemble a leaf page: hero, persistent nav, breadcrumb, structured body, and
|
|
237
|
+
// a prev / back-to-section / next nav bar.
|
|
232
238
|
function buildPage(section, title, body, prev, next) {
|
|
233
239
|
const hero = `[hero title="${heroAttr(title)}" tagline="${heroAttr(section.label)}" variant="gradient" align="center" fullwidth="true"][/hero]`;
|
|
234
240
|
const nav = docNav(section.label);
|
|
@@ -242,39 +248,111 @@ function buildPage(section, title, body, prev, next) {
|
|
|
242
248
|
return `${hero}\n\n${nav}\n\n${crumb}\n\n${structureBody(body)}\n\n${pagenav}`;
|
|
243
249
|
}
|
|
244
250
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
// ── landing pages ────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
// Build a section landing body: hero + nav + a grid of cards linking each leaf.
|
|
254
|
+
function buildSectionLanding(label) {
|
|
255
|
+
const section = SECTIONS.find(s => s.label === label);
|
|
256
|
+
const pages = MAP.filter(m => sectionFor(m[1]).label === label);
|
|
257
|
+
const hero = `[hero title="${heroAttr(label)}" tagline="Domma Docs" variant="gradient" align="center" fullwidth="true"][/hero]`;
|
|
258
|
+
const cards = pages.map(([, tail, title]) =>
|
|
259
|
+
`[card]\n### ${escText(title)}\n\n[button label="Read ${heroAttr(title)} →" href="/domma-docs/${tail}" variant="ghost" /]\n[/card]`
|
|
260
|
+
).join('\n');
|
|
261
|
+
return {title: label, body: `${hero}\n\n${docNav(label)}\n\n[grid cols="3"]\n${cards}\n[/grid]`};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Build the home landing: hero + nav + a card per top-level section.
|
|
265
|
+
function buildHomeLanding() {
|
|
266
|
+
const hero = `[hero title="Domma Docs" tagline="The Domma CMS handbook" variant="gradient" align="center" fullwidth="true"][/hero]`;
|
|
267
|
+
const cards = [
|
|
268
|
+
{label: 'Usage', href: '/domma-docs/usage', icon: 'book-open', blurb: 'Day-to-day guides for running the CMS — pages, media, navigation, shortcodes and more.'},
|
|
269
|
+
{label: 'Tutorials', href: '/domma-docs/tutorials', icon: 'academic-cap', blurb: 'Step-by-step walkthroughs that build something real from start to finish.'},
|
|
270
|
+
{label: 'Components', href: '/domma-docs/components', icon: 'puzzle', blurb: 'The full component reference — shortcodes, how-tos, walkthroughs and rules.'},
|
|
271
|
+
{label: 'API Reference', href: '/domma-docs/api', icon: 'code', blurb: 'Every REST endpoint for headless and external integrations.'}
|
|
272
|
+
].map(s =>
|
|
273
|
+
`[card icon="${s.icon}" title="${heroAttr(s.label)}"]\n${escText(s.blurb)}\n\n[button label="Browse ${heroAttr(s.label)} →" href="${s.href}" variant="primary" /]\n[/card]`
|
|
274
|
+
).join('\n');
|
|
275
|
+
return {title: 'Documentation', body: `${hero}\n\n${docNav('Home')}\n\n[grid cols="2"]\n${cards}\n[/grid]`};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── render ───────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
// Build the shortcode body for a doc path, or null if the path is unknown.
|
|
281
|
+
// Returns {title, body, styles[]} — `styles` is the inline CSS extracted from a
|
|
282
|
+
// leaf template (empty for synthesized landings).
|
|
283
|
+
async function buildDocBody(docPath) {
|
|
284
|
+
if (docPath in LANDINGS) {
|
|
285
|
+
const built = docPath === '' ? buildHomeLanding() : buildSectionLanding(LANDINGS[docPath]);
|
|
286
|
+
return {...built, styles: []};
|
|
287
|
+
}
|
|
288
|
+
const entry = MAP.find(m => m[1] === docPath);
|
|
289
|
+
if (!entry) return null;
|
|
290
|
+
const [tpl, path, title] = entry;
|
|
291
|
+
let src;
|
|
292
|
+
try {
|
|
293
|
+
src = await readFile(join(TPL_DIR, `${tpl}.html`), 'utf8');
|
|
294
|
+
} catch {
|
|
295
|
+
return null; // template missing — treat as a 404
|
|
296
|
+
}
|
|
297
|
+
const {html: stripped, styles} = stripStyleBlocks(stripHeader(src));
|
|
298
|
+
const body = escapeCodeBrackets(dedentOutsidePre(rewriteLinks(stripped))).trim();
|
|
248
299
|
const section = sectionFor(path);
|
|
249
|
-
const src = readFileSync(join(TPL, `${tpl}.html`), 'utf8');
|
|
250
|
-
const body = escapeCodeBrackets(dedentOutsidePre(rewriteLinks(stripStyleBlocks(stripHeader(src))))).trim();
|
|
251
300
|
// prev / next are the section-mates either side of this page in MAP order.
|
|
252
301
|
const mates = MAP.filter(m => sectionFor(m[1]).label === section.label);
|
|
253
302
|
const mi = mates.findIndex(m => m[1] === path);
|
|
254
303
|
const prev = mi > 0 ? mates[mi - 1] : null;
|
|
255
304
|
const next = mi >= 0 && mi < mates.length - 1 ? mates[mi + 1] : null;
|
|
256
|
-
|
|
257
|
-
mkdirSync(dirname(dest), {recursive: true});
|
|
258
|
-
writeFileSync(dest, frontmatter(title) + buildPage(section, title, body, prev, next) + '\n');
|
|
259
|
-
n++;
|
|
305
|
+
return {title, body: buildPage(section, title, body, prev, next), styles};
|
|
260
306
|
}
|
|
261
|
-
console.log(`[migrate-docs] wrote ${n} pages to ${OUT}`);
|
|
262
307
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
308
|
+
/**
|
|
309
|
+
* Render a Domma Docs page to full HTML, or return null for an unknown path
|
|
310
|
+
* (the route turns that into a 404). Cached per path+origin under the static
|
|
311
|
+
* 'docs' tag — invalidated on boot so a version bump never serves stale HTML.
|
|
312
|
+
*
|
|
313
|
+
* @param {string} docPath url tail after /domma-docs (e.g. 'usage/pages', '' for home)
|
|
314
|
+
* @param {{user?: object|null, baseUrl?: string}} [opts]
|
|
315
|
+
* @returns {Promise<string|null>}
|
|
316
|
+
*/
|
|
317
|
+
export async function renderDocPage(docPath, opts = {}) {
|
|
318
|
+
const path = String(docPath || '').replace(/^\/+|\/+$/g, '');
|
|
319
|
+
const built = await buildDocBody(path);
|
|
320
|
+
if (!built) return null;
|
|
321
|
+
|
|
322
|
+
const {user = null, baseUrl} = opts;
|
|
323
|
+
return cache.wrap(
|
|
324
|
+
`docs:${path}:o${baseUrl || ''}`,
|
|
325
|
+
async () => {
|
|
326
|
+
const parsed = await parseMarkdown(built.body, {user});
|
|
327
|
+
const css = [DOCS_BODY_CSS, ...built.styles].filter(Boolean).join('\n\n');
|
|
328
|
+
const html = `<style>${css}</style>\n${parsed.html}`;
|
|
329
|
+
const page = {
|
|
330
|
+
title: built.title,
|
|
331
|
+
layout: 'default',
|
|
332
|
+
status: 'published',
|
|
333
|
+
visibility: 'public',
|
|
334
|
+
project: 'domma-docs',
|
|
335
|
+
urlPath: `/domma-docs${path ? '/' + path : ''}`,
|
|
336
|
+
html,
|
|
337
|
+
usedComponents: parsed.usedComponents || [],
|
|
338
|
+
dconfig: parsed.data?.dconfig || null,
|
|
339
|
+
seo: {title: built.title},
|
|
340
|
+
sidebar: false,
|
|
341
|
+
tags: ['docs']
|
|
342
|
+
};
|
|
343
|
+
return renderPage(page, {baseUrl, user});
|
|
344
|
+
},
|
|
345
|
+
{tags: ['docs']}
|
|
346
|
+
);
|
|
347
|
+
}
|
|
270
348
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Every valid doc path (home + 29 leaves + 3 section landings). The Components
|
|
351
|
+
* landing is the 'components' leaf, already in MAP. `renderDocPage` returning
|
|
352
|
+
* null is the authoritative miss; this is an enumeration aid (e.g. sitemap).
|
|
353
|
+
*
|
|
354
|
+
* @returns {string[]}
|
|
355
|
+
*/
|
|
356
|
+
export function listDocPaths() {
|
|
357
|
+
return ['', ...MAP.map(m => m[1]), 'usage', 'tutorials', 'api'];
|
|
358
|
+
}
|