@iwonz/ywtc 3.6.0 → 4.0.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +17 -14
  3. package/dist/adapters/confluence.d.ts +0 -2
  4. package/dist/adapters/confluence.js +61 -27
  5. package/dist/adapters/confluence.js.map +1 -1
  6. package/dist/adapters/yandex.d.ts +7 -9
  7. package/dist/adapters/yandex.js +68 -27
  8. package/dist/adapters/yandex.js.map +1 -1
  9. package/dist/cli.js +2 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/clients/confluence-cloud.d.ts +1 -0
  12. package/dist/clients/confluence-cloud.js +2 -2
  13. package/dist/clients/confluence-cloud.js.map +1 -1
  14. package/dist/clients/confluence.d.ts +1 -0
  15. package/dist/clients/confluence.js +4 -3
  16. package/dist/clients/confluence.js.map +1 -1
  17. package/dist/clients/yandex-wiki.js +2 -6
  18. package/dist/clients/yandex-wiki.js.map +1 -1
  19. package/dist/consistency.d.ts +1 -1
  20. package/dist/consistency.js +6 -1
  21. package/dist/consistency.js.map +1 -1
  22. package/dist/content-model.d.ts +1 -0
  23. package/dist/content-model.js +118 -37
  24. package/dist/content-model.js.map +1 -1
  25. package/dist/conversion.d.ts +2 -0
  26. package/dist/conversion.js +42 -17
  27. package/dist/conversion.js.map +1 -1
  28. package/dist/domain.d.ts +27 -4
  29. package/dist/domain.js +20 -0
  30. package/dist/domain.js.map +1 -1
  31. package/dist/exclusive-lease.d.ts +14 -0
  32. package/dist/exclusive-lease.js +87 -0
  33. package/dist/exclusive-lease.js.map +1 -0
  34. package/dist/filesystem-export.d.ts +2 -0
  35. package/dist/filesystem-export.js +188 -167
  36. package/dist/filesystem-export.js.map +1 -1
  37. package/dist/graph.js +4 -4
  38. package/dist/graph.js.map +1 -1
  39. package/dist/http.js +1 -1
  40. package/dist/http.js.map +1 -1
  41. package/dist/interactive-config.js +55 -3
  42. package/dist/interactive-config.js.map +1 -1
  43. package/dist/issues.d.ts +1 -1
  44. package/dist/migrator.js +604 -366
  45. package/dist/migrator.js.map +1 -1
  46. package/dist/report-catalog.d.ts +4 -0
  47. package/dist/report-catalog.js +4 -0
  48. package/dist/report-catalog.js.map +1 -1
  49. package/dist/report-contract.d.ts +6 -0
  50. package/dist/report-contract.js.map +1 -1
  51. package/dist/report-ui/index.html +6 -6
  52. package/dist/report.js +34 -23
  53. package/dist/report.js.map +1 -1
  54. package/dist/state.d.ts +18 -0
  55. package/dist/state.js +60 -32
  56. package/dist/state.js.map +1 -1
  57. package/dist/types.d.ts +1 -0
  58. package/package.json +1 -1
@@ -17,6 +17,23 @@ const linkifier = new LinkifyIt();
17
17
  function sha256(value) {
18
18
  return createHash('sha256').update(value).digest('hex');
19
19
  }
20
+ function transformationNotices(input) {
21
+ const make = (kind, message) => ({
22
+ code: `${kind}:${sha256(message).slice(0, 12)}`,
23
+ kind,
24
+ message,
25
+ verification: kind === 'normalization'
26
+ ? 'semantic-content-equivalence'
27
+ : kind === 'adaptation'
28
+ ? 'payload-preserved'
29
+ : 'full-representation-unverified',
30
+ });
31
+ return [
32
+ ...(input.normalizations ?? []).map((message) => make('normalization', message)),
33
+ ...(input.adaptations ?? []).map((message) => make('adaptation', message)),
34
+ ...(input.degradations ?? []).map((message) => make('degradation', message)),
35
+ ];
36
+ }
20
37
  function escapeHtml(value) {
21
38
  return String(value ?? '').replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
22
39
  }
@@ -210,7 +227,7 @@ const confluenceLayoutCellCounts = new Map([
210
227
  ['three_equal', 3],
211
228
  ['three_with_sidebars', 3],
212
229
  ]);
213
- function storageToSemanticHtml(storage, warnings) {
230
+ function storageToSemanticHtml(storage, warnings, adaptations = []) {
214
231
  const $ = load(`<main data-ywtc-root>${storage}</main>`, { xmlMode: false }, false);
215
232
  $('script, iframe, object, embed, form').each((_index, element) => {
216
233
  warnings.push(`Active or embedded element <${element.tagName}> was replaced by a safe fallback`);
@@ -219,7 +236,7 @@ function storageToSemanticHtml(storage, warnings) {
219
236
  $('ac\\:layout').toArray().reverse().forEach((element) => {
220
237
  const layout = $(element);
221
238
  if (layout.parents('ac\\:layout').length > 0)
222
- warnings.push('Nested Confluence layout was preserved as a readable layout fallback');
239
+ adaptations.push('Nested Confluence layout was preserved as readable nested sections');
223
240
  const sections = layout.children('ac\\:layout-section').toArray().map((section) => {
224
241
  const item = $(section);
225
242
  const requestedType = item.attr('ac:type') ?? 'unsupported';
@@ -227,7 +244,7 @@ function storageToSemanticHtml(storage, warnings) {
227
244
  const cells = item.children('ac\\:layout-cell').toArray();
228
245
  const sectionType = expectedCells === cells.length ? requestedType : 'unsupported';
229
246
  if (sectionType === 'unsupported') {
230
- warnings.push(`Confluence layout section "${requestedType}" with ${cells.length} cells was preserved as an unsupported-width layout fallback`);
247
+ adaptations.push(`Confluence layout section "${requestedType}" with ${cells.length} cells was preserved with equivalent cell content and adaptive widths`);
231
248
  }
232
249
  const body = cells.map((cell) => `<div class="ywtc-layout-cell">${$(cell).html() ?? ''}</div>`).join('');
233
250
  return `<section class="ywtc-layout-section ywtc-layout-${escapeHtml(sectionType)} ywtc-layout-columns-${cells.length}" data-ywtc-layout-type="${escapeHtml(sectionType)}">${body}</section>`;
@@ -277,7 +294,11 @@ function storageToSemanticHtml(storage, warnings) {
277
294
  const attachment = image.children('ri\\:attachment').attr('ri:filename');
278
295
  const external = image.children('ri\\:url').attr('ri:value');
279
296
  const source = attachment ? `attachment:${attachment}` : external;
280
- image.replaceWith(source ? `<img src="${escapeHtml(source)}" alt="${escapeHtml(image.attr('ac:alt') ?? attachment ?? '')}">` : '<span>[image]</span>');
297
+ const dimensions = [
298
+ image.attr('ac:width') ? ` width="${escapeHtml(image.attr('ac:width'))}"` : '',
299
+ image.attr('ac:height') ? ` height="${escapeHtml(image.attr('ac:height'))}"` : '',
300
+ ].join('');
301
+ image.replaceWith(source ? `<img src="${escapeHtml(source)}" alt="${escapeHtml(image.attr('ac:alt') ?? attachment ?? '')}"${dimensions}>` : '<span>[image]</span>');
281
302
  });
282
303
  $('ac\\:link').each((_index, element) => {
283
304
  const link = $(element);
@@ -422,7 +443,7 @@ function yfmToHtml(content, warnings, normalizations = []) {
422
443
  }
423
444
  function metrics(html) {
424
445
  const $ = load(html, null, false);
425
- return { links: $('a').length, images: $('img').length, tables: $('table').length, codeBlocks: $('pre').length };
446
+ return { links: $('a').length, images: $('img').length, tables: $('table').length, codeBlocks: $('pre').length, preservedExternalReferences: 0 };
426
447
  }
427
448
  function normalizedNodesFromHtml(html, provider) {
428
449
  const $ = load(`<main data-ywtc-nodes>${html}</main>`, null, false);
@@ -469,8 +490,9 @@ function semanticallyEmptyHtml(html) {
469
490
  export function normalizePageContent(page) {
470
491
  const warnings = [];
471
492
  const normalizations = [];
493
+ const adaptations = [];
472
494
  const tree = page.rawFormat === 'yfm' ? extractTreeDirectives(page.rawContent) : { content: page.rawContent, directives: [] };
473
- const html = page.rawFormat === 'yfm' ? yfmToHtml(tree.content, warnings, normalizations) : storageToSemanticHtml(page.rawContent, warnings);
495
+ const html = page.rawFormat === 'yfm' ? yfmToHtml(tree.content, warnings, normalizations) : storageToSemanticHtml(page.rawContent, warnings, adaptations);
474
496
  const references = extractPageReferences(page.rawContent, page.rawFormat).map((reference, index) => ({
475
497
  token: `ref-${index}`,
476
498
  kind: reference.kind,
@@ -498,6 +520,8 @@ export function normalizePageContent(page) {
498
520
  references,
499
521
  warnings,
500
522
  normalizations,
523
+ adaptations,
524
+ notices: transformationNotices({ normalizations, adaptations, degradations: warnings }),
501
525
  degraded: warnings.length > 0,
502
526
  semanticallyEmpty: tree.directives.length === 0 && semanticallyEmptyHtml(html),
503
527
  metrics: metrics(html),
@@ -533,10 +557,11 @@ export function appendGridSnapshots(document, resources) {
533
557
  htmlNode.value = `${htmlNode.value ?? ''}${html}`;
534
558
  else
535
559
  document.root.children?.push({ type: 'html', value: html });
560
+ const adaptations = [...document.adaptations, ...grids.map((resource) => `Dynamic table "${resource.name}" was converted to a static table`)];
536
561
  return {
537
562
  ...document,
538
- warnings: [...document.warnings, ...grids.map((resource) => `Dynamic table "${resource.name}" was converted to a static table`)],
539
- degraded: true,
563
+ adaptations,
564
+ notices: transformationNotices({ normalizations: document.normalizations, adaptations, degradations: document.warnings }),
540
565
  semanticallyEmpty: false,
541
566
  metrics: { ...document.metrics, tables: document.metrics.tables + grids.length },
542
567
  };
@@ -568,7 +593,7 @@ function linearizeLayoutsForMarkdown(html) {
568
593
  return { html: $.html(), layouts };
569
594
  }
570
595
  async function semanticHtmlToYfm(html) {
571
- const warnings = new Set();
596
+ const adaptations = new Set();
572
597
  let tokenSequence = 0;
573
598
  const convert = async (fragment) => {
574
599
  const $ = load(fragment, null, false);
@@ -584,7 +609,7 @@ async function semanticHtmlToYfm(html) {
584
609
  const sectionItem = $(section);
585
610
  const sectionType = sectionItem.attr('data-ywtc-layout-type') ?? 'unsupported';
586
611
  if (['two_left_sidebar', 'two_right_sidebar', 'three_with_sidebars', 'unsupported'].includes(sectionType)) {
587
- warnings.add(`Confluence ${sectionType} layout widths were converted to equal YFM columns`);
612
+ adaptations.add(`Confluence ${sectionType} layout widths were converted to equal YFM columns`);
588
613
  }
589
614
  const blocks = [];
590
615
  for (const cell of sectionItem.children('.ywtc-layout-cell').toArray()) {
@@ -640,7 +665,7 @@ async function semanticHtmlToYfm(html) {
640
665
  markdown = markdown.replaceAll(token, rendered);
641
666
  return markdown.trim();
642
667
  };
643
- return { body: `${await convert(html)}\n`, warnings: [...warnings] };
668
+ return { body: `${await convert(html)}\n`, adaptations: [...adaptations] };
644
669
  }
645
670
  async function sanitizeHtml(html) {
646
671
  const parsed = load(html, null, false);
@@ -704,6 +729,19 @@ function markdownFence(value) {
704
729
  }
705
730
  export function rawContentFallback(page, provider, reason) {
706
731
  const warning = `Preferred rendering failed; complete raw source is shown instead: ${reason}`;
732
+ if (!page.rawContent.trim()) {
733
+ const normalizations = ['Preserved an intentionally empty source page'];
734
+ return {
735
+ body: provider === 'confluence' ? '<p></p>' : '',
736
+ warnings: [],
737
+ normalizations,
738
+ adaptations: [],
739
+ notices: transformationNotices({ normalizations }),
740
+ degraded: false,
741
+ representedReferences: page.references.length,
742
+ preservedExternalReferences: 0,
743
+ };
744
+ }
707
745
  if (provider === 'confluence') {
708
746
  const raw = page.rawContent
709
747
  ? `<pre><code>${escapeHtml(page.rawContent)}</code></pre>`
@@ -712,8 +750,11 @@ export function rawContentFallback(page, provider, reason) {
712
750
  body: `<p><strong>Source content fallback</strong> · <a href="${escapeHtml(page.url)}">original page</a></p>${raw}`,
713
751
  warnings: [warning],
714
752
  normalizations: [],
753
+ adaptations: [],
754
+ notices: transformationNotices({ degradations: [warning] }),
715
755
  degraded: true,
716
756
  representedReferences: page.references.length,
757
+ preservedExternalReferences: 0,
717
758
  };
718
759
  }
719
760
  const raw = page.rawContent
@@ -723,8 +764,11 @@ export function rawContentFallback(page, provider, reason) {
723
764
  body: `**Source content fallback** · [original page](${page.url})\n\n${raw}\n`,
724
765
  warnings: [warning],
725
766
  normalizations: [],
767
+ adaptations: [],
768
+ notices: transformationNotices({ degradations: [warning] }),
726
769
  degraded: true,
727
770
  representedReferences: page.references.length,
771
+ preservedExternalReferences: 0,
728
772
  };
729
773
  }
730
774
  export function emptyRendererFallbackReason(document) {
@@ -790,6 +834,12 @@ function semanticContent(body, provider) {
790
834
  links: $('a[href]').toArray().map((element) => `${$(element).attr('href') ?? ''}|${semanticText($(element).text())}`).sort(),
791
835
  images: $('img').toArray().map((element) => $(element).attr('src') ?? '').sort(),
792
836
  imageLabels: $('img').toArray().map((element) => $(element).attr('alt') ?? '').sort(),
837
+ imageAttributes: $('img').toArray().map((element) => [
838
+ $(element).attr('src') ?? '',
839
+ $(element).attr('alt') ?? '',
840
+ $(element).attr('width') ?? '',
841
+ $(element).attr('height') ?? '',
842
+ ].join('|')).sort(),
793
843
  anchors: $('[id],a[name]').toArray().map((element) => $(element).attr('id') ?? $(element).attr('name') ?? '').filter(Boolean).sort(),
794
844
  structure: {
795
845
  lists: $('ul,ol').length,
@@ -810,6 +860,7 @@ function semanticContentIsEmpty(content) {
810
860
  && content.code.length === 0
811
861
  && content.links.length === 0
812
862
  && content.images.length === 0
863
+ && content.imageAttributes.length === 0
813
864
  && content.anchors.length === 0
814
865
  && content.macros.length === 0
815
866
  && content.layouts.length === 0
@@ -820,20 +871,20 @@ export function semanticContentFingerprint(body, provider) {
820
871
  }
821
872
  export function verifyDestinationContent(expectedBody, actualBody, provider, page) {
822
873
  if (actualBody === undefined)
823
- return { status: 'failed', differences: [`Verification could not read destination content for ${page.title}`] };
874
+ return { status: 'failed', differences: [`Verification could not read destination content for ${page.title}`], adaptations: [] };
824
875
  if (isLegacyTransferPlaceholder(actualBody, provider, page)) {
825
- return { status: 'failed', differences: [`Verification found a transfer placeholder instead of source content for ${page.title}`] };
876
+ return { status: 'failed', differences: [`Verification found a transfer placeholder instead of source content for ${page.title}`], adaptations: [] };
826
877
  }
827
878
  const expected = semanticContent(expectedBody, provider);
828
879
  const actual = semanticContent(actualBody, provider);
829
880
  const expectedEmpty = semanticContentIsEmpty(expected);
830
881
  const actualEmpty = semanticContentIsEmpty(actual);
831
882
  if (expectedEmpty && actualEmpty)
832
- return { status: 'complete', differences: [] };
883
+ return { status: 'complete', differences: [], adaptations: [] };
833
884
  if (!expectedEmpty && actualEmpty)
834
- return { status: 'failed', differences: [`Verification found empty destination content for ${page.title}`] };
885
+ return { status: 'failed', differences: [`Verification found empty destination content for ${page.title}`], adaptations: [] };
835
886
  if (expectedEmpty && !actualEmpty) {
836
- return { status: 'failed', differences: [`Verification found unexpected destination content for semantically empty source page ${page.title}`] };
887
+ return { status: 'failed', differences: [`Verification found unexpected destination content for semantically empty source page ${page.title}`], adaptations: [] };
837
888
  }
838
889
  const differs = (key) => JSON.stringify(expected[key]) !== JSON.stringify(actual[key]);
839
890
  const missingData = ['text', 'tableCells', 'code', 'links', 'images', 'anchors', 'macros'].filter(differs);
@@ -841,16 +892,20 @@ export function verifyDestinationContent(expectedBody, actualBody, provider, pag
841
892
  return {
842
893
  status: 'failed',
843
894
  differences: [`Verification found destination content that differs from the rendered source for ${page.title}: ${missingData.join(', ')}`],
895
+ adaptations: [],
844
896
  };
845
897
  }
846
- const simplifiedFormatting = ['headings', 'listItems', 'imageLabels', 'structure', 'layouts'].filter(differs);
847
- if (simplifiedFormatting.length > 0) {
898
+ const missingStructure = ['headings', 'listItems', 'imageLabels', 'imageAttributes', 'structure'].filter(differs);
899
+ if (missingStructure.length > 0) {
848
900
  return {
849
901
  status: 'partial',
850
- differences: [`Destination formatting was simplified for ${page.title}: ${simplifiedFormatting.join(', ')}`],
902
+ differences: [`Destination lost or could not verify meaningful structure for ${page.title}: ${missingStructure.join(', ')}`],
903
+ adaptations: [],
851
904
  };
852
905
  }
853
- return { status: 'complete', differences: [] };
906
+ if (differs('layouts'))
907
+ return { status: 'complete', differences: [], adaptations: [`Destination adapted visual layout geometry for ${page.title} while preserving its payload`] };
908
+ return { status: 'complete', differences: [], adaptations: [] };
854
909
  }
855
910
  export function assertDestinationContent(expectedBody, actualBody, provider, page) {
856
911
  const result = verifyDestinationContent(expectedBody, actualBody, provider, page);
@@ -904,6 +959,13 @@ function rewritePortableHtml(html, context) {
904
959
  });
905
960
  return $.html();
906
961
  }
962
+ function preservedExternalImageCount(document, context) {
963
+ const $ = load(htmlFromDocument(document), null, false);
964
+ return $('img[src]').toArray().filter((element) => {
965
+ const source = $(element).attr('src') ?? '';
966
+ return /^https?:\/\//i.test(source) && !resourceBinding(source, context);
967
+ }).length;
968
+ }
907
969
  function treeDirectiveNodes(document) {
908
970
  return document.root.children?.filter((node) => node.type === 'tree-directive') ?? [];
909
971
  }
@@ -944,19 +1006,20 @@ function renderTreeHtml(context, directive) {
944
1006
  }
945
1007
  export async function renderMarkdown(document, context) {
946
1008
  if (document.semanticallyEmpty) {
947
- return { body: '', warnings: [...document.warnings], normalizations: [...document.normalizations], degraded: document.degraded, representedReferences: document.references.length };
1009
+ return { body: '', warnings: [...document.warnings], normalizations: [...document.normalizations], adaptations: [...document.adaptations], notices: [...document.notices], degraded: document.degraded, representedReferences: document.references.length, preservedExternalReferences: 0 };
948
1010
  }
949
1011
  const portable = rewritePortableHtml(htmlFromDocument(document), context);
950
1012
  const linearized = linearizeLayoutsForMarkdown(portable);
951
1013
  const tree = treeDirectiveNodes(document).map((node) => renderTreeMarkdown(context, node.tree ?? {})).filter(Boolean).join('\n\n');
952
1014
  const content = await htmlToMarkdown(linearized.html);
953
1015
  const body = tree ? `${content.trimEnd()}\n\n${tree}\n` : content;
954
- const layoutWarnings = linearized.layouts > 0 ? [`${linearized.layouts} column layout${linearized.layouts === 1 ? ' was' : 's were'} linearized for portable Markdown`] : [];
955
- return { body, warnings: [...document.warnings, ...layoutWarnings], normalizations: [...(document.normalizations ?? [])], degraded: document.degraded || layoutWarnings.length > 0, representedReferences: document.references.length };
1016
+ const layoutAdaptations = linearized.layouts > 0 ? [`${linearized.layouts} column layout${linearized.layouts === 1 ? ' was' : 's were'} linearized for portable Markdown`] : [];
1017
+ const adaptations = [...document.adaptations, ...layoutAdaptations];
1018
+ return { body, warnings: [...document.warnings], normalizations: [...document.normalizations], adaptations, notices: transformationNotices({ normalizations: document.normalizations, adaptations, degradations: document.warnings }), degraded: document.degraded, representedReferences: document.references.length, preservedExternalReferences: preservedExternalImageCount(document, context) };
956
1019
  }
957
1020
  export async function renderStaticHtml(document, context) {
958
1021
  if (document.semanticallyEmpty) {
959
- return { body: '', warnings: [...document.warnings], normalizations: [...document.normalizations], degraded: document.degraded, representedReferences: document.references.length };
1022
+ return { body: '', warnings: [...document.warnings], normalizations: [...document.normalizations], adaptations: [...document.adaptations], notices: [...document.notices], degraded: document.degraded, representedReferences: document.references.length, preservedExternalReferences: 0 };
960
1023
  }
961
1024
  const rewritten = rewritePortableHtml(htmlFromDocument(document), context);
962
1025
  const content = await sanitizeHtml(rewritten);
@@ -970,12 +1033,13 @@ export async function renderStaticHtml(document, context) {
970
1033
  visit(child);
971
1034
  };
972
1035
  visit(document.root);
973
- const layoutWarnings = unsupportedLayouts.map((cells) => `Unsupported ${cells}-cell layout was stacked in static HTML without losing cell content`);
974
- return { body, warnings: [...document.warnings, ...layoutWarnings], normalizations: [...(document.normalizations ?? [])], degraded: document.degraded || layoutWarnings.length > 0, representedReferences: document.references.length };
1036
+ const layoutAdaptations = unsupportedLayouts.map((cells) => `Unsupported ${cells}-cell layout was stacked in static HTML without losing cell content`);
1037
+ const adaptations = [...document.adaptations, ...layoutAdaptations];
1038
+ return { body, warnings: [...document.warnings], normalizations: [...document.normalizations], adaptations, notices: transformationNotices({ normalizations: document.normalizations, adaptations, degradations: document.warnings }), degraded: document.degraded, representedReferences: document.references.length, preservedExternalReferences: preservedExternalImageCount(document, context) };
975
1039
  }
976
1040
  export async function renderYfm(document, context) {
977
1041
  if (document.semanticallyEmpty) {
978
- return { body: '', warnings: [...document.warnings], normalizations: [...document.normalizations], degraded: document.degraded, representedReferences: document.references.length };
1042
+ return { body: '', warnings: [...document.warnings], normalizations: [...document.normalizations], adaptations: [...document.adaptations], notices: [...document.notices], degraded: document.degraded, representedReferences: document.references.length, preservedExternalReferences: 0 };
979
1043
  }
980
1044
  if (document.rawFormat === 'yfm') {
981
1045
  let body = document.raw;
@@ -1015,28 +1079,39 @@ export async function renderYfm(document, context) {
1015
1079
  body = `${body.trimEnd()}\n\n${(await htmlToMarkdown(gridHtml)).trimStart()}`;
1016
1080
  return {
1017
1081
  body,
1018
- warnings: [...document.warnings, ...resourceWarnings],
1019
- normalizations: [...(document.normalizations ?? [])],
1020
- degraded: document.degraded || resourceWarnings.size > 0,
1082
+ warnings: [...resourceWarnings],
1083
+ normalizations: [...document.normalizations],
1084
+ adaptations: [...document.adaptations],
1085
+ notices: transformationNotices({ normalizations: document.normalizations, adaptations: document.adaptations, degradations: [...resourceWarnings] }),
1086
+ degraded: resourceWarnings.size > 0,
1021
1087
  representedReferences: document.references.length,
1088
+ preservedExternalReferences: preservedExternalImageCount(document, context),
1022
1089
  };
1023
1090
  }
1024
1091
  const converted = await semanticHtmlToYfm(rewritePortableHtml(htmlFromDocument(document), context));
1025
1092
  return {
1026
1093
  body: converted.body,
1027
- warnings: [...document.warnings, ...converted.warnings],
1028
- normalizations: [...(document.normalizations ?? []), 'Converted supported Confluence Storage Format constructs to native YFM equivalents'],
1029
- degraded: document.degraded || converted.warnings.length > 0,
1094
+ warnings: [...document.warnings],
1095
+ normalizations: [...document.normalizations, 'Converted supported Confluence Storage Format constructs to native YFM equivalents'],
1096
+ adaptations: [...document.adaptations, ...converted.adaptations],
1097
+ notices: transformationNotices({ normalizations: [...document.normalizations, 'Converted supported Confluence Storage Format constructs to native YFM equivalents'], adaptations: [...document.adaptations, ...converted.adaptations], degradations: document.warnings }),
1098
+ degraded: document.degraded,
1030
1099
  representedReferences: document.references.length,
1100
+ preservedExternalReferences: preservedExternalImageCount(document, context),
1031
1101
  };
1032
1102
  }
1033
1103
  export async function renderConfluenceStorage(document, page, context, spaceKey) {
1034
1104
  if (document.semanticallyEmpty) {
1035
- return { body: '<p></p>', warnings: [...document.warnings], normalizations: [...document.normalizations], degraded: document.degraded, representedReferences: document.references.length };
1105
+ return { body: '<p></p>', warnings: [...document.warnings], normalizations: [...document.normalizations], adaptations: [...document.adaptations], notices: [...document.notices], degraded: document.degraded, representedReferences: document.references.length, preservedExternalReferences: 0 };
1036
1106
  }
1037
1107
  if (document.rawFormat === 'confluence-storage') {
1038
1108
  const $ = load(`<main data-root>${document.raw}</main>`, { xmlMode: true }, false);
1039
- const warnings = [...document.warnings];
1109
+ const warnings = [];
1110
+ $('script, iframe, object, embed, form').each((_index, element) => {
1111
+ const raw = $.html(element);
1112
+ warnings.push(`Active or embedded element <${element.tagName}> was preserved as inert source text`);
1113
+ $(element).replaceWith(`<pre><code>${escapeHtml(raw)}</code></pre>`);
1114
+ });
1040
1115
  $('ac\\:image').each((_index, element) => {
1041
1116
  const image = $(element);
1042
1117
  const attachment = image.children('ri\\:attachment');
@@ -1111,9 +1186,12 @@ export async function renderConfluenceStorage(document, page, context, spaceKey)
1111
1186
  return {
1112
1187
  body: $('main[data-root]').html() ?? '<p></p>',
1113
1188
  warnings,
1114
- normalizations: [...(document.normalizations ?? [])],
1115
- degraded: document.degraded || warnings.length > document.warnings.length,
1189
+ normalizations: [...document.normalizations],
1190
+ adaptations: [...document.adaptations],
1191
+ notices: transformationNotices({ normalizations: document.normalizations, adaptations: document.adaptations, degradations: warnings }),
1192
+ degraded: warnings.length > 0,
1116
1193
  representedReferences: document.references.length,
1194
+ preservedExternalReferences: preservedExternalImageCount(document, context),
1117
1195
  };
1118
1196
  }
1119
1197
  const pagesBySlug = new Map();
@@ -1183,8 +1261,11 @@ export async function renderConfluenceStorage(document, page, context, spaceKey)
1183
1261
  body: `${normalizedStorage('main[data-root]').html() ?? '<p></p>'}${treeStorage}${gridStorage}`,
1184
1262
  warnings: [...document.warnings, ...conversionWarnings],
1185
1263
  normalizations,
1264
+ adaptations: [...document.adaptations, ...converted.adaptations],
1265
+ notices: transformationNotices({ normalizations, adaptations: [...document.adaptations, ...converted.adaptations], degradations: [...document.warnings, ...conversionWarnings] }),
1186
1266
  degraded: document.degraded || conversionWarnings.length > 0,
1187
1267
  representedReferences: document.references.length,
1268
+ preservedExternalReferences: converted.stats.preservedExternalReferences,
1188
1269
  };
1189
1270
  }
1190
1271
  export function resolveYandexReferenceSlug(page, href) {