@iwonz/ywtc 3.2.0 → 3.2.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/CHANGELOG.md +17 -0
- package/README.md +7 -5
- package/dist/adapters/confluence.d.ts +6 -0
- package/dist/adapters/confluence.js +16 -0
- package/dist/adapters/confluence.js.map +1 -1
- package/dist/adapters/yandex.d.ts +6 -0
- package/dist/adapters/yandex.js +24 -2
- package/dist/adapters/yandex.js.map +1 -1
- package/dist/consistency.d.ts +11 -0
- package/dist/consistency.js +38 -0
- package/dist/consistency.js.map +1 -0
- package/dist/content-model.d.ts +10 -1
- package/dist/content-model.js +356 -40
- package/dist/content-model.js.map +1 -1
- package/dist/domain.d.ts +24 -1
- package/dist/domain.js.map +1 -1
- package/dist/filesystem-export.js +6 -5
- package/dist/filesystem-export.js.map +1 -1
- package/dist/issues.d.ts +23 -0
- package/dist/issues.js +31 -0
- package/dist/issues.js.map +1 -0
- package/dist/migrator.d.ts +3 -1
- package/dist/migrator.js +316 -94
- package/dist/migrator.js.map +1 -1
- package/dist/report-catalog.d.ts +2 -0
- package/dist/report-catalog.js +4 -2
- package/dist/report-catalog.js.map +1 -1
- package/dist/report.d.ts +14 -2
- package/dist/report.js +35 -7
- package/dist/report.js.map +1 -1
- package/dist/resource-reconciliation.d.ts +21 -0
- package/dist/resource-reconciliation.js +72 -0
- package/dist/resource-reconciliation.js.map +1 -0
- package/dist/state.d.ts +3 -0
- package/dist/state.js +3 -0
- package/dist/state.js.map +1 -1
- package/package.json +1 -1
package/dist/content-model.js
CHANGED
|
@@ -30,11 +30,97 @@ function absoluteResourceUrl(page, resource) {
|
|
|
30
30
|
}
|
|
31
31
|
return page.url;
|
|
32
32
|
}
|
|
33
|
+
function normalizedResourceAlias(value) {
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
if (!trimmed)
|
|
36
|
+
return undefined;
|
|
37
|
+
let normalized = trimmed;
|
|
38
|
+
try {
|
|
39
|
+
normalized = decodeURIComponent(trimmed);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Keep provider text when percent encoding is malformed.
|
|
43
|
+
}
|
|
44
|
+
return normalized.replace(/\\/g, '/').replace(/^\.\//, '').toLocaleLowerCase();
|
|
45
|
+
}
|
|
46
|
+
function resourceAliasCandidates(reference) {
|
|
47
|
+
const aliases = new Set();
|
|
48
|
+
const add = (value) => {
|
|
49
|
+
if (!value)
|
|
50
|
+
return;
|
|
51
|
+
const normalized = normalizedResourceAlias(value);
|
|
52
|
+
if (!normalized)
|
|
53
|
+
return;
|
|
54
|
+
aliases.add(normalized);
|
|
55
|
+
aliases.add(normalized.replace(/^attachment:/, ''));
|
|
56
|
+
aliases.add(normalized.replace(/^\/+/, ''));
|
|
57
|
+
const marker = normalized.match(/(?:^|\/)((?:\.files|_files|attachments)\/.*)$/i)?.[1];
|
|
58
|
+
if (marker)
|
|
59
|
+
aliases.add(marker);
|
|
60
|
+
};
|
|
61
|
+
const withoutFragment = reference.split('#', 1)[0] ?? reference;
|
|
62
|
+
add(withoutFragment);
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(withoutFragment);
|
|
65
|
+
add(url.pathname);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Relative references are already covered above.
|
|
69
|
+
}
|
|
70
|
+
const snapshot = [...aliases];
|
|
71
|
+
for (const alias of snapshot)
|
|
72
|
+
add(alias.split('/').at(-1));
|
|
73
|
+
return [...aliases];
|
|
74
|
+
}
|
|
75
|
+
export function preferredResourceName(resource) {
|
|
76
|
+
return resource.preferredName ?? resource.name;
|
|
77
|
+
}
|
|
78
|
+
export function sourceResourceAliases(page, resource) {
|
|
79
|
+
const aliases = new Set();
|
|
80
|
+
const add = (value) => {
|
|
81
|
+
for (const alias of value ? resourceAliasCandidates(value) : [])
|
|
82
|
+
aliases.add(alias);
|
|
83
|
+
};
|
|
84
|
+
add(resource.name);
|
|
85
|
+
add(resource.preferredName);
|
|
86
|
+
add(resource.downloadUrl);
|
|
87
|
+
for (const alias of resource.referenceAliases ?? [])
|
|
88
|
+
add(alias);
|
|
89
|
+
add(absoluteResourceUrl(page, resource));
|
|
90
|
+
return [...aliases];
|
|
91
|
+
}
|
|
33
92
|
export function sourceResourceBindings(page, resources) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
93
|
+
const bindings = new Map();
|
|
94
|
+
const aliases = new Map();
|
|
95
|
+
for (const resource of resources.filter((item) => item.kind !== 'grid')) {
|
|
96
|
+
for (const alias of sourceResourceAliases(page, resource)) {
|
|
97
|
+
const matched = aliases.get(alias) ?? [];
|
|
98
|
+
if (!matched.some((candidate) => candidate.id === resource.id))
|
|
99
|
+
matched.push(resource);
|
|
100
|
+
aliases.set(alias, matched);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const [alias, matched] of aliases) {
|
|
104
|
+
if (matched.length !== 1)
|
|
105
|
+
continue;
|
|
106
|
+
const resource = matched[0];
|
|
107
|
+
bindings.set(alias, { kind: 'source', resource, url: absoluteResourceUrl(page, resource) });
|
|
108
|
+
}
|
|
109
|
+
return bindings;
|
|
110
|
+
}
|
|
111
|
+
export function setSourceResourceBinding(bindings, page, resources, resource, binding) {
|
|
112
|
+
const aliasOwners = new Map();
|
|
113
|
+
for (const candidate of resources.filter((item) => item.kind !== 'grid')) {
|
|
114
|
+
for (const alias of sourceResourceAliases(page, candidate)) {
|
|
115
|
+
const owners = aliasOwners.get(alias) ?? new Set();
|
|
116
|
+
owners.add(candidate.id);
|
|
117
|
+
aliasOwners.set(alias, owners);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const alias of sourceResourceAliases(page, resource)) {
|
|
121
|
+
if (aliasOwners.get(alias)?.size === 1)
|
|
122
|
+
bindings.set(alias, binding);
|
|
123
|
+
}
|
|
38
124
|
}
|
|
39
125
|
function bindingTarget(binding) {
|
|
40
126
|
return binding.kind === 'uploaded' ? binding.resource.url ?? binding.resource.name
|
|
@@ -42,13 +128,12 @@ function bindingTarget(binding) {
|
|
|
42
128
|
: binding.path;
|
|
43
129
|
}
|
|
44
130
|
function resourceBinding(reference, context) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return basename ? context.resources.get(basename) : undefined;
|
|
131
|
+
for (const alias of resourceAliasCandidates(reference)) {
|
|
132
|
+
const binding = context.resources.get(alias);
|
|
133
|
+
if (binding)
|
|
134
|
+
return binding;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
52
137
|
}
|
|
53
138
|
function uniqueReferences(references) {
|
|
54
139
|
const seen = new Set();
|
|
@@ -172,7 +257,69 @@ function storageToSemanticHtml(storage, warnings) {
|
|
|
172
257
|
});
|
|
173
258
|
return $('main[data-ywtc-root]').html() ?? '';
|
|
174
259
|
}
|
|
175
|
-
function
|
|
260
|
+
export function stripAnsi(value) {
|
|
261
|
+
let output = '';
|
|
262
|
+
for (let index = 0; index < value.length;) {
|
|
263
|
+
const code = value.charCodeAt(index);
|
|
264
|
+
const controlSequence = code === 0x9b || (code === 0x1b && value[index + 1] === '[');
|
|
265
|
+
if (controlSequence) {
|
|
266
|
+
index += code === 0x9b ? 1 : 2;
|
|
267
|
+
while (index < value.length) {
|
|
268
|
+
const current = value.charCodeAt(index++);
|
|
269
|
+
if (current >= 0x40 && current <= 0x7e)
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (code === 0x1b && value[index + 1] === ']') {
|
|
275
|
+
index += 2;
|
|
276
|
+
while (index < value.length) {
|
|
277
|
+
if (value.charCodeAt(index) === 0x07) {
|
|
278
|
+
index += 1;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
if (value.charCodeAt(index) === 0x1b && value[index + 1] === '\\') {
|
|
282
|
+
index += 2;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
index += 1;
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (code === 0x1b) {
|
|
290
|
+
index += Math.min(2, value.length - index);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
output += value[index];
|
|
294
|
+
index += 1;
|
|
295
|
+
}
|
|
296
|
+
return output;
|
|
297
|
+
}
|
|
298
|
+
function extractTreeDirectives(content) {
|
|
299
|
+
const directives = [];
|
|
300
|
+
const next = content.replace(/{%\s*tree\b([^%]*?)%}/gi, (raw, attributes) => {
|
|
301
|
+
const values = new Map();
|
|
302
|
+
for (const match of attributes.matchAll(/([\w-]+)\s*=\s*["']([^"']*)["']/g)) {
|
|
303
|
+
if (match[1] && match[2] !== undefined)
|
|
304
|
+
values.set(match[1].toLocaleLowerCase(), match[2].toLocaleLowerCase());
|
|
305
|
+
}
|
|
306
|
+
const depthValue = Number(values.get('depth'));
|
|
307
|
+
const sort = values.get('sort');
|
|
308
|
+
const sortBy = values.get('sort_by');
|
|
309
|
+
directives.push({
|
|
310
|
+
raw,
|
|
311
|
+
...(Number.isInteger(depthValue) && depthValue > 0 ? { depth: depthValue } : {}),
|
|
312
|
+
...(sort === 'asc' || sort === 'desc' ? { sort } : {}),
|
|
313
|
+
...(sortBy === 'title' ? { sortBy } : {}),
|
|
314
|
+
});
|
|
315
|
+
return '';
|
|
316
|
+
});
|
|
317
|
+
return { content: next, directives };
|
|
318
|
+
}
|
|
319
|
+
function containsEmptyYfmHeading(content) {
|
|
320
|
+
return /^\s{0,3}#{1,6}\s*$/m.test(content);
|
|
321
|
+
}
|
|
322
|
+
function yfmToHtml(content, warnings, normalizations = []) {
|
|
176
323
|
try {
|
|
177
324
|
const result = transform(content || '', {
|
|
178
325
|
allowHTML: true,
|
|
@@ -181,11 +328,36 @@ function yfmToHtml(content, warnings) {
|
|
|
181
328
|
lang: 'ru',
|
|
182
329
|
linkify: true,
|
|
183
330
|
});
|
|
184
|
-
|
|
185
|
-
|
|
331
|
+
const $ = load(result.result.html || '<p></p>', null, false);
|
|
332
|
+
let removedHeadings = 0;
|
|
333
|
+
$('h1,h2,h3,h4,h5,h6').each((_index, element) => {
|
|
334
|
+
const heading = $(element);
|
|
335
|
+
const meaningful = heading.text().replace(/\s+/g, '').length > 0
|
|
336
|
+
|| heading.find('img,table,pre,code').length > 0;
|
|
337
|
+
if (meaningful)
|
|
338
|
+
return;
|
|
339
|
+
heading.find('[id],a[name]').each((_anchorIndex, anchor) => { heading.before($(anchor).clone()); });
|
|
340
|
+
heading.remove();
|
|
341
|
+
removedHeadings += 1;
|
|
342
|
+
});
|
|
343
|
+
let ignoredHeaderDiagnostics = removedHeadings;
|
|
344
|
+
for (const diagnostic of [...(result.logs.warn ?? []), ...(result.logs.error ?? [])]) {
|
|
345
|
+
const clean = stripAnsi(String(diagnostic)).trim();
|
|
346
|
+
if (/Header without title/i.test(clean)) {
|
|
347
|
+
if (ignoredHeaderDiagnostics > 0)
|
|
348
|
+
ignoredHeaderDiagnostics -= 1;
|
|
349
|
+
if (removedHeadings > 0 || !containsEmptyYfmHeading(content))
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (clean)
|
|
353
|
+
warnings.push(clean);
|
|
354
|
+
}
|
|
355
|
+
if (removedHeadings > 0)
|
|
356
|
+
normalizations.push(`Removed ${removedHeadings} empty heading${removedHeadings === 1 ? '' : 's'} emitted by source conversion`);
|
|
357
|
+
return $.html() || '<p></p>';
|
|
186
358
|
}
|
|
187
359
|
catch (error) {
|
|
188
|
-
warnings.push(`YFM transformation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
360
|
+
warnings.push(`YFM transformation failed: ${stripAnsi(error instanceof Error ? error.message : String(error))}`);
|
|
189
361
|
return `<pre data-ywtc-fallback><code>${escapeHtml(content)}</code></pre>`;
|
|
190
362
|
}
|
|
191
363
|
}
|
|
@@ -195,7 +367,9 @@ function metrics(html) {
|
|
|
195
367
|
}
|
|
196
368
|
export function normalizePageContent(page) {
|
|
197
369
|
const warnings = [];
|
|
198
|
-
const
|
|
370
|
+
const normalizations = [];
|
|
371
|
+
const tree = page.rawFormat === 'yfm' ? extractTreeDirectives(page.rawContent) : { content: page.rawContent, directives: [] };
|
|
372
|
+
const html = page.rawFormat === 'yfm' ? yfmToHtml(tree.content, warnings, normalizations) : storageToSemanticHtml(page.rawContent, warnings);
|
|
199
373
|
const references = extractPageReferences(page.rawContent, page.rawFormat).map((reference, index) => ({
|
|
200
374
|
token: `ref-${index}`,
|
|
201
375
|
kind: reference.kind,
|
|
@@ -205,6 +379,15 @@ export function normalizePageContent(page) {
|
|
|
205
379
|
...(reference.anchor ? { anchor: reference.anchor } : {}),
|
|
206
380
|
}));
|
|
207
381
|
const children = [{ type: 'html', value: html }];
|
|
382
|
+
children.push(...tree.directives.map((directive) => ({
|
|
383
|
+
type: 'tree-directive',
|
|
384
|
+
value: directive.raw,
|
|
385
|
+
tree: {
|
|
386
|
+
...(directive.depth ? { depth: directive.depth } : {}),
|
|
387
|
+
...(directive.sort ? { sort: directive.sort } : {}),
|
|
388
|
+
...(directive.sortBy ? { sortBy: directive.sortBy } : {}),
|
|
389
|
+
},
|
|
390
|
+
})));
|
|
208
391
|
if (warnings.length > 0)
|
|
209
392
|
children.push({ type: 'vendor-extension', vendor: page.provider, value: page.rawContent });
|
|
210
393
|
return {
|
|
@@ -213,6 +396,7 @@ export function normalizePageContent(page) {
|
|
|
213
396
|
rawFormat: page.rawFormat,
|
|
214
397
|
references,
|
|
215
398
|
warnings,
|
|
399
|
+
normalizations,
|
|
216
400
|
degraded: warnings.length > 0,
|
|
217
401
|
metrics: metrics(html),
|
|
218
402
|
};
|
|
@@ -304,6 +488,7 @@ export function rawContentFallback(page, provider, reason) {
|
|
|
304
488
|
return {
|
|
305
489
|
body: `<p><strong>Source content fallback</strong> · <a href="${escapeHtml(page.url)}">original page</a></p>${raw}`,
|
|
306
490
|
warnings: [warning],
|
|
491
|
+
normalizations: [],
|
|
307
492
|
degraded: true,
|
|
308
493
|
representedReferences: page.references.length,
|
|
309
494
|
};
|
|
@@ -314,35 +499,88 @@ export function rawContentFallback(page, provider, reason) {
|
|
|
314
499
|
return {
|
|
315
500
|
body: `**Source content fallback** · [original page](${page.url})\n\n${raw}\n`,
|
|
316
501
|
warnings: [warning],
|
|
502
|
+
normalizations: [],
|
|
317
503
|
degraded: true,
|
|
318
504
|
representedReferences: page.references.length,
|
|
319
505
|
};
|
|
320
506
|
}
|
|
321
|
-
|
|
507
|
+
function semanticText(value) {
|
|
508
|
+
return value.replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim();
|
|
509
|
+
}
|
|
510
|
+
function semanticContent(body, provider) {
|
|
322
511
|
const html = renderedPreviewHtml(body, provider);
|
|
323
512
|
const $ = load(html, null, false);
|
|
324
|
-
$('script,
|
|
325
|
-
|
|
513
|
+
$('script,style').remove();
|
|
514
|
+
$('h1,h2,h3,h4,h5,h6,p,div,span').each((_index, element) => {
|
|
326
515
|
const node = $(element);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const tagName = 'name' in element ? String(element.name) : element.type;
|
|
330
|
-
return `${tagName.toLocaleLowerCase()}[${attributes.join(',')}](${directText})`;
|
|
516
|
+
if (!semanticText(node.text()) && node.find('img,table,pre,code,a[href]').length === 0)
|
|
517
|
+
node.remove();
|
|
331
518
|
});
|
|
332
|
-
const
|
|
333
|
-
|
|
519
|
+
const textValues = (selector) => $(selector).toArray().map((element) => semanticText($(element).text()));
|
|
520
|
+
const macroNames = [];
|
|
521
|
+
if (provider === 'confluence') {
|
|
522
|
+
const storage = load(`<root>${body}</root>`, { xmlMode: true }, false);
|
|
523
|
+
storage('ac\\:structured-macro').each((_index, element) => {
|
|
524
|
+
const macro = storage(element);
|
|
525
|
+
const name = macro.attr('ac:name') ?? 'unknown';
|
|
526
|
+
const parameters = macro.children('ac\\:parameter').toArray().map((parameter) => (`${storage(parameter).attr('ac:name') ?? ''}=${semanticText(storage(parameter).text())}`)).sort();
|
|
527
|
+
macroNames.push(`${name}[${parameters.join(',')}]`);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
text: semanticText($.root().text()),
|
|
532
|
+
headings: $('h1,h2,h3,h4,h5,h6').toArray().map((element) => `${element.tagName}:${semanticText($(element).text())}`),
|
|
533
|
+
listItems: textValues('li'),
|
|
534
|
+
tableCells: $('th,td').toArray().map((element) => `${element.tagName}:${semanticText($(element).text())}`),
|
|
535
|
+
code: textValues('pre,code'),
|
|
536
|
+
links: $('a[href]').toArray().map((element) => `${$(element).attr('href') ?? ''}|${semanticText($(element).text())}`).sort(),
|
|
537
|
+
images: $('img').toArray().map((element) => `${$(element).attr('src') ?? ''}|${$(element).attr('alt') ?? ''}`).sort(),
|
|
538
|
+
anchors: $('[id],a[name]').toArray().map((element) => $(element).attr('id') ?? $(element).attr('name') ?? '').filter(Boolean).sort(),
|
|
539
|
+
structure: {
|
|
540
|
+
lists: $('ul,ol').length,
|
|
541
|
+
rows: $('tr').length,
|
|
542
|
+
details: $('details').length,
|
|
543
|
+
asides: $('aside').length,
|
|
544
|
+
codeBlocks: $('pre').length,
|
|
545
|
+
},
|
|
546
|
+
macros: macroNames.sort(),
|
|
547
|
+
};
|
|
334
548
|
}
|
|
335
|
-
export function
|
|
549
|
+
export function semanticContentFingerprint(body, provider) {
|
|
550
|
+
return sha256(JSON.stringify(semanticContent(body, provider)));
|
|
551
|
+
}
|
|
552
|
+
export function verifyDestinationContent(expectedBody, actualBody, provider, page) {
|
|
336
553
|
if (actualBody === undefined)
|
|
337
|
-
|
|
554
|
+
return { status: 'failed', differences: [`Verification could not read destination content for ${page.title}`] };
|
|
338
555
|
if (actualBody.trim().length === 0)
|
|
339
|
-
|
|
556
|
+
return { status: 'failed', differences: [`Verification found empty destination content for ${page.title}`] };
|
|
340
557
|
if (isLegacyTransferPlaceholder(actualBody, provider, page)) {
|
|
341
|
-
|
|
558
|
+
return { status: 'failed', differences: [`Verification found a transfer placeholder instead of source content for ${page.title}`] };
|
|
342
559
|
}
|
|
343
|
-
|
|
344
|
-
|
|
560
|
+
const expected = semanticContent(expectedBody, provider);
|
|
561
|
+
const actual = semanticContent(actualBody, provider);
|
|
562
|
+
const differs = (key) => JSON.stringify(expected[key]) !== JSON.stringify(actual[key]);
|
|
563
|
+
const missingData = ['text', 'tableCells', 'code', 'links', 'images', 'anchors', 'macros'].filter(differs);
|
|
564
|
+
if (missingData.length > 0) {
|
|
565
|
+
return {
|
|
566
|
+
status: 'failed',
|
|
567
|
+
differences: [`Verification found destination content that differs from the rendered source for ${page.title}: ${missingData.join(', ')}`],
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const simplifiedFormatting = ['headings', 'listItems', 'structure'].filter(differs);
|
|
571
|
+
if (simplifiedFormatting.length > 0) {
|
|
572
|
+
return {
|
|
573
|
+
status: 'partial',
|
|
574
|
+
differences: [`Destination formatting was simplified for ${page.title}: ${simplifiedFormatting.join(', ')}`],
|
|
575
|
+
};
|
|
345
576
|
}
|
|
577
|
+
return { status: 'complete', differences: [] };
|
|
578
|
+
}
|
|
579
|
+
export function assertDestinationContent(expectedBody, actualBody, provider, page) {
|
|
580
|
+
const result = verifyDestinationContent(expectedBody, actualBody, provider, page);
|
|
581
|
+
if (result.status === 'failed')
|
|
582
|
+
throw new Error(result.differences.join('; '));
|
|
583
|
+
return result;
|
|
346
584
|
}
|
|
347
585
|
function relativeTarget(currentPath, targetPath) {
|
|
348
586
|
if (!currentPath)
|
|
@@ -390,15 +628,57 @@ function rewritePortableHtml(html, context) {
|
|
|
390
628
|
});
|
|
391
629
|
return $.html();
|
|
392
630
|
}
|
|
631
|
+
function treeDirectiveNodes(document) {
|
|
632
|
+
return document.root.children?.filter((node) => node.type === 'tree-directive') ?? [];
|
|
633
|
+
}
|
|
634
|
+
function treeChildren(context, parentId, directive, depth = 1) {
|
|
635
|
+
if (directive.depth !== undefined && depth > directive.depth)
|
|
636
|
+
return [];
|
|
637
|
+
return [...context.sourcePages.values()]
|
|
638
|
+
.filter((page) => page.projectedParentId === parentId)
|
|
639
|
+
.sort((left, right) => {
|
|
640
|
+
const order = directive.sortBy === 'title' ? left.title.localeCompare(right.title) : left.canonicalId.localeCompare(right.canonicalId);
|
|
641
|
+
return directive.sort === 'desc' ? -order : order;
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function renderTreeMarkdown(context, directive) {
|
|
645
|
+
const render = (parentId, depth) => treeChildren(context, parentId, directive, depth).map((page) => {
|
|
646
|
+
const destination = context.pages.get(page.canonicalId);
|
|
647
|
+
if (!destination)
|
|
648
|
+
return '';
|
|
649
|
+
const href = 'path' in destination ? relativeTarget(context.currentPath, destination.path) : destination.url;
|
|
650
|
+
const nested = render(page.canonicalId, depth + 1);
|
|
651
|
+
return `${' '.repeat(depth - 1)}- [${page.title}](${href})${nested ? `\n${nested}` : ''}`;
|
|
652
|
+
}).filter(Boolean).join('\n');
|
|
653
|
+
return render(context.currentPageId, 1);
|
|
654
|
+
}
|
|
655
|
+
function renderTreeHtml(context, directive) {
|
|
656
|
+
const render = (parentId, depth) => {
|
|
657
|
+
const items = treeChildren(context, parentId, directive, depth).map((page) => {
|
|
658
|
+
const destination = context.pages.get(page.canonicalId);
|
|
659
|
+
if (!destination)
|
|
660
|
+
return '';
|
|
661
|
+
const href = 'path' in destination ? relativeTarget(context.currentPath, destination.path) : destination.url;
|
|
662
|
+
return `<li><a href="${escapeHtml(href)}">${escapeHtml(page.title)}</a>${render(page.canonicalId, depth + 1)}</li>`;
|
|
663
|
+
}).filter(Boolean).join('');
|
|
664
|
+
return items ? `<ul>${items}</ul>` : '';
|
|
665
|
+
};
|
|
666
|
+
const tree = render(context.currentPageId, 1);
|
|
667
|
+
return tree ? `<nav class="ywtc-tree" aria-label="Child pages">${tree}</nav>` : '';
|
|
668
|
+
}
|
|
393
669
|
export async function renderMarkdown(document, context) {
|
|
394
670
|
const html = rewritePortableHtml(htmlFromDocument(document), context);
|
|
395
|
-
const
|
|
396
|
-
|
|
671
|
+
const tree = treeDirectiveNodes(document).map((node) => renderTreeMarkdown(context, node.tree ?? {})).filter(Boolean).join('\n\n');
|
|
672
|
+
const content = await htmlToMarkdown(html);
|
|
673
|
+
const body = tree ? `${content.trimEnd()}\n\n${tree}\n` : content;
|
|
674
|
+
return { body, warnings: [...document.warnings], normalizations: [...(document.normalizations ?? [])], degraded: document.degraded, representedReferences: document.references.length };
|
|
397
675
|
}
|
|
398
676
|
export async function renderStaticHtml(document, context) {
|
|
399
677
|
const rewritten = rewritePortableHtml(htmlFromDocument(document), context);
|
|
400
|
-
const
|
|
401
|
-
|
|
678
|
+
const content = await sanitizeHtml(rewritten);
|
|
679
|
+
const tree = treeDirectiveNodes(document).map((node) => renderTreeHtml(context, node.tree ?? {})).filter(Boolean).join('');
|
|
680
|
+
const body = `${content}${tree}`;
|
|
681
|
+
return { body, warnings: [...document.warnings], normalizations: [...(document.normalizations ?? [])], degraded: document.degraded, representedReferences: document.references.length };
|
|
402
682
|
}
|
|
403
683
|
export async function renderYfm(document, context) {
|
|
404
684
|
if (document.rawFormat === 'yfm') {
|
|
@@ -440,12 +720,13 @@ export async function renderYfm(document, context) {
|
|
|
440
720
|
return {
|
|
441
721
|
body,
|
|
442
722
|
warnings: [...document.warnings, ...resourceWarnings],
|
|
723
|
+
normalizations: [...(document.normalizations ?? [])],
|
|
443
724
|
degraded: document.degraded || resourceWarnings.size > 0,
|
|
444
725
|
representedReferences: document.references.length,
|
|
445
726
|
};
|
|
446
727
|
}
|
|
447
728
|
const body = await htmlToMarkdown(rewritePortableHtml(htmlFromDocument(document), context));
|
|
448
|
-
return { body, warnings: [...document.warnings, 'Confluence Storage Format was converted to YFM-compatible Markdown'], degraded: document.degraded, representedReferences: document.references.length };
|
|
729
|
+
return { body, warnings: [...document.warnings, 'Confluence Storage Format was converted to YFM-compatible Markdown'], normalizations: [...(document.normalizations ?? [])], degraded: document.degraded, representedReferences: document.references.length };
|
|
449
730
|
}
|
|
450
731
|
export async function renderConfluenceStorage(document, page, context, spaceKey) {
|
|
451
732
|
if (document.rawFormat === 'confluence-storage') {
|
|
@@ -525,6 +806,7 @@ export async function renderConfluenceStorage(document, page, context, spaceKey)
|
|
|
525
806
|
return {
|
|
526
807
|
body: $('main[data-root]').html() ?? '<p></p>',
|
|
527
808
|
warnings,
|
|
809
|
+
normalizations: [...(document.normalizations ?? [])],
|
|
528
810
|
degraded: document.degraded || warnings.length > document.warnings.length,
|
|
529
811
|
representedReferences: document.references.length,
|
|
530
812
|
};
|
|
@@ -549,7 +831,8 @@ export async function renderConfluenceStorage(document, page, context, spaceKey)
|
|
|
549
831
|
else if (binding.kind === 'source' && binding.url)
|
|
550
832
|
resourceFallbacks.set(name, binding.url);
|
|
551
833
|
}
|
|
552
|
-
const
|
|
834
|
+
const tree = extractTreeDirectives(document.raw);
|
|
835
|
+
const converted = convertYfmToStorage(tree.content, {
|
|
553
836
|
currentSlug: page.locator,
|
|
554
837
|
spaceKey,
|
|
555
838
|
wikiWebUrl: new URL(page.url).origin,
|
|
@@ -557,12 +840,45 @@ export async function renderConfluenceStorage(document, page, context, spaceKey)
|
|
|
557
840
|
attachmentsByReference: attachments,
|
|
558
841
|
resourceFallbacksByReference: resourceFallbacks,
|
|
559
842
|
});
|
|
843
|
+
const normalizedStorage = load(`<main data-root>${converted.storage}</main>`, { xmlMode: true }, false);
|
|
844
|
+
let removedHeadings = 0;
|
|
845
|
+
normalizedStorage('h1,h2,h3,h4,h5,h6').each((_index, element) => {
|
|
846
|
+
const heading = normalizedStorage(element);
|
|
847
|
+
if (semanticText(heading.text()) || heading.find('img,table,pre,code').length > 0)
|
|
848
|
+
return;
|
|
849
|
+
heading.find('[id],a[name]').each((_anchorIndex, anchor) => { heading.before(normalizedStorage(anchor).clone()); });
|
|
850
|
+
heading.remove();
|
|
851
|
+
removedHeadings += 1;
|
|
852
|
+
});
|
|
853
|
+
let ignorableHeaderWarnings = removedHeadings;
|
|
854
|
+
const conversionWarnings = converted.warnings.map(stripAnsi).filter((warning) => {
|
|
855
|
+
if (/Header without title/i.test(warning)) {
|
|
856
|
+
if (ignorableHeaderWarnings > 0)
|
|
857
|
+
ignorableHeaderWarnings -= 1;
|
|
858
|
+
if (removedHeadings > 0 || !containsEmptyYfmHeading(tree.content))
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
return Boolean(warning.trim());
|
|
862
|
+
});
|
|
863
|
+
const normalizations = [...(document.normalizations ?? [])];
|
|
864
|
+
if (removedHeadings > 0 && !normalizations.some((item) => /empty heading/i.test(item))) {
|
|
865
|
+
normalizations.push(`Removed ${removedHeadings} empty heading${removedHeadings === 1 ? '' : 's'} emitted by source conversion`);
|
|
866
|
+
}
|
|
867
|
+
const treeStorage = tree.directives.map((directive) => {
|
|
868
|
+
const parameters = [
|
|
869
|
+
directive.depth ? `<ac:parameter ac:name="depth">${directive.depth}</ac:parameter>` : '',
|
|
870
|
+
directive.sortBy ? `<ac:parameter ac:name="sort">${directive.sortBy}</ac:parameter>` : '',
|
|
871
|
+
directive.sort ? `<ac:parameter ac:name="reverse">${directive.sort === 'desc' ? 'true' : 'false'}</ac:parameter>` : '',
|
|
872
|
+
].join('');
|
|
873
|
+
return `<ac:structured-macro ac:name="children">${parameters}</ac:structured-macro>`;
|
|
874
|
+
}).join('');
|
|
560
875
|
const $ = load(htmlFromDocument(document), null, false);
|
|
561
876
|
const gridStorage = $('.ywtc-grid').toArray().map((element) => $(element).html() ?? '').join('');
|
|
562
877
|
return {
|
|
563
|
-
body: `${
|
|
564
|
-
warnings: [...document.warnings, ...
|
|
565
|
-
|
|
878
|
+
body: `${normalizedStorage('main[data-root]').html() ?? '<p></p>'}${treeStorage}${gridStorage}`,
|
|
879
|
+
warnings: [...document.warnings, ...conversionWarnings],
|
|
880
|
+
normalizations,
|
|
881
|
+
degraded: document.degraded || conversionWarnings.length > 0,
|
|
566
882
|
representedReferences: document.references.length,
|
|
567
883
|
};
|
|
568
884
|
}
|