@anydocs/core 1.0.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.
Files changed (124) hide show
  1. package/dist/config/index.d.ts +2 -0
  2. package/dist/config/index.d.ts.map +1 -0
  3. package/dist/config/index.js +1 -0
  4. package/dist/config/project-config.d.ts +10 -0
  5. package/dist/config/project-config.d.ts.map +1 -0
  6. package/dist/config/project-config.js +52 -0
  7. package/dist/errors/domain-error.d.ts +12 -0
  8. package/dist/errors/domain-error.d.ts.map +1 -0
  9. package/dist/errors/domain-error.js +8 -0
  10. package/dist/errors/index.d.ts +3 -0
  11. package/dist/errors/index.d.ts.map +1 -0
  12. package/dist/errors/index.js +2 -0
  13. package/dist/errors/validation-error.d.ts +5 -0
  14. package/dist/errors/validation-error.d.ts.map +1 -0
  15. package/dist/errors/validation-error.js +7 -0
  16. package/dist/fs/api-source-repository.d.ts +16 -0
  17. package/dist/fs/api-source-repository.d.ts.map +1 -0
  18. package/dist/fs/api-source-repository.js +89 -0
  19. package/dist/fs/content-repository.d.ts +13 -0
  20. package/dist/fs/content-repository.d.ts.map +1 -0
  21. package/dist/fs/content-repository.js +171 -0
  22. package/dist/fs/docs-repository.d.ts +26 -0
  23. package/dist/fs/docs-repository.d.ts.map +1 -0
  24. package/dist/fs/docs-repository.js +270 -0
  25. package/dist/fs/index.d.ts +5 -0
  26. package/dist/fs/index.d.ts.map +1 -0
  27. package/dist/fs/index.js +4 -0
  28. package/dist/fs/project-paths.d.ts +4 -0
  29. package/dist/fs/project-paths.d.ts.map +1 -0
  30. package/dist/fs/project-paths.js +55 -0
  31. package/dist/index.d.ts +9 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +8 -0
  34. package/dist/publishing/build-artifacts.d.ts +4 -0
  35. package/dist/publishing/build-artifacts.d.ts.map +1 -0
  36. package/dist/publishing/build-artifacts.js +453 -0
  37. package/dist/publishing/build-openapi-artifacts.d.ts +3 -0
  38. package/dist/publishing/build-openapi-artifacts.d.ts.map +1 -0
  39. package/dist/publishing/build-openapi-artifacts.js +253 -0
  40. package/dist/publishing/index.d.ts +4 -0
  41. package/dist/publishing/index.d.ts.map +1 -0
  42. package/dist/publishing/index.js +3 -0
  43. package/dist/publishing/publication-filter.d.ts +22 -0
  44. package/dist/publishing/publication-filter.d.ts.map +1 -0
  45. package/dist/publishing/publication-filter.js +98 -0
  46. package/dist/schemas/api-source-schema.d.ts +3 -0
  47. package/dist/schemas/api-source-schema.d.ts.map +1 -0
  48. package/dist/schemas/api-source-schema.js +110 -0
  49. package/dist/schemas/docs-schema.d.ts +7 -0
  50. package/dist/schemas/docs-schema.d.ts.map +1 -0
  51. package/dist/schemas/docs-schema.js +212 -0
  52. package/dist/schemas/index.d.ts +4 -0
  53. package/dist/schemas/index.d.ts.map +1 -0
  54. package/dist/schemas/index.js +3 -0
  55. package/dist/schemas/project-schema.d.ts +3 -0
  56. package/dist/schemas/project-schema.d.ts.map +1 -0
  57. package/dist/schemas/project-schema.js +268 -0
  58. package/dist/services/authoring-service.d.ts +137 -0
  59. package/dist/services/authoring-service.d.ts.map +1 -0
  60. package/dist/services/authoring-service.js +583 -0
  61. package/dist/services/build-service.d.ts +35 -0
  62. package/dist/services/build-service.d.ts.map +1 -0
  63. package/dist/services/build-service.js +84 -0
  64. package/dist/services/index.d.ts +11 -0
  65. package/dist/services/index.d.ts.map +1 -0
  66. package/dist/services/index.js +10 -0
  67. package/dist/services/init-service.d.ts +15 -0
  68. package/dist/services/init-service.d.ts.map +1 -0
  69. package/dist/services/init-service.js +127 -0
  70. package/dist/services/legacy-conversion-service.d.ts +8 -0
  71. package/dist/services/legacy-conversion-service.d.ts.map +1 -0
  72. package/dist/services/legacy-conversion-service.js +601 -0
  73. package/dist/services/legacy-import-service.d.ts +10 -0
  74. package/dist/services/legacy-import-service.d.ts.map +1 -0
  75. package/dist/services/legacy-import-service.js +239 -0
  76. package/dist/services/page-template-service.d.ts +81 -0
  77. package/dist/services/page-template-service.d.ts.map +1 -0
  78. package/dist/services/page-template-service.js +342 -0
  79. package/dist/services/preview-service.d.ts +29 -0
  80. package/dist/services/preview-service.d.ts.map +1 -0
  81. package/dist/services/preview-service.js +45 -0
  82. package/dist/services/watch-service.d.ts +24 -0
  83. package/dist/services/watch-service.d.ts.map +1 -0
  84. package/dist/services/watch-service.js +216 -0
  85. package/dist/services/web-runtime-bridge.d.ts +33 -0
  86. package/dist/services/web-runtime-bridge.d.ts.map +1 -0
  87. package/dist/services/web-runtime-bridge.js +330 -0
  88. package/dist/services/workflow-compatibility-service.d.ts +3 -0
  89. package/dist/services/workflow-compatibility-service.d.ts.map +1 -0
  90. package/dist/services/workflow-compatibility-service.js +53 -0
  91. package/dist/services/workflow-standard-service.d.ts +9 -0
  92. package/dist/services/workflow-standard-service.d.ts.map +1 -0
  93. package/dist/services/workflow-standard-service.js +372 -0
  94. package/dist/types/api-source.d.ts +34 -0
  95. package/dist/types/api-source.d.ts.map +1 -0
  96. package/dist/types/api-source.js +8 -0
  97. package/dist/types/docs.d.ts +65 -0
  98. package/dist/types/docs.d.ts.map +1 -0
  99. package/dist/types/docs.js +8 -0
  100. package/dist/types/index.d.ts +6 -0
  101. package/dist/types/index.d.ts.map +1 -0
  102. package/dist/types/index.js +5 -0
  103. package/dist/types/legacy-import.d.ts +72 -0
  104. package/dist/types/legacy-import.d.ts.map +1 -0
  105. package/dist/types/legacy-import.js +1 -0
  106. package/dist/types/project.d.ts +85 -0
  107. package/dist/types/project.d.ts.map +1 -0
  108. package/dist/types/project.js +5 -0
  109. package/dist/types/workflow-standard.d.ts +51 -0
  110. package/dist/types/workflow-standard.d.ts.map +1 -0
  111. package/dist/types/workflow-standard.js +1 -0
  112. package/dist/utils/index.d.ts +4 -0
  113. package/dist/utils/index.d.ts.map +1 -0
  114. package/dist/utils/index.js +3 -0
  115. package/dist/utils/slug.d.ts +3 -0
  116. package/dist/utils/slug.d.ts.map +1 -0
  117. package/dist/utils/slug.js +21 -0
  118. package/dist/utils/yoopta-content.d.ts +13 -0
  119. package/dist/utils/yoopta-content.d.ts.map +1 -0
  120. package/dist/utils/yoopta-content.js +73 -0
  121. package/dist/utils/yoopta-render.d.ts +7 -0
  122. package/dist/utils/yoopta-render.d.ts.map +1 -0
  123. package/dist/utils/yoopta-render.js +155 -0
  124. package/package.json +30 -0
@@ -0,0 +1,239 @@
1
+ import { mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { ValidationError } from "../errors/validation-error.js";
5
+ import { loadProjectContract } from "../fs/content-repository.js";
6
+ function createImportValidationError(message, rule, remediation, metadata) {
7
+ return new ValidationError(message, {
8
+ entity: 'legacy-import',
9
+ rule,
10
+ remediation,
11
+ metadata,
12
+ });
13
+ }
14
+ function normalizeImportSlug(relativePath) {
15
+ const withoutExtension = relativePath.replace(/\.(md|mdx)$/i, '');
16
+ return withoutExtension
17
+ .split(path.sep)
18
+ .join('/')
19
+ .split('/')
20
+ .map((segment) => segment
21
+ .trim()
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9-]/g, '-')
24
+ .replace(/-+/g, '-')
25
+ .replace(/^-|-$/g, ''))
26
+ .filter(Boolean)
27
+ .join('/');
28
+ }
29
+ function inferTitle(slug, frontmatter, body) {
30
+ const title = frontmatter.title;
31
+ if (typeof title === 'string' && title.trim()) {
32
+ return title.trim();
33
+ }
34
+ const headingMatch = body.match(/^#\s+(.+)$/m);
35
+ if (headingMatch?.[1]) {
36
+ return headingMatch[1].trim();
37
+ }
38
+ const leaf = slug.split('/').at(-1) ?? 'untitled';
39
+ return leaf
40
+ .split('-')
41
+ .filter(Boolean)
42
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
43
+ .join(' ');
44
+ }
45
+ function inferDescription(frontmatter) {
46
+ const description = frontmatter.description;
47
+ return typeof description === 'string' && description.trim() ? description.trim() : undefined;
48
+ }
49
+ function inferTags(frontmatter) {
50
+ const tags = frontmatter.tags;
51
+ if (Array.isArray(tags)) {
52
+ const normalized = tags.map((tag) => String(tag).trim()).filter(Boolean);
53
+ return normalized.length ? normalized : undefined;
54
+ }
55
+ if (typeof tags === 'string' && tags.trim()) {
56
+ return tags
57
+ .split(',')
58
+ .map((tag) => tag.trim())
59
+ .filter(Boolean);
60
+ }
61
+ return undefined;
62
+ }
63
+ function parseFrontmatter(rawContent, sourcePath) {
64
+ if (!rawContent.startsWith('---\n') && !rawContent.startsWith('---\r\n')) {
65
+ return { body: rawContent, frontmatter: {} };
66
+ }
67
+ const frontmatterMatch = rawContent.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
68
+ if (!frontmatterMatch) {
69
+ throw createImportValidationError(`Legacy document "${sourcePath}" contains an unclosed frontmatter block.`, 'legacy-frontmatter-closed', 'Close the frontmatter block with a terminating --- line or remove the malformed frontmatter.', { sourcePath });
70
+ }
71
+ const rawFrontmatter = frontmatterMatch[1] ?? '';
72
+ const body = rawContent.slice(frontmatterMatch[0].length);
73
+ const frontmatter = {};
74
+ for (const line of rawFrontmatter.split(/\r?\n/)) {
75
+ const trimmed = line.trim();
76
+ if (!trimmed || trimmed.startsWith('#')) {
77
+ continue;
78
+ }
79
+ const separatorIndex = trimmed.indexOf(':');
80
+ if (separatorIndex <= 0) {
81
+ throw createImportValidationError(`Legacy document "${sourcePath}" contains malformed frontmatter.`, 'legacy-frontmatter-key-value', 'Use simple "key: value" frontmatter lines for supported legacy imports.', { sourcePath, line: trimmed });
82
+ }
83
+ const key = trimmed.slice(0, separatorIndex).trim();
84
+ const value = trimmed.slice(separatorIndex + 1).trim();
85
+ if (!key) {
86
+ throw createImportValidationError(`Legacy document "${sourcePath}" contains malformed frontmatter.`, 'legacy-frontmatter-key-required', 'Provide a non-empty frontmatter key before the colon.', { sourcePath, line: trimmed });
87
+ }
88
+ if (value.startsWith('[') && value.endsWith(']')) {
89
+ frontmatter[key] = value
90
+ .slice(1, -1)
91
+ .split(',')
92
+ .map((entry) => entry.trim().replace(/^['"]|['"]$/g, ''))
93
+ .filter(Boolean);
94
+ continue;
95
+ }
96
+ frontmatter[key] = value.replace(/^['"]|['"]$/g, '');
97
+ }
98
+ return { body, frontmatter };
99
+ }
100
+ async function collectLegacyFiles(sourceRoot) {
101
+ const entries = await readdir(sourceRoot, { withFileTypes: true });
102
+ const files = [];
103
+ for (const entry of entries) {
104
+ const absolutePath = path.join(sourceRoot, entry.name);
105
+ if (entry.isDirectory()) {
106
+ const nestedFiles = await collectLegacyFiles(absolutePath);
107
+ files.push(...nestedFiles);
108
+ continue;
109
+ }
110
+ const extension = path.extname(entry.name).toLowerCase();
111
+ if (extension !== '.md' && extension !== '.mdx') {
112
+ continue;
113
+ }
114
+ const rawContent = await readFile(absolutePath, 'utf8');
115
+ const { body, frontmatter } = parseFrontmatter(rawContent, absolutePath);
116
+ files.push({
117
+ absolutePath,
118
+ relativePath: absolutePath,
119
+ format: extension === '.mdx' ? 'mdx' : 'markdown',
120
+ rawContent,
121
+ body,
122
+ frontmatter,
123
+ });
124
+ }
125
+ return files;
126
+ }
127
+ function toRelativeFiles(sourceRoot, files) {
128
+ return files.map((file) => ({
129
+ ...file,
130
+ relativePath: path.relative(sourceRoot, file.absolutePath),
131
+ }));
132
+ }
133
+ function createItemId(relativePath, index) {
134
+ const base = relativePath
135
+ .replace(/\.(md|mdx)$/i, '')
136
+ .split(path.sep)
137
+ .join('-')
138
+ .toLowerCase()
139
+ .replace(/[^a-z0-9-]/g, '-')
140
+ .replace(/-+/g, '-')
141
+ .replace(/^-|-$/g, '');
142
+ return base || `legacy-item-${index + 1}`;
143
+ }
144
+ function buildImportItems(files, lang, importedAt) {
145
+ const seenSlugs = new Set();
146
+ return files.map((file, index) => {
147
+ const slug = normalizeImportSlug(file.relativePath);
148
+ if (!slug) {
149
+ throw createImportValidationError(`Legacy document "${file.relativePath}" could not be normalized into a slug.`, 'legacy-slug-normalized', 'Rename the source file or move it into a directory structure that produces a non-empty slug.', { sourcePath: file.relativePath });
150
+ }
151
+ if (seenSlugs.has(slug)) {
152
+ throw createImportValidationError(`Legacy import contains duplicate slug "${slug}".`, 'legacy-import-slug-unique', 'Rename or move the conflicting source files so each imported document has a unique slug.', { slug, sourcePath: file.relativePath });
153
+ }
154
+ seenSlugs.add(slug);
155
+ return {
156
+ id: createItemId(file.relativePath, index),
157
+ sourcePath: file.relativePath.split(path.sep).join('/'),
158
+ lang,
159
+ slug,
160
+ title: inferTitle(slug, file.frontmatter, file.body),
161
+ description: inferDescription(file.frontmatter),
162
+ tags: inferTags(file.frontmatter),
163
+ format: file.format,
164
+ importedAt,
165
+ rawContent: file.rawContent,
166
+ body: file.body,
167
+ frontmatter: file.frontmatter,
168
+ status: 'staged',
169
+ };
170
+ });
171
+ }
172
+ async function writeJson(filePath, value) {
173
+ await mkdir(path.dirname(filePath), { recursive: true });
174
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
175
+ }
176
+ export async function importLegacyDocumentation(options) {
177
+ const contractResult = await loadProjectContract(options.repoRoot, options.projectId);
178
+ if (!contractResult.ok) {
179
+ throw contractResult.error;
180
+ }
181
+ const contract = contractResult.value;
182
+ const lang = options.lang ?? contract.config.defaultLanguage;
183
+ const sourceStats = await stat(options.sourceRoot).catch(() => null);
184
+ if (!sourceStats?.isDirectory()) {
185
+ throw createImportValidationError(`Legacy source root "${options.sourceRoot}" does not exist or is not a directory.`, 'legacy-source-root-directory', 'Provide a directory containing .md or .mdx files for import.', { sourceRoot: options.sourceRoot });
186
+ }
187
+ const collectedFiles = toRelativeFiles(options.sourceRoot, await collectLegacyFiles(options.sourceRoot));
188
+ if (collectedFiles.length === 0) {
189
+ throw createImportValidationError(`Legacy source root "${options.sourceRoot}" does not contain any supported files.`, 'legacy-source-supported-files-required', 'Add at least one .md or .mdx file before invoking the import workflow.', { sourceRoot: options.sourceRoot });
190
+ }
191
+ const importedAt = new Date().toISOString();
192
+ const items = buildImportItems(collectedFiles, lang, importedAt);
193
+ const importId = `legacy-${importedAt.replace(/[:.]/g, '-').toLowerCase()}`;
194
+ const importRoot = path.join(contract.paths.importsRoot, importId);
195
+ const manifestFile = path.join(importRoot, 'manifest.json');
196
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'anydocs-legacy-import-'));
197
+ try {
198
+ const stagedImportRoot = path.join(tempRoot, importId);
199
+ const stagedItemsRoot = path.join(stagedImportRoot, 'items');
200
+ const manifest = {
201
+ version: 1,
202
+ importId,
203
+ projectId: contract.config.projectId,
204
+ sourceRoot: options.sourceRoot,
205
+ importedAt,
206
+ itemCount: items.length,
207
+ status: 'staged',
208
+ items: items.map((item) => ({
209
+ id: item.id,
210
+ sourcePath: item.sourcePath,
211
+ lang: item.lang,
212
+ slug: item.slug,
213
+ title: item.title,
214
+ format: item.format,
215
+ status: item.status,
216
+ })),
217
+ };
218
+ await writeJson(path.join(stagedImportRoot, 'manifest.json'), manifest);
219
+ for (const item of items) {
220
+ await writeJson(path.join(stagedItemsRoot, `${item.id}.json`), item);
221
+ }
222
+ await mkdir(contract.paths.importsRoot, { recursive: true });
223
+ await rename(stagedImportRoot, importRoot);
224
+ return {
225
+ importId,
226
+ importRoot,
227
+ manifestFile,
228
+ itemCount: items.length,
229
+ items: manifest.items,
230
+ };
231
+ }
232
+ catch (error) {
233
+ await rm(importRoot, { recursive: true, force: true }).catch(() => undefined);
234
+ throw error;
235
+ }
236
+ finally {
237
+ await rm(tempRoot, { recursive: true, force: true }).catch(() => undefined);
238
+ }
239
+ }
@@ -0,0 +1,81 @@
1
+ import type { DocsLang, PageStatus, PageReview } from '../types/docs.ts';
2
+ import type { AuthoringPageResult, UpdatePagePatch } from './authoring-service.ts';
3
+ export declare const PAGE_TEMPLATE_KINDS: readonly ["concept", "how_to", "reference"];
4
+ export type PageTemplateKind = (typeof PAGE_TEMPLATE_KINDS)[number];
5
+ export type PageTemplateCalloutTheme = 'info' | 'warning' | 'success';
6
+ export type PageTemplateCalloutInput = {
7
+ title?: string;
8
+ body: string;
9
+ theme?: PageTemplateCalloutTheme;
10
+ };
11
+ export type PageTemplateSectionInput = {
12
+ title: string;
13
+ body?: string;
14
+ items?: string[];
15
+ code?: string;
16
+ language?: string;
17
+ callout?: PageTemplateCalloutInput;
18
+ };
19
+ export type PageTemplateStepInput = {
20
+ title: string;
21
+ body?: string;
22
+ code?: string;
23
+ language?: string;
24
+ };
25
+ export type CreatePageFromTemplateInput = {
26
+ projectRoot: string;
27
+ lang: DocsLang;
28
+ page: {
29
+ id: string;
30
+ slug: string;
31
+ title: string;
32
+ description?: string;
33
+ tags?: string[];
34
+ status?: PageStatus;
35
+ review?: PageReview;
36
+ };
37
+ template: PageTemplateKind;
38
+ summary?: string;
39
+ sections?: PageTemplateSectionInput[];
40
+ steps?: PageTemplateStepInput[];
41
+ callouts?: PageTemplateCalloutInput[];
42
+ };
43
+ export type UpdatePageFromTemplateInput = {
44
+ projectRoot: string;
45
+ lang: DocsLang;
46
+ pageId: string;
47
+ patch?: Pick<UpdatePagePatch<Record<string, unknown>>, 'slug' | 'title' | 'description' | 'tags' | 'review'>;
48
+ template: PageTemplateKind;
49
+ summary?: string;
50
+ sections?: PageTemplateSectionInput[];
51
+ steps?: PageTemplateStepInput[];
52
+ callouts?: PageTemplateCalloutInput[];
53
+ };
54
+ export declare const PAGE_TEMPLATE_DEFINITIONS: readonly [{
55
+ readonly id: "concept";
56
+ readonly label: "Concept";
57
+ readonly description: "Explain an idea, architecture, or mental model with sectioned prose and supporting callouts.";
58
+ readonly recommendedInputs: readonly ["summary", "sections"];
59
+ }, {
60
+ readonly id: "how_to";
61
+ readonly label: "How-To";
62
+ readonly description: "Teach a procedure with ordered steps, optional code examples, and final guidance.";
63
+ readonly recommendedInputs: readonly ["summary", "steps"];
64
+ }, {
65
+ readonly id: "reference";
66
+ readonly label: "Reference";
67
+ readonly description: "Capture stable facts, options, APIs, and operational details in scan-friendly sections.";
68
+ readonly recommendedInputs: readonly ["summary", "sections"];
69
+ }];
70
+ type PageTemplateComposition = {
71
+ content: Record<string, unknown>;
72
+ render: {
73
+ markdown: string;
74
+ plainText: string;
75
+ };
76
+ };
77
+ export declare function composePageFromTemplate(input: Omit<CreatePageFromTemplateInput, 'projectRoot' | 'lang' | 'page'>): PageTemplateComposition;
78
+ export declare function createPageFromTemplate(input: CreatePageFromTemplateInput): Promise<AuthoringPageResult<Record<string, unknown>>>;
79
+ export declare function updatePageFromTemplate(input: UpdatePageFromTemplateInput): Promise<AuthoringPageResult<Record<string, unknown>>>;
80
+ export {};
81
+ //# sourceMappingURL=page-template-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-template-service.d.ts","sourceRoot":"","sources":["../../src/services/page-template-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,342 @@
1
+ import { ValidationError } from "../errors/validation-error.js";
2
+ import { createPage, updatePage } from "./authoring-service.js";
3
+ export const PAGE_TEMPLATE_KINDS = ['concept', 'how_to', 'reference'];
4
+ export const PAGE_TEMPLATE_DEFINITIONS = [
5
+ {
6
+ id: 'concept',
7
+ label: 'Concept',
8
+ description: 'Explain an idea, architecture, or mental model with sectioned prose and supporting callouts.',
9
+ recommendedInputs: ['summary', 'sections'],
10
+ },
11
+ {
12
+ id: 'how_to',
13
+ label: 'How-To',
14
+ description: 'Teach a procedure with ordered steps, optional code examples, and final guidance.',
15
+ recommendedInputs: ['summary', 'steps'],
16
+ },
17
+ {
18
+ id: 'reference',
19
+ label: 'Reference',
20
+ description: 'Capture stable facts, options, APIs, and operational details in scan-friendly sections.',
21
+ recommendedInputs: ['summary', 'sections'],
22
+ },
23
+ ];
24
+ function normalizeText(value, key) {
25
+ if (value == null) {
26
+ return undefined;
27
+ }
28
+ if (typeof value !== 'string') {
29
+ throw new ValidationError(`Template field "${key}" must be a string.`, {
30
+ entity: 'page-template',
31
+ rule: 'page-template-string-field',
32
+ remediation: `Provide "${key}" as a string or omit it.`,
33
+ metadata: { key, received: value },
34
+ });
35
+ }
36
+ const trimmed = value.trim();
37
+ return trimmed.length > 0 ? trimmed : undefined;
38
+ }
39
+ function normalizeStringList(values, key) {
40
+ if (values == null) {
41
+ return [];
42
+ }
43
+ if (!Array.isArray(values) || values.some((value) => typeof value !== 'string')) {
44
+ throw new ValidationError(`Template field "${key}" must be a string array.`, {
45
+ entity: 'page-template',
46
+ rule: 'page-template-string-array-field',
47
+ remediation: `Provide "${key}" as an array of non-empty strings.`,
48
+ metadata: { key, received: values },
49
+ });
50
+ }
51
+ return values.map((value) => value.trim()).filter(Boolean);
52
+ }
53
+ function normalizeCallout(value, key) {
54
+ const body = normalizeText(value.body, `${key}.body`);
55
+ if (!body) {
56
+ throw new ValidationError(`Template callout "${key}" must include body text.`, {
57
+ entity: 'page-template',
58
+ rule: 'page-template-callout-body-required',
59
+ remediation: 'Provide a non-empty callout body.',
60
+ metadata: { key },
61
+ });
62
+ }
63
+ return {
64
+ body,
65
+ ...(normalizeText(value.title, `${key}.title`) ? { title: normalizeText(value.title, `${key}.title`) } : {}),
66
+ ...(value.theme ? { theme: value.theme } : {}),
67
+ };
68
+ }
69
+ function normalizeSections(sections) {
70
+ if (sections == null) {
71
+ return [];
72
+ }
73
+ if (!Array.isArray(sections)) {
74
+ throw new ValidationError('Template field "sections" must be an array.', {
75
+ entity: 'page-template',
76
+ rule: 'page-template-sections-array',
77
+ remediation: 'Provide "sections" as an array of structured section objects.',
78
+ metadata: { received: sections },
79
+ });
80
+ }
81
+ return sections.map((section, index) => {
82
+ const title = normalizeText(section.title, `sections[${index}].title`);
83
+ const body = normalizeText(section.body, `sections[${index}].body`);
84
+ const items = normalizeStringList(section.items, `sections[${index}].items`);
85
+ const code = normalizeText(section.code, `sections[${index}].code`);
86
+ const language = normalizeText(section.language, `sections[${index}].language`);
87
+ const callout = section.callout ? normalizeCallout(section.callout, `sections[${index}].callout`) : undefined;
88
+ if (!title) {
89
+ throw new ValidationError(`Template section ${index + 1} must include a title.`, {
90
+ entity: 'page-template',
91
+ rule: 'page-template-section-title-required',
92
+ remediation: 'Provide a non-empty section title.',
93
+ metadata: { index },
94
+ });
95
+ }
96
+ if (!body && items.length === 0 && !code && !callout) {
97
+ throw new ValidationError(`Template section "${title}" has no content.`, {
98
+ entity: 'page-template',
99
+ rule: 'page-template-section-content-required',
100
+ remediation: 'Provide body, items, code, or a callout for each section.',
101
+ metadata: { index, title },
102
+ });
103
+ }
104
+ return {
105
+ title,
106
+ ...(body ? { body } : {}),
107
+ ...(items.length > 0 ? { items } : {}),
108
+ ...(code ? { code } : {}),
109
+ ...(language ? { language } : {}),
110
+ ...(callout ? { callout } : {}),
111
+ };
112
+ });
113
+ }
114
+ function normalizeSteps(steps) {
115
+ if (steps == null) {
116
+ return [];
117
+ }
118
+ if (!Array.isArray(steps)) {
119
+ throw new ValidationError('Template field "steps" must be an array.', {
120
+ entity: 'page-template',
121
+ rule: 'page-template-steps-array',
122
+ remediation: 'Provide "steps" as an array of structured step objects.',
123
+ metadata: { received: steps },
124
+ });
125
+ }
126
+ return steps.map((step, index) => {
127
+ const title = normalizeText(step.title, `steps[${index}].title`);
128
+ const body = normalizeText(step.body, `steps[${index}].body`);
129
+ const code = normalizeText(step.code, `steps[${index}].code`);
130
+ const language = normalizeText(step.language, `steps[${index}].language`);
131
+ if (!title) {
132
+ throw new ValidationError(`Template step ${index + 1} must include a title.`, {
133
+ entity: 'page-template',
134
+ rule: 'page-template-step-title-required',
135
+ remediation: 'Provide a non-empty step title.',
136
+ metadata: { index },
137
+ });
138
+ }
139
+ if (!body && !code) {
140
+ throw new ValidationError(`Template step "${title}" has no content.`, {
141
+ entity: 'page-template',
142
+ rule: 'page-template-step-content-required',
143
+ remediation: 'Provide step body text or code.',
144
+ metadata: { index, title },
145
+ });
146
+ }
147
+ return {
148
+ title,
149
+ ...(body ? { body } : {}),
150
+ ...(code ? { code } : {}),
151
+ ...(language ? { language } : {}),
152
+ };
153
+ });
154
+ }
155
+ class YooptaTemplateBuilder {
156
+ constructor() {
157
+ this.content = {};
158
+ this.markdownBlocks = [];
159
+ this.plainTextBlocks = [];
160
+ this.blockOrder = 0;
161
+ this.idCounter = 1;
162
+ }
163
+ paragraph(text) {
164
+ const normalized = text.trim();
165
+ if (!normalized)
166
+ return;
167
+ this.pushBlock('Paragraph', 'paragraph', [{ text: normalized }], { nodeType: 'block' });
168
+ this.markdownBlocks.push(normalized);
169
+ this.plainTextBlocks.push(normalized);
170
+ }
171
+ heading(level, text) {
172
+ const normalized = text.trim();
173
+ if (!normalized)
174
+ return;
175
+ const type = level === 2 ? 'HeadingTwo' : 'HeadingThree';
176
+ const elementType = level === 2 ? 'heading-two' : 'heading-three';
177
+ this.pushBlock(type, elementType, [{ text: normalized }], { nodeType: 'block' });
178
+ this.markdownBlocks.push(`${'#'.repeat(level)} ${normalized}`);
179
+ this.plainTextBlocks.push(normalized);
180
+ }
181
+ bulletedList(items) {
182
+ for (const item of items.map((value) => value.trim()).filter(Boolean)) {
183
+ this.pushBlock('BulletedList', 'bulleted-list', [{ text: item }], { nodeType: 'block' });
184
+ this.markdownBlocks.push(`- ${item}`);
185
+ this.plainTextBlocks.push(item);
186
+ }
187
+ }
188
+ numberedList(items) {
189
+ items
190
+ .map((value) => value.trim())
191
+ .filter(Boolean)
192
+ .forEach((item, index) => {
193
+ this.pushBlock('NumberedList', 'numbered-list', [{ text: item }], { nodeType: 'block' });
194
+ this.markdownBlocks.push(`${index + 1}. ${item}`);
195
+ this.plainTextBlocks.push(item);
196
+ });
197
+ }
198
+ code(code, language) {
199
+ const normalized = code.trim();
200
+ if (!normalized)
201
+ return;
202
+ this.pushBlock('Code', 'code', [{ text: normalized }], {
203
+ nodeType: 'void',
204
+ ...(language ? { language } : {}),
205
+ });
206
+ this.markdownBlocks.push(`\`\`\`${language ?? ''}\n${normalized}\n\`\`\``);
207
+ this.plainTextBlocks.push(normalized);
208
+ }
209
+ callout(callout) {
210
+ const parts = [callout.title?.trim(), callout.body.trim()].filter(Boolean);
211
+ const text = parts.join(': ');
212
+ if (!text)
213
+ return;
214
+ this.pushBlock('Callout', 'callout', [{ text }], {
215
+ nodeType: 'block',
216
+ theme: callout.theme ?? 'info',
217
+ });
218
+ this.markdownBlocks.push(`> ${text}`);
219
+ this.plainTextBlocks.push(text);
220
+ }
221
+ build() {
222
+ return {
223
+ content: this.content,
224
+ render: {
225
+ markdown: this.markdownBlocks.join('\n\n'),
226
+ plainText: this.plainTextBlocks.join('\n\n'),
227
+ },
228
+ };
229
+ }
230
+ nextId(prefix) {
231
+ const value = `${prefix}-${this.idCounter}`;
232
+ this.idCounter += 1;
233
+ return value;
234
+ }
235
+ pushBlock(type, elementType, children, props) {
236
+ const blockId = this.nextId('block');
237
+ const elementId = this.nextId('element');
238
+ this.content[blockId] = {
239
+ id: blockId,
240
+ type,
241
+ value: [
242
+ {
243
+ id: elementId,
244
+ type: elementType,
245
+ children,
246
+ props,
247
+ },
248
+ ],
249
+ meta: { order: this.blockOrder, depth: 0 },
250
+ };
251
+ this.blockOrder += 1;
252
+ }
253
+ }
254
+ export function composePageFromTemplate(input) {
255
+ const summary = normalizeText(input.summary, 'summary');
256
+ const sections = normalizeSections(input.sections);
257
+ const steps = normalizeSteps(input.steps);
258
+ const callouts = (input.callouts ?? []).map((callout, index) => normalizeCallout(callout, `callouts[${index}]`));
259
+ if (input.template === 'how_to' && steps.length === 0) {
260
+ throw new ValidationError('Template "how_to" requires at least one step.', {
261
+ entity: 'page-template',
262
+ rule: 'page-template-how-to-steps-required',
263
+ remediation: 'Provide at least one structured step when using the "how_to" template.',
264
+ metadata: { template: input.template },
265
+ });
266
+ }
267
+ if (!summary && sections.length === 0 && steps.length === 0 && callouts.length === 0) {
268
+ throw new ValidationError(`Template "${input.template}" has no content to compose.`, {
269
+ entity: 'page-template',
270
+ rule: 'page-template-content-required',
271
+ remediation: 'Provide summary, sections, steps, or callouts when creating a templated page.',
272
+ metadata: { template: input.template },
273
+ });
274
+ }
275
+ const builder = new YooptaTemplateBuilder();
276
+ if (summary) {
277
+ builder.paragraph(summary);
278
+ }
279
+ if (input.template === 'how_to' && steps.length > 0) {
280
+ builder.heading(2, 'Steps');
281
+ builder.numberedList(steps.map((step) => step.title));
282
+ for (const step of steps) {
283
+ builder.heading(3, step.title);
284
+ if (step.body)
285
+ builder.paragraph(step.body);
286
+ if (step.code)
287
+ builder.code(step.code, step.language);
288
+ }
289
+ }
290
+ for (const section of sections) {
291
+ builder.heading(2, section.title);
292
+ if (section.body)
293
+ builder.paragraph(section.body);
294
+ if (section.items)
295
+ builder.bulletedList(section.items);
296
+ if (section.code)
297
+ builder.code(section.code, section.language);
298
+ if (section.callout)
299
+ builder.callout(section.callout);
300
+ }
301
+ for (const callout of callouts) {
302
+ builder.callout(callout);
303
+ }
304
+ return builder.build();
305
+ }
306
+ export async function createPageFromTemplate(input) {
307
+ const composition = composePageFromTemplate({
308
+ template: input.template,
309
+ summary: input.summary,
310
+ sections: input.sections,
311
+ steps: input.steps,
312
+ callouts: input.callouts,
313
+ });
314
+ return createPage({
315
+ projectRoot: input.projectRoot,
316
+ lang: input.lang,
317
+ page: {
318
+ ...input.page,
319
+ content: composition.content,
320
+ render: composition.render,
321
+ },
322
+ });
323
+ }
324
+ export async function updatePageFromTemplate(input) {
325
+ const composition = composePageFromTemplate({
326
+ template: input.template,
327
+ summary: input.summary,
328
+ sections: input.sections,
329
+ steps: input.steps,
330
+ callouts: input.callouts,
331
+ });
332
+ return updatePage({
333
+ projectRoot: input.projectRoot,
334
+ lang: input.lang,
335
+ pageId: input.pageId,
336
+ patch: {
337
+ ...(input.patch ?? {}),
338
+ content: composition.content,
339
+ render: composition.render,
340
+ },
341
+ });
342
+ }