create-ailk 0.32.0 → 0.33.0
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 +9 -3
- package/component-catalog.json +12 -1
- package/component-catalog.md +6 -5
- package/dist/export-content.d.ts +6 -3
- package/dist/export-content.js +79 -10
- package/dist/lib/import-mdx.d.ts +42 -0
- package/dist/lib/import-mdx.js +531 -45
- package/dist/seed-flows.d.ts +1 -1
- package/dist/seed-flows.js +9 -4
- package/dist/surfaces.js +1 -0
- package/package.json +3 -3
- package/templates/apps/api/CLAUDE.md +7 -5
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +28 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/__tests__/actions.test.ts +94 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/actions.ts +77 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +79 -14
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +21 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +15 -9
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +38 -105
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.invite.test.tsx +72 -58
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/verify.test.tsx +249 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +164 -20
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/by-result.test.tsx +269 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/score.test.tsx +179 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +108 -40
- package/templates/apps/web/app/[locale]/flows/[slug]/verify/page.tsx +153 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/verify.actions.ts +151 -0
- package/templates/apps/web/app/[locale]/meet/confirmed/BookingHandOff.tsx +1 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +3 -20
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +2 -9
- package/templates/apps/web/auth.ts +22 -6
- package/templates/apps/web/lib/__tests__/account-config.test.ts +69 -0
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +99 -1
- package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +20 -0
- package/templates/apps/web/lib/account-config.ts +52 -14
- package/templates/apps/web/lib/auth-email.ts +87 -5
- package/templates/apps/web/lib/telemetry-scrub.ts +6 -0
- package/templates/apps/web/messages/en.json +26 -1
- package/templates/database/seed.ts +7 -7
- package/versions.json +48 -48
- package/templates/apps/web/app/[locale]/thanks/[slug]/AccountInviteLink.tsx +0 -89
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/AccountInviteLink.test.tsx +0 -104
package/dist/lib/import-mdx.js
CHANGED
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* reports `unchanged`; a changed layout on a live draft is refused
|
|
20
20
|
* `draft_is_live` and the page fails with that reason.
|
|
21
21
|
*
|
|
22
|
+
* A section string that holds inline HTML is converted first (#10804) — a
|
|
23
|
+
* link to `[label](href)`, a line break to a newline, a block to a blank
|
|
24
|
+
* line, every other tag unwrapped to its text — so the words and the layout
|
|
25
|
+
* carry no tag, which the hub's structure write refuses. The layout is then
|
|
26
|
+
* checked against the hub's section-string rules before any write, so a page
|
|
27
|
+
* the hub would refuse fails with the rule and the path, and a page whose
|
|
28
|
+
* draft holds no layout yet is never reported `unchanged`.
|
|
29
|
+
*
|
|
22
30
|
* create-ailk ships one runtime dependency, so the adapter is never imported:
|
|
23
31
|
* the caller injects it, and `loadSiteMdxAdapter` resolves the SITE's
|
|
24
32
|
* installed copy from its `apps/web` at run time (the #6937 precedent), and
|
|
@@ -29,7 +37,9 @@
|
|
|
29
37
|
* `<slug>/index.mdx` becomes a `BlogPosting` / `DocsDetail` page (the
|
|
30
38
|
* directory's own `index.mdx` its `BlogList` / `DocsList` index), its
|
|
31
39
|
* fumadocs frontmatter mapped to the stored vocabulary and written through the
|
|
32
|
-
* same converting path. Every outcome names the root it came from.
|
|
40
|
+
* same converting path. Every outcome names the root it came from. A key the
|
|
41
|
+
* vocabulary does not name rides in `siteFields`, and a blog post's tags are
|
|
42
|
+
* sent (#10806); a key the store cannot keep is named with its reason.
|
|
33
43
|
*
|
|
34
44
|
* Given `publish` (#10659 D2), each page with no Live version is set Live
|
|
35
45
|
* after it is written (`POST …/publish`, the draft tip); a page already Live
|
|
@@ -41,7 +51,8 @@
|
|
|
41
51
|
* a status and a `[a-z0-9_]` error code reach output (SEC-7876-S1).
|
|
42
52
|
*
|
|
43
53
|
* Spec: docs/specs/10203-import-mdx-one-mdx-artifact-spec.md; the editorial
|
|
44
|
-
* walk docs/specs/10701-import-mdx-walks-the-editorial-spec.md
|
|
54
|
+
* walk docs/specs/10701-import-mdx-walks-the-editorial-spec.md; its site
|
|
55
|
+
* fields docs/specs/10806-the-editorial-import-drops-blog-spec.md.
|
|
45
56
|
*/
|
|
46
57
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
47
58
|
import { createRequire } from "node:module";
|
|
@@ -71,10 +82,15 @@ const EDITORIAL_TYPES = {
|
|
|
71
82
|
/** The slug the runtime reads as a surface's own index. */
|
|
72
83
|
const INDEX_SLUG = "index";
|
|
73
84
|
const INDEX_TYPES = new Set(Object.values(EDITORIAL_TYPES).map((k) => k.index));
|
|
85
|
+
/** Each index type's own slug, its editorial directory: `BlogList/blog` shares `/blog` with `BlogList/index` (#10805 D5). */
|
|
86
|
+
const INDEX_OWN_SLUG = new Map(Object.entries(EDITORIAL_TYPES).map(([dir, k]) => [k.index, dir]));
|
|
87
|
+
/** `PAGE_TAG_TYPES` (the validation package's `site-fields.ts`), restated: the blog collection keeps a sent `tags`. */
|
|
88
|
+
const PAGE_TAG_TYPES = new Set(["BlogPosting", "BlogList"]);
|
|
74
89
|
/**
|
|
75
90
|
* `fumadocsToArtifactFrontmatter` (`apps/web/lib/runtime-source.ts`) restated —
|
|
76
|
-
* fumadocs key to stored key — less `tags`, which
|
|
77
|
-
* `sections`, which `convert` moves. Any other key
|
|
91
|
+
* fumadocs key to stored key — less `tags`, which only a blog page sends, plus
|
|
92
|
+
* `sections`, which `convert` moves. Any other key rides in `siteFields`
|
|
93
|
+
* (#10806 D4).
|
|
78
94
|
*/
|
|
79
95
|
const EDITORIAL_FIELDS = {
|
|
80
96
|
title: "title",
|
|
@@ -89,6 +105,14 @@ const EDITORIAL_FIELDS = {
|
|
|
89
105
|
};
|
|
90
106
|
/** Keys not sent and not reported: the path decides both, and a strip marker. */
|
|
91
107
|
const EDITORIAL_IGNORED = new Set(["type", "slug", "oss"]);
|
|
108
|
+
/** Why a key is not sent (#10806 D4); `siteFieldRefusal`'s own reasons are the rest. */
|
|
109
|
+
const NOT_SENT = {
|
|
110
|
+
parent: "the path decides a blog post's parent",
|
|
111
|
+
agentTag: "a tag agents act on, or a site tag, is kept by AI",
|
|
112
|
+
notList: "not a list of tags",
|
|
113
|
+
full: "siteFields is full (32 keys or 64 KiB)",
|
|
114
|
+
olderSite: `the site's ${VALIDATION_PACKAGE} predates site fields; update its dependencies and re-run`,
|
|
115
|
+
};
|
|
92
116
|
/** `wksp_<uuidv7>` — the `@working-theory/ids` shape, restated: no runtime dep. */
|
|
93
117
|
const WORKSPACE_ID = /^wksp_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
94
118
|
/** A key is visible ASCII only: what an HTTP header can carry, with no space. */
|
|
@@ -338,35 +362,118 @@ function dateString(value) {
|
|
|
338
362
|
const iso = value.toISOString();
|
|
339
363
|
return iso.endsWith("T00:00:00.000Z") ? iso.slice(0, 10) : iso;
|
|
340
364
|
}
|
|
365
|
+
/** Below this depth a value is left as read: the site's rule refuses it as too deep. */
|
|
366
|
+
const DATES_MAX_DEPTH = 8;
|
|
367
|
+
/** `value` with every `Date` to a bounded depth as `dateString` gives it (#10806 D4). */
|
|
368
|
+
function datesAsStrings(value, depth = 1) {
|
|
369
|
+
if (value instanceof Date)
|
|
370
|
+
return dateString(value);
|
|
371
|
+
if (depth > DATES_MAX_DEPTH)
|
|
372
|
+
return value;
|
|
373
|
+
if (Array.isArray(value))
|
|
374
|
+
return value.map((item) => datesAsStrings(item, depth + 1));
|
|
375
|
+
if (value === null || typeof value !== "object")
|
|
376
|
+
return value;
|
|
377
|
+
if (Object.getPrototypeOf(value) !== Object.prototype)
|
|
378
|
+
return value;
|
|
379
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, datesAsStrings(item, depth + 1)]));
|
|
380
|
+
}
|
|
381
|
+
/** A tag as printed: control characters as `?`, at most 64 characters. */
|
|
382
|
+
function printedTag(tag) {
|
|
383
|
+
// eslint-disable-next-line no-control-regex -- the controls are what is replaced
|
|
384
|
+
return String(tag).replace(/[\u0000-\u001f\u007f-\u009f]/g, "?").slice(0, 64);
|
|
385
|
+
}
|
|
341
386
|
/**
|
|
342
387
|
* An editorial file as the adapter would return a page (#10701 D4): its
|
|
343
388
|
* fumadocs frontmatter mapped to the stored vocabulary, the path deciding the
|
|
344
|
-
* type and slug — and, for a blog page, its `parent` (#10775 D3)
|
|
345
|
-
*
|
|
389
|
+
* type and slug — and, for a blog page, its `parent` (#10775 D3). Every other
|
|
390
|
+
* key the site's rule accepts rides in `siteFields`, and a blog page sends its
|
|
391
|
+
* `tags` (#10806 D4); the keys not sent are named with their reasons.
|
|
346
392
|
*/
|
|
347
|
-
function editorialSource(page, parsed) {
|
|
393
|
+
function editorialSource(page, parsed, rules) {
|
|
348
394
|
const frontmatter = {
|
|
349
395
|
type: page.type,
|
|
350
396
|
slug: page.slug,
|
|
351
397
|
};
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
398
|
+
const notSent = [];
|
|
399
|
+
const siteFields = {};
|
|
400
|
+
const blog = PAGE_TAG_TYPES.has(page.type);
|
|
401
|
+
for (const [key, raw] of Object.entries(parsed.data)) {
|
|
402
|
+
if (EDITORIAL_IGNORED.has(key))
|
|
403
|
+
continue;
|
|
404
|
+
if (blog && key === "parent") {
|
|
405
|
+
notSent.push({ key, reason: NOT_SENT.parent });
|
|
406
|
+
}
|
|
407
|
+
else if (Object.hasOwn(EDITORIAL_FIELDS, key)) {
|
|
408
|
+
frontmatter[EDITORIAL_FIELDS[key]] = dateString(raw);
|
|
409
|
+
}
|
|
410
|
+
else if (rules === undefined) {
|
|
411
|
+
notSent.push({ key, reason: NOT_SENT.olderSite });
|
|
412
|
+
}
|
|
413
|
+
else if (blog && key === "tags") {
|
|
414
|
+
if (!Array.isArray(raw)) {
|
|
415
|
+
notSent.push({ key, reason: NOT_SENT.notList });
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
// Sent as written, so the stored list is what the store computes; a
|
|
419
|
+
// tag it keeps for itself is named.
|
|
420
|
+
frontmatter["tags"] = raw;
|
|
421
|
+
const kept = new Set(rules.publicPageTags(raw));
|
|
422
|
+
for (const tag of new Set(raw.filter((tag) => !kept.has(tag)))) {
|
|
423
|
+
notSent.push({ key: `tags: ${printedTag(tag)}`, reason: NOT_SENT.agentTag });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
const value = datesAsStrings(raw);
|
|
428
|
+
const refused = rules.refusal(key, value);
|
|
429
|
+
const fits = Object.keys(siteFields).length < rules.maxKeys &&
|
|
430
|
+
new TextEncoder().encode(JSON.stringify({ ...siteFields, [key]: value })).length <=
|
|
431
|
+
rules.maxBytes;
|
|
432
|
+
if (refused !== null)
|
|
433
|
+
notSent.push({ key, reason: refused });
|
|
434
|
+
else if (!fits)
|
|
435
|
+
notSent.push({ key, reason: NOT_SENT.full });
|
|
436
|
+
else
|
|
437
|
+
siteFields[key] = value;
|
|
438
|
+
}
|
|
362
439
|
}
|
|
363
440
|
if (page.parent !== undefined)
|
|
364
441
|
frontmatter["parent"] = page.parent;
|
|
442
|
+
if (Object.keys(siteFields).length > 0)
|
|
443
|
+
frontmatter["siteFields"] = siteFields;
|
|
365
444
|
return {
|
|
366
445
|
source: { frontmatter, body: parsed.content },
|
|
367
|
-
|
|
446
|
+
notSent: notSent.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)),
|
|
368
447
|
};
|
|
369
448
|
}
|
|
449
|
+
/** `a (reason); b, c (reason)` — keys in order, one segment per reason. */
|
|
450
|
+
function notSentList(notSent) {
|
|
451
|
+
const byReason = new Map();
|
|
452
|
+
for (const { key, reason } of notSent) {
|
|
453
|
+
byReason.set(reason, [...(byReason.get(reason) ?? []), key]);
|
|
454
|
+
}
|
|
455
|
+
return [...byReason]
|
|
456
|
+
.map(([reason, keys]) => `${keys.join(", ")} (${reason})`)
|
|
457
|
+
.join("; ");
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* The sent site fields and public tags a write's stored content lacks
|
|
461
|
+
* (#10806 D4): what a hub that predates site fields dropped.
|
|
462
|
+
*/
|
|
463
|
+
function notKept(sent, kept, rules) {
|
|
464
|
+
const keptFields = kept["siteFields"] !== null && typeof kept["siteFields"] === "object"
|
|
465
|
+
? kept["siteFields"]
|
|
466
|
+
: {};
|
|
467
|
+
const keptTags = Array.isArray(kept["tags"]) ? kept["tags"] : [];
|
|
468
|
+
const sentFields = Object.keys((sent["siteFields"] ?? {}));
|
|
469
|
+
const sentTags = PAGE_TAG_TYPES.has(String(sent["type"])) && Array.isArray(sent["tags"])
|
|
470
|
+
? rules.publicPageTags(sent["tags"])
|
|
471
|
+
: [];
|
|
472
|
+
return [
|
|
473
|
+
...sentFields.filter((key) => !Object.hasOwn(keptFields, key)).sort(),
|
|
474
|
+
...sentTags.filter((tag) => !keptTags.includes(tag)).map((tag) => `tags: ${printedTag(tag)}`),
|
|
475
|
+
];
|
|
476
|
+
}
|
|
370
477
|
// ── "Unchanged" (D4) ────────────────────────────────────────────────────────
|
|
371
478
|
function sortKeys(value) {
|
|
372
479
|
if (Array.isArray(value))
|
|
@@ -382,14 +489,21 @@ function canonical(value) {
|
|
|
382
489
|
return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value ?? null))));
|
|
383
490
|
}
|
|
384
491
|
/**
|
|
385
|
-
* Content
|
|
386
|
-
*
|
|
492
|
+
* Content as compared: the store's tagger writes `tags` on every version
|
|
493
|
+
* (#10342 D5), so they are dropped — except a blog page that sends them, whose
|
|
494
|
+
* public tags are the importer's (#10806 D4): compared as `publicTags` gives
|
|
495
|
+
* them, so a changed category is an update.
|
|
387
496
|
*/
|
|
388
|
-
function
|
|
497
|
+
function compared(value, sent, publicTags) {
|
|
389
498
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
390
499
|
return value;
|
|
391
|
-
const { tags
|
|
392
|
-
|
|
500
|
+
const { tags, ...rest } = value;
|
|
501
|
+
if (publicTags === undefined ||
|
|
502
|
+
!PAGE_TAG_TYPES.has(String(sent["type"])) ||
|
|
503
|
+
!Array.isArray(sent["tags"])) {
|
|
504
|
+
return rest;
|
|
505
|
+
}
|
|
506
|
+
return { ...rest, tags: publicTags(Array.isArray(tags) ? tags : []) };
|
|
393
507
|
}
|
|
394
508
|
function safeCode(value) {
|
|
395
509
|
return typeof value === "string" && SAFE_CODE.test(value) ? value : "unknown";
|
|
@@ -474,16 +588,18 @@ function wrongLocale(at, answer, locale) {
|
|
|
474
588
|
}
|
|
475
589
|
return failed(at, `the API does not serve locale ${locale}`, `not sent: the API does not serve locale ${locale}`);
|
|
476
590
|
}
|
|
477
|
-
async function writePage(t, page, locale, content, layout) {
|
|
591
|
+
async function writePage(t, page, locale, content, layout, publicTags) {
|
|
478
592
|
const { root, path, type, slug } = page;
|
|
479
593
|
const pagePath = `${t.base}/content/${encodeURIComponent(type)}/${encodeURIComponent(slug)}`;
|
|
480
594
|
const query = `?locale=${encodeURIComponent(locale)}`;
|
|
481
595
|
const address = `${pagePath}${query}`;
|
|
482
596
|
// The route's `statusOf`: anything but `draft` has a Live version.
|
|
483
597
|
let live = false;
|
|
598
|
+
let stored;
|
|
484
599
|
const outcome = (result) => ({
|
|
485
600
|
outcome: { root, path, result, type, slug },
|
|
486
601
|
live,
|
|
602
|
+
stored,
|
|
487
603
|
});
|
|
488
604
|
// The words are written; now the layout, under the token the draft holds
|
|
489
605
|
// (`null` for a page that has none yet).
|
|
@@ -497,7 +613,11 @@ async function writePage(t, page, locale, content, layout) {
|
|
|
497
613
|
if ("network" in put || put.status !== 200)
|
|
498
614
|
return refusedAnswer(page, put);
|
|
499
615
|
return (wrongLocale(page, put, locale) ??
|
|
500
|
-
outcome(
|
|
616
|
+
outcome(
|
|
617
|
+
// #10804 D6 — with no token the hub writes the layout (or refuses a
|
|
618
|
+
// live tip), so a dry run's `written: false` never makes it unchanged.
|
|
619
|
+
result === "unchanged" &&
|
|
620
|
+
(structureToken === null || put.data["written"] === true)
|
|
501
621
|
? "updated"
|
|
502
622
|
: result));
|
|
503
623
|
};
|
|
@@ -515,8 +635,9 @@ async function writePage(t, page, locale, content, layout) {
|
|
|
515
635
|
live = read.data["status"] !== "draft";
|
|
516
636
|
const token = read.data["structureToken"];
|
|
517
637
|
const structureToken = typeof token === "string" ? token : null;
|
|
518
|
-
|
|
519
|
-
|
|
638
|
+
stored = read.data["content"];
|
|
639
|
+
if (canonical(compared(stored, content, publicTags)) ===
|
|
640
|
+
canonical(compared(content, content, publicTags))) {
|
|
520
641
|
return done("unchanged", structureToken);
|
|
521
642
|
}
|
|
522
643
|
// The token is the version just compared against, so a draft saved
|
|
@@ -524,6 +645,7 @@ async function writePage(t, page, locale, content, layout) {
|
|
|
524
645
|
const saved = await call(t, "PATCH", address, content, version);
|
|
525
646
|
if ("network" in saved || saved.status !== 200)
|
|
526
647
|
return refusedAnswer(page, saved);
|
|
648
|
+
stored = saved.data["content"];
|
|
527
649
|
// A save carries the structure token forward (#10330 D5).
|
|
528
650
|
return (wrongLocale(page, saved, locale) ?? done("updated", structureToken));
|
|
529
651
|
}
|
|
@@ -536,9 +658,10 @@ async function writePage(t, page, locale, content, layout) {
|
|
|
536
658
|
if ("network" in created)
|
|
537
659
|
return refusedAnswer(page, created);
|
|
538
660
|
if (created.status === 201) {
|
|
661
|
+
stored = created.data["content"];
|
|
539
662
|
return wrongLocale(page, created, locale) ?? done("created");
|
|
540
663
|
}
|
|
541
|
-
// 409 slug_taken: another
|
|
664
|
+
// 409 slug_taken: another page holds this page's registry link (#10268
|
|
542
665
|
// D5) — reported as it is, since a re-read at this address finds nothing.
|
|
543
666
|
// Any other 409: created by someone else since the read — read it once more.
|
|
544
667
|
if (created.status !== 409 || created.code === "slug_taken") {
|
|
@@ -564,6 +687,323 @@ export function formatImportMdxReport(result) {
|
|
|
564
687
|
`created ${created} · updated ${updated} · unchanged ${unchanged} · skipped ${skipped} · failed ${failures}`,
|
|
565
688
|
].join("\n")}\n`;
|
|
566
689
|
}
|
|
690
|
+
// ── Inline HTML in section strings (#10804) ─────────────────────────────────
|
|
691
|
+
/** `HTML_TAG` (`content-draft.ts`, from `derive-publish.ts`, SEC-10589-01), restated: no runtime dep. */
|
|
692
|
+
const HTML_TAG = /<[A-Za-z!/?]/;
|
|
693
|
+
/** `checkSectionData`'s string and key rules (`content-draft.ts`), restated: no runtime dep. */
|
|
694
|
+
const SECTION_STRING_MAX = 2_000;
|
|
695
|
+
const SECTION_KEY_MAX = 100;
|
|
696
|
+
const SCRIPT_SCHEME = /^(?:javascript|vbscript|data):/;
|
|
697
|
+
/** `FAQ_ALLOWED_HREF` (`faq-questions.ts`), the hrefs `parseFaqAnswer` draws as links, restated. */
|
|
698
|
+
const LINK_HREF = /^(?:\/|#|https?:|mailto:|tel:)/i;
|
|
699
|
+
/** Elements removed with their content (D3). */
|
|
700
|
+
const DROPPED_ELEMENTS = new Set([
|
|
701
|
+
"script",
|
|
702
|
+
"style",
|
|
703
|
+
"template",
|
|
704
|
+
"iframe",
|
|
705
|
+
"noscript",
|
|
706
|
+
"svg",
|
|
707
|
+
"object",
|
|
708
|
+
"video",
|
|
709
|
+
"audio",
|
|
710
|
+
"canvas",
|
|
711
|
+
]);
|
|
712
|
+
/** Elements whose open or close tag is a paragraph break (D3). */
|
|
713
|
+
const BLOCK_ELEMENTS = new Set([
|
|
714
|
+
"p",
|
|
715
|
+
"div",
|
|
716
|
+
"section",
|
|
717
|
+
"article",
|
|
718
|
+
"header",
|
|
719
|
+
"footer",
|
|
720
|
+
"blockquote",
|
|
721
|
+
"figure",
|
|
722
|
+
"figcaption",
|
|
723
|
+
"pre",
|
|
724
|
+
"table",
|
|
725
|
+
"tr",
|
|
726
|
+
"dl",
|
|
727
|
+
"dt",
|
|
728
|
+
"dd",
|
|
729
|
+
"h1",
|
|
730
|
+
"h2",
|
|
731
|
+
"h3",
|
|
732
|
+
"h4",
|
|
733
|
+
"h5",
|
|
734
|
+
"h6",
|
|
735
|
+
"ul",
|
|
736
|
+
"ol",
|
|
737
|
+
"hr",
|
|
738
|
+
]);
|
|
739
|
+
/** What the layout keeps of a tag (D3): `a` only when it becomes a link. */
|
|
740
|
+
const CARRIED_ELEMENTS = new Set(["a", "br", "p", "ul", "li"]);
|
|
741
|
+
const NAMED_ENTITIES = {
|
|
742
|
+
amp: "&",
|
|
743
|
+
lt: "<",
|
|
744
|
+
gt: ">",
|
|
745
|
+
quot: '"',
|
|
746
|
+
apos: "'",
|
|
747
|
+
nbsp: "\u00a0",
|
|
748
|
+
};
|
|
749
|
+
const ENTITY = /&(?:#(\d+)|#[xX]([0-9A-Fa-f]+)|(amp|lt|gt|quot|apos|nbsp));/g;
|
|
750
|
+
const TAG_NAME = /\/?([A-Za-z][A-Za-z0-9-]*)/y;
|
|
751
|
+
const TAG_END = /[ \t\r\n\f]*(\/?)>/y;
|
|
752
|
+
/**
|
|
753
|
+
* One attribute, or one stray character a browser would skip. A `<` ends the
|
|
754
|
+
* scan whether it is unquoted or inside a quoted value, so a `<` that begins
|
|
755
|
+
* no tag costs only the gap to the next one and the tokenizer stays linear in
|
|
756
|
+
* the string (spec §9). D3 needs a quoted value to hold `>`, never `<`.
|
|
757
|
+
*/
|
|
758
|
+
const TAG_ATTR = /[ \t\r\n\f]*(?:([^ \t\r\n\f"'<>/=]+)(?:[ \t\r\n\f]*=[ \t\r\n\f]*(?:"([^"<]*)"|'([^'<]*)'|([^ \t\r\n\f<>]+)))?|[^ \t\r\n\f<>])/y;
|
|
759
|
+
const NEWLINE = 1;
|
|
760
|
+
const PARAGRAPH = 2;
|
|
761
|
+
function decodeEntities(text) {
|
|
762
|
+
return text.replace(ENTITY, (source, dec, hex, name) => {
|
|
763
|
+
if (name !== undefined)
|
|
764
|
+
return NAMED_ENTITIES[name];
|
|
765
|
+
const point = dec !== undefined ? Number(dec) : parseInt(hex, 16);
|
|
766
|
+
return point > 0 &&
|
|
767
|
+
point <= 0x10ffff &&
|
|
768
|
+
(point < 0xd800 || point > 0xdfff)
|
|
769
|
+
? String.fromCodePoint(point)
|
|
770
|
+
: source;
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
/** The tag at `at` (`<`, `/`?, a name, attributes, `/`?, `>`), or `undefined` when `<` begins none. */
|
|
774
|
+
function readTag(value, at) {
|
|
775
|
+
TAG_NAME.lastIndex = at + 1;
|
|
776
|
+
const name = TAG_NAME.exec(value)?.[1];
|
|
777
|
+
if (name === undefined)
|
|
778
|
+
return undefined;
|
|
779
|
+
let href;
|
|
780
|
+
for (let i = TAG_NAME.lastIndex;; i = TAG_ATTR.lastIndex) {
|
|
781
|
+
TAG_END.lastIndex = i;
|
|
782
|
+
const end = TAG_END.exec(value);
|
|
783
|
+
if (end !== null) {
|
|
784
|
+
return {
|
|
785
|
+
end: TAG_END.lastIndex,
|
|
786
|
+
close: value[at + 1] === "/",
|
|
787
|
+
selfClosing: end[1] === "/",
|
|
788
|
+
name: name.toLowerCase(),
|
|
789
|
+
...(href !== undefined && { href }),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
TAG_ATTR.lastIndex = i;
|
|
793
|
+
const attr = TAG_ATTR.exec(value);
|
|
794
|
+
if (attr === null)
|
|
795
|
+
return undefined;
|
|
796
|
+
if (href === undefined && attr[1]?.toLowerCase() === "href") {
|
|
797
|
+
href = attr[2] ?? attr[3] ?? attr[4] ?? "";
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* A section string's inline HTML as the constrained Markdown the upstream
|
|
803
|
+
* renderers read (#10804 D3): a link with an allowed href becomes
|
|
804
|
+
* `[label](href)`, `<br>` a newline, a block or list item a paragraph break,
|
|
805
|
+
* and every other tag goes, its text kept. A string with no tag comes back
|
|
806
|
+
* byte-identical; the result never holds one. `notCarried` names, sorted,
|
|
807
|
+
* every tag the layout does not keep.
|
|
808
|
+
*/
|
|
809
|
+
export function inlineHtmlToMarkdown(value) {
|
|
810
|
+
if (!HTML_TAG.test(value))
|
|
811
|
+
return { text: value, notCarried: [] };
|
|
812
|
+
const pieces = [];
|
|
813
|
+
const notCarried = new Set();
|
|
814
|
+
const text = (raw) => {
|
|
815
|
+
const decoded = decodeEntities(raw.replace(/[ \t\r\n\f]+/g, " "));
|
|
816
|
+
decoded.split("\n").forEach((part, i) => {
|
|
817
|
+
if (i > 0)
|
|
818
|
+
pieces.push(NEWLINE);
|
|
819
|
+
if (part !== "")
|
|
820
|
+
pieces.push(part);
|
|
821
|
+
});
|
|
822
|
+
};
|
|
823
|
+
let link;
|
|
824
|
+
const closeLink = () => {
|
|
825
|
+
if (link === undefined)
|
|
826
|
+
return;
|
|
827
|
+
const label = pieces.slice(link.at);
|
|
828
|
+
const words = label.every((p) => typeof p === "string")
|
|
829
|
+
? label.join("")
|
|
830
|
+
: "";
|
|
831
|
+
if (words !== "" &&
|
|
832
|
+
!words.includes("]") &&
|
|
833
|
+
LINK_HREF.test(link.href) &&
|
|
834
|
+
!/[\s)]/.test(link.href)) {
|
|
835
|
+
pieces.splice(link.at, label.length, `[${words}](${link.href})`);
|
|
836
|
+
}
|
|
837
|
+
else {
|
|
838
|
+
notCarried.add("a");
|
|
839
|
+
}
|
|
840
|
+
link = undefined;
|
|
841
|
+
};
|
|
842
|
+
let from = 0;
|
|
843
|
+
for (let lt = value.indexOf("<"); lt !== -1; lt = value.indexOf("<", Math.max(lt + 1, from))) {
|
|
844
|
+
if (value.startsWith("<!--", lt)) {
|
|
845
|
+
text(value.slice(from, lt));
|
|
846
|
+
const end = value.indexOf("-->", lt + 4);
|
|
847
|
+
from = end === -1 ? value.length : end + 3;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
const tag = readTag(value, lt);
|
|
851
|
+
if (tag === undefined)
|
|
852
|
+
continue;
|
|
853
|
+
text(value.slice(from, lt));
|
|
854
|
+
from = tag.end;
|
|
855
|
+
const { name } = tag;
|
|
856
|
+
if (!CARRIED_ELEMENTS.has(name))
|
|
857
|
+
notCarried.add(name);
|
|
858
|
+
if (DROPPED_ELEMENTS.has(name)) {
|
|
859
|
+
if (!tag.close && !tag.selfClosing) {
|
|
860
|
+
const close = new RegExp(`</${name}[ \\t\\r\\n\\f]*>`, "gi");
|
|
861
|
+
close.lastIndex = from;
|
|
862
|
+
from = close.exec(value) === null ? value.length : close.lastIndex;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
else if (name === "a") {
|
|
866
|
+
closeLink();
|
|
867
|
+
if (!tag.close) {
|
|
868
|
+
link = { at: pieces.length, href: decodeEntities(tag.href ?? "") };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else if (name === "br") {
|
|
872
|
+
pieces.push(NEWLINE);
|
|
873
|
+
}
|
|
874
|
+
else if (name === "li") {
|
|
875
|
+
// An item with no text leaves no bullet.
|
|
876
|
+
if (tag.close && pieces.at(-1) === "- ")
|
|
877
|
+
pieces.pop();
|
|
878
|
+
pieces.push(PARAGRAPH);
|
|
879
|
+
if (!tag.close)
|
|
880
|
+
pieces.push("- ");
|
|
881
|
+
}
|
|
882
|
+
else if (BLOCK_ELEMENTS.has(name)) {
|
|
883
|
+
pieces.push(PARAGRAPH);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
text(value.slice(from));
|
|
887
|
+
closeLink();
|
|
888
|
+
// Spaces never touch a newline; a run holding a paragraph break, or two or
|
|
889
|
+
// more newlines, is one blank line; the ends are trimmed.
|
|
890
|
+
const parts = [];
|
|
891
|
+
let run;
|
|
892
|
+
for (const piece of pieces) {
|
|
893
|
+
if (typeof piece !== "string") {
|
|
894
|
+
if (run === undefined) {
|
|
895
|
+
while (parts.length > 0) {
|
|
896
|
+
const last = parts[parts.length - 1].replace(/ +$/, "");
|
|
897
|
+
if (last !== "") {
|
|
898
|
+
parts[parts.length - 1] = last;
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
parts.pop();
|
|
902
|
+
}
|
|
903
|
+
run = { newlines: 0, paragraph: false };
|
|
904
|
+
}
|
|
905
|
+
if (piece === PARAGRAPH)
|
|
906
|
+
run.paragraph = true;
|
|
907
|
+
else
|
|
908
|
+
run.newlines += 1;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
let words = piece;
|
|
912
|
+
if (run !== undefined) {
|
|
913
|
+
words = words.replace(/^ +/, "");
|
|
914
|
+
if (words === "")
|
|
915
|
+
continue;
|
|
916
|
+
if (parts.length > 0) {
|
|
917
|
+
parts.push(run.paragraph || run.newlines > 1 ? "\n\n" : "\n");
|
|
918
|
+
}
|
|
919
|
+
run = undefined;
|
|
920
|
+
}
|
|
921
|
+
else if (words.startsWith(" ") && parts.at(-1)?.endsWith(" ") === true) {
|
|
922
|
+
words = words.slice(1);
|
|
923
|
+
}
|
|
924
|
+
parts.push(words);
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
text: parts
|
|
928
|
+
.join("")
|
|
929
|
+
.replace(/^[ \n]+|[ \n]+$/g, "")
|
|
930
|
+
.replace(/<(?=[A-Za-z!/?])/g, "<\u2060"),
|
|
931
|
+
notCarried: [...notCarried].sort(),
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const isPlainObject = (value) => value !== null &&
|
|
935
|
+
typeof value === "object" &&
|
|
936
|
+
[Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
937
|
+
/**
|
|
938
|
+
* Each section with every string in its `data` that holds a tag converted by
|
|
939
|
+
* `inlineHtmlToMarkdown` (#10804 D2); keys and tag-free strings are kept as
|
|
940
|
+
* they are. `notCarried` is the sorted union of the names it did not carry.
|
|
941
|
+
*/
|
|
942
|
+
export function sectionsWithoutHtml(sections) {
|
|
943
|
+
const names = new Set();
|
|
944
|
+
const walk = (value) => {
|
|
945
|
+
if (typeof value === "string") {
|
|
946
|
+
const out = inlineHtmlToMarkdown(value);
|
|
947
|
+
for (const name of out.notCarried)
|
|
948
|
+
names.add(name);
|
|
949
|
+
return out.text;
|
|
950
|
+
}
|
|
951
|
+
if (Array.isArray(value))
|
|
952
|
+
return value.map(walk);
|
|
953
|
+
if (!isPlainObject(value))
|
|
954
|
+
return value;
|
|
955
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, walk(child)]));
|
|
956
|
+
};
|
|
957
|
+
return {
|
|
958
|
+
sections: sections.map((section) => isPlainObject(section) && "data" in section
|
|
959
|
+
? { ...section, data: walk(section.data) }
|
|
960
|
+
: section),
|
|
961
|
+
notCarried: [...names].sort(),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
/** The first string or key rule of the hub's that `value` breaks, at `path`. */
|
|
965
|
+
function refusalAt(value, path) {
|
|
966
|
+
if (typeof value === "string") {
|
|
967
|
+
// A URL parser drops tabs, newlines and leading control characters.
|
|
968
|
+
// eslint-disable-next-line no-control-regex -- the control range is the point
|
|
969
|
+
const scheme = value.replace(/[\u0000- ]/g, "").toLowerCase();
|
|
970
|
+
const message = value.length > SECTION_STRING_MAX
|
|
971
|
+
? `A section string may hold at most ${SECTION_STRING_MAX} characters`
|
|
972
|
+
: HTML_TAG.test(value)
|
|
973
|
+
? "A section string may not hold an HTML tag"
|
|
974
|
+
: SCRIPT_SCHEME.test(scheme)
|
|
975
|
+
? "A section string may not use a script-bearing URL scheme"
|
|
976
|
+
: undefined;
|
|
977
|
+
return message === undefined ? undefined : `${message} (${path})`;
|
|
978
|
+
}
|
|
979
|
+
if (value === null || typeof value !== "object")
|
|
980
|
+
return undefined;
|
|
981
|
+
const array = Array.isArray(value);
|
|
982
|
+
for (const [key, child] of Object.entries(value)) {
|
|
983
|
+
if (!array && (key.length > SECTION_KEY_MAX || HTML_TAG.test(key))) {
|
|
984
|
+
return `A section key must be a short plain name (${path}.${key})`;
|
|
985
|
+
}
|
|
986
|
+
const refusal = refusalAt(child, array ? `${path}[${key}]` : `${path}.${key}`);
|
|
987
|
+
if (refusal !== undefined)
|
|
988
|
+
return refusal;
|
|
989
|
+
}
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* The first section-string or key rule of the hub's structure write
|
|
994
|
+
* (`checkSectionData`) the layout breaks, as `<message> (sections[i].data<path>)`,
|
|
995
|
+
* or `undefined` (#10804 D5). The depth and size bounds are not restated.
|
|
996
|
+
*/
|
|
997
|
+
export function layoutRefusal(layout) {
|
|
998
|
+
for (const [index, section] of layout.sections.entries()) {
|
|
999
|
+
if (!isPlainObject(section) || section["data"] === undefined)
|
|
1000
|
+
continue;
|
|
1001
|
+
const refusal = refusalAt(section["data"], `sections[${index}].data`);
|
|
1002
|
+
if (refusal !== undefined)
|
|
1003
|
+
return refusal;
|
|
1004
|
+
}
|
|
1005
|
+
return undefined;
|
|
1006
|
+
}
|
|
567
1007
|
// ── The importer ────────────────────────────────────────────────────────────
|
|
568
1008
|
/** One whitespace-collapsed line of at most `ADAPTER_REASON_MAX` characters. */
|
|
569
1009
|
function adapterReason(err, stage = "adapter") {
|
|
@@ -573,14 +1013,19 @@ function adapterReason(err, stage = "adapter") {
|
|
|
573
1013
|
/**
|
|
574
1014
|
* What the page stores (#10534): with `sections` and a codec, its words after
|
|
575
1015
|
* the body's own text and its sections as the layout; else the page as read.
|
|
1016
|
+
* Both come from one array, its inline HTML converted (#10804 D2).
|
|
576
1017
|
*/
|
|
577
1018
|
function convert(source, codec) {
|
|
578
|
-
const { sections, ...frontmatter } = source.frontmatter;
|
|
579
|
-
if (!Array.isArray(
|
|
580
|
-
|
|
1019
|
+
const { sections: authored, ...frontmatter } = source.frontmatter;
|
|
1020
|
+
if (!Array.isArray(authored) ||
|
|
1021
|
+
authored.length === 0 ||
|
|
581
1022
|
codec === undefined) {
|
|
582
|
-
return {
|
|
1023
|
+
return {
|
|
1024
|
+
content: { ...source.frontmatter, body: source.body },
|
|
1025
|
+
notCarried: [],
|
|
1026
|
+
};
|
|
583
1027
|
}
|
|
1028
|
+
const { sections, notCarried } = sectionsWithoutHtml(authored);
|
|
584
1029
|
let words;
|
|
585
1030
|
try {
|
|
586
1031
|
words = codec(sections);
|
|
@@ -595,6 +1040,7 @@ function convert(source, codec) {
|
|
|
595
1040
|
body: lead === "" ? words : `${lead}\n\n${words}`,
|
|
596
1041
|
},
|
|
597
1042
|
layout: { kind: "sections", sections },
|
|
1043
|
+
notCarried,
|
|
598
1044
|
};
|
|
599
1045
|
}
|
|
600
1046
|
export async function importMdx(opts) {
|
|
@@ -616,15 +1062,17 @@ export async function importMdx(opts) {
|
|
|
616
1062
|
? { pages: [], skipped: [] }
|
|
617
1063
|
: planEditorial(editorialRoot, types, defaultLocale);
|
|
618
1064
|
const parseFrontmatter = opts.adapters.parseFrontmatter;
|
|
1065
|
+
const rules = opts.adapters.siteFields;
|
|
619
1066
|
// #10701 D3 — one route path holds one page, so an editorial index the run
|
|
620
|
-
// will write supersedes
|
|
621
|
-
// scaffold's teaser), which the create would refuse `slug_taken`.
|
|
1067
|
+
// will write supersedes the content page of its type at the surface's own
|
|
1068
|
+
// slug (the scaffold's teaser), which the create would refuse `slug_taken`.
|
|
1069
|
+
// Any other slug of the type has a link of its own (#10805 D5).
|
|
622
1070
|
const indexTypes = new Set(parseFrontmatter === undefined
|
|
623
1071
|
? []
|
|
624
1072
|
: editorial.pages
|
|
625
1073
|
.filter((p) => p.invalid === undefined && INDEX_TYPES.has(p.type))
|
|
626
1074
|
.map((p) => p.type));
|
|
627
|
-
const superseded = (p) => indexTypes.has(p.type) && p.slug
|
|
1075
|
+
const superseded = (p) => indexTypes.has(p.type) && p.slug === INDEX_OWN_SLUG.get(p.type);
|
|
628
1076
|
const pages = [
|
|
629
1077
|
...content.pages.filter((p) => !superseded(p)),
|
|
630
1078
|
...editorial.pages,
|
|
@@ -681,15 +1129,15 @@ export async function importMdx(opts) {
|
|
|
681
1129
|
}
|
|
682
1130
|
let read;
|
|
683
1131
|
try {
|
|
684
|
-
read = editorialSource(page, parseFrontmatter(readFileSync(join(editorialRoot, page.path), "utf8")));
|
|
1132
|
+
read = editorialSource(page, parseFrontmatter(readFileSync(join(editorialRoot, page.path), "utf8")), rules);
|
|
685
1133
|
}
|
|
686
1134
|
catch (err) {
|
|
687
1135
|
fail(adapterReason(err, "editorial"));
|
|
688
1136
|
continue;
|
|
689
1137
|
}
|
|
690
1138
|
source = read.source;
|
|
691
|
-
if (read.
|
|
692
|
-
onLine(`${page.path}: not imported — ${read.
|
|
1139
|
+
if (read.notSent.length > 0) {
|
|
1140
|
+
onLine(`${page.path}: not imported — ${notSentList(read.notSent)}`);
|
|
693
1141
|
}
|
|
694
1142
|
}
|
|
695
1143
|
else {
|
|
@@ -712,13 +1160,37 @@ export async function importMdx(opts) {
|
|
|
712
1160
|
fail(converted.reason);
|
|
713
1161
|
continue;
|
|
714
1162
|
}
|
|
1163
|
+
if (converted.notCarried.length > 0) {
|
|
1164
|
+
onLine(`${outcomePath(page)}: inline HTML not carried — ${converted.notCarried.join(", ")} (the layout holds no HTML; their text is kept)`);
|
|
1165
|
+
}
|
|
715
1166
|
if (converted.layout === undefined &&
|
|
716
1167
|
Array.isArray(source.frontmatter["sections"]) &&
|
|
717
1168
|
source.frontmatter["sections"].length > 0) {
|
|
718
1169
|
onLine(`${page.path}: keeps its words in sections — the site's ${VALIDATION_PACKAGE} has no words codec; update its dependencies and re-run`);
|
|
719
1170
|
}
|
|
720
|
-
|
|
1171
|
+
// #10804 D5 — a layout the hub would refuse fails here, in a dry run and
|
|
1172
|
+
// under --apply alike, before its words are saved.
|
|
1173
|
+
const refusal = converted.layout === undefined
|
|
1174
|
+
? undefined
|
|
1175
|
+
: layoutRefusal(converted.layout);
|
|
1176
|
+
if (refusal !== undefined) {
|
|
1177
|
+
fail(`layout: ${refusal}`);
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
const result = await writePage(t, page, defaultLocale, converted.content, converted.layout, rules?.publicPageTags);
|
|
721
1181
|
halt = result.halt;
|
|
1182
|
+
// #10806 D4 — an older hub strips what it has no member for; say so.
|
|
1183
|
+
// An answer that carries no content says nothing about what was kept.
|
|
1184
|
+
const dropped = rules === undefined ||
|
|
1185
|
+
page.root !== "editorial" ||
|
|
1186
|
+
"reason" in result.outcome ||
|
|
1187
|
+
result.stored === null ||
|
|
1188
|
+
typeof result.stored !== "object"
|
|
1189
|
+
? []
|
|
1190
|
+
: notKept(converted.content, result.stored, rules);
|
|
1191
|
+
if (dropped.length > 0) {
|
|
1192
|
+
onLine(`${page.path}: not kept by the hub: ${dropped.join(", ")} (the hub predates site fields; update it and re-run)`);
|
|
1193
|
+
}
|
|
722
1194
|
if (opts.publish !== true || "reason" in result.outcome) {
|
|
723
1195
|
record(result.outcome);
|
|
724
1196
|
continue;
|
|
@@ -806,16 +1278,19 @@ export async function loadSiteMdxAdapter(siteRoot) {
|
|
|
806
1278
|
!types.every((type) => typeof type === "string")) {
|
|
807
1279
|
throw new ImportMdxRefusedError(`import-mdx: ${CONTENT_ADAPTERS_PACKAGE} in ${web} does not export createMdxAdapter and SCHEMA_ORG_TYPES. Install the site's dependencies (pnpm install) first. Nothing was sent.`);
|
|
808
1280
|
}
|
|
809
|
-
// The codec the adapter's own validation package carries (#10534)
|
|
810
|
-
// that predates
|
|
811
|
-
|
|
1281
|
+
// The codec the adapter's own validation package carries (#10534), and its
|
|
1282
|
+
// site-field rule (#10806 D4); one that predates either, or none, leaves
|
|
1283
|
+
// pages unconverted or site fields unsent, and the run says so.
|
|
1284
|
+
let validation = {};
|
|
812
1285
|
try {
|
|
813
|
-
const
|
|
814
|
-
|
|
1286
|
+
const path = createRequire(entry).resolve(VALIDATION_PACKAGE);
|
|
1287
|
+
validation = (await import(pathToFileURL(path).href));
|
|
815
1288
|
}
|
|
816
1289
|
catch {
|
|
817
|
-
// Not resolvable from the adapter: no codec.
|
|
1290
|
+
// Not resolvable from the adapter: no codec and no site-field rule.
|
|
818
1291
|
}
|
|
1292
|
+
const codec = validation["sectionsToMarkdown"];
|
|
1293
|
+
const { siteFieldRefusal: refusal, publicPageTags, SITE_FIELDS_MAX_KEYS: maxKeys, SITE_FIELDS_MAX_BYTES: maxBytes, } = validation;
|
|
819
1294
|
// The adapter's own frontmatter parser (#10701 D1), for the editorial tree;
|
|
820
1295
|
// none leaves every editorial page failed, and the run says so.
|
|
821
1296
|
let matter;
|
|
@@ -831,6 +1306,17 @@ export async function loadSiteMdxAdapter(siteRoot) {
|
|
|
831
1306
|
...(typeof codec === "function" && {
|
|
832
1307
|
sectionsToMarkdown: codec,
|
|
833
1308
|
}),
|
|
1309
|
+
...(typeof refusal === "function" &&
|
|
1310
|
+
typeof publicPageTags === "function" &&
|
|
1311
|
+
typeof maxKeys === "number" &&
|
|
1312
|
+
typeof maxBytes === "number" && {
|
|
1313
|
+
siteFields: {
|
|
1314
|
+
refusal: refusal,
|
|
1315
|
+
publicPageTags: publicPageTags,
|
|
1316
|
+
maxKeys,
|
|
1317
|
+
maxBytes,
|
|
1318
|
+
},
|
|
1319
|
+
}),
|
|
834
1320
|
...(typeof matter === "function" && {
|
|
835
1321
|
parseFrontmatter: (raw) => {
|
|
836
1322
|
const { data, content } = matter(raw);
|