@beyondwork/docx-react-component 1.0.10 → 1.0.12

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.
@@ -7,6 +7,7 @@ import type {
7
7
  EditorWarning,
8
8
  } from "../core/state/editor-state.ts";
9
9
  import type {
10
+ BlockNode,
10
11
  DocumentRootNode,
11
12
  InlineNode,
12
13
  ParagraphNode,
@@ -221,6 +222,9 @@ function collectPreservationFeatures(
221
222
  document: CanonicalDocumentEnvelope,
222
223
  ): CompatibilityFeatureEntry[] {
223
224
  const entries: CompatibilityFeatureEntry[] = [];
225
+ const structuredSubPartPaths = collectStructuredSubPartPaths(document);
226
+
227
+ entries.push(...collectSubPartFeatures(document));
224
228
 
225
229
  for (const fragment of listOpaqueFragments(document.preservation as never)) {
226
230
  const descriptor = describeOpaqueFragment(fragment);
@@ -242,6 +246,10 @@ function collectPreservationFeatures(
242
246
  }
243
247
 
244
248
  for (const packagePart of listPreservedPackageParts(document.preservation as never)) {
249
+ if (structuredSubPartPaths.has(packagePart.packagePartName)) {
250
+ continue;
251
+ }
252
+
245
253
  entries.push({
246
254
  featureEntryId: `feature:package:${packagePart.packagePartName}`,
247
255
  featureKey: "unknown-package-parts",
@@ -258,6 +266,206 @@ function collectPreservationFeatures(
258
266
  return entries;
259
267
  }
260
268
 
269
+ function collectStructuredSubPartPaths(
270
+ document: CanonicalDocumentEnvelope,
271
+ ): ReadonlySet<string> {
272
+ const subParts = document.subParts;
273
+ if (!subParts) {
274
+ return new Set<string>();
275
+ }
276
+
277
+ const paths = new Set<string>();
278
+
279
+ for (const header of subParts.headers ?? []) {
280
+ paths.add(header.partPath);
281
+ }
282
+
283
+ for (const footer of subParts.footers ?? []) {
284
+ paths.add(footer.partPath);
285
+ }
286
+
287
+ if (subParts.footnoteCollection) {
288
+ paths.add("/word/footnotes.xml");
289
+ paths.add("/word/endnotes.xml");
290
+ }
291
+
292
+ if (subParts.theme) {
293
+ paths.add("/word/theme/theme1.xml");
294
+ }
295
+
296
+ return paths;
297
+ }
298
+
299
+ function collectSubPartFeatures(
300
+ document: CanonicalDocumentEnvelope,
301
+ ): CompatibilityFeatureEntry[] {
302
+ const subParts = document.subParts;
303
+ if (!subParts) {
304
+ return [];
305
+ }
306
+
307
+ const entries: CompatibilityFeatureEntry[] = collectLossySubPartFeatures(subParts);
308
+ const hasHeaderFooterContent = (subParts.headers?.length ?? 0) > 0 || (subParts.footers?.length ?? 0) > 0;
309
+ const noteCount =
310
+ Object.keys(subParts.footnoteCollection?.footnotes ?? {}).length +
311
+ Object.keys(subParts.footnoteCollection?.endnotes ?? {}).length;
312
+
313
+ if (hasHeaderFooterContent) {
314
+ entries.push({
315
+ featureEntryId: "feature:subparts:headers-footers",
316
+ featureKey: "headers-footers",
317
+ featureClass: "preserve-only",
318
+ message: "Headers and footers are preserved through structured sub-part ownership.",
319
+ details: {
320
+ headerCount: subParts.headers?.length ?? 0,
321
+ footerCount: subParts.footers?.length ?? 0,
322
+ },
323
+ });
324
+ }
325
+
326
+ if (noteCount > 0) {
327
+ entries.push({
328
+ featureEntryId: "feature:subparts:notes",
329
+ featureKey: "notes",
330
+ featureClass: "preserve-only",
331
+ message: "Footnotes and endnotes are preserved through structured sub-part ownership.",
332
+ details: {
333
+ footnoteCount: Object.keys(subParts.footnoteCollection?.footnotes ?? {}).length,
334
+ endnoteCount: Object.keys(subParts.footnoteCollection?.endnotes ?? {}).length,
335
+ },
336
+ });
337
+ }
338
+
339
+ if (subParts.theme) {
340
+ entries.push({
341
+ featureEntryId: "feature:subparts:theme",
342
+ featureKey: "unknown-package-parts",
343
+ featureClass: "preserve-only",
344
+ message: "Theme metadata is preserved through structured sub-part ownership.",
345
+ details: {
346
+ surface: "theme-subpart",
347
+ themeName: subParts.theme.name,
348
+ },
349
+ });
350
+ }
351
+
352
+ return entries;
353
+ }
354
+
355
+ function collectLossySubPartFeatures(
356
+ subParts: NonNullable<CanonicalDocumentEnvelope["subParts"]>,
357
+ ): CompatibilityFeatureEntry[] {
358
+ const entries: CompatibilityFeatureEntry[] = [];
359
+
360
+ const headerFooterLossy = [
361
+ ...(subParts.headers ?? []).flatMap((header) =>
362
+ collectLossyBlocks(header.blocks, `header:${header.partPath}`),
363
+ ),
364
+ ...(subParts.footers ?? []).flatMap((footer) =>
365
+ collectLossyBlocks(footer.blocks, `footer:${footer.partPath}`),
366
+ ),
367
+ ];
368
+ if (headerFooterLossy.length > 0) {
369
+ entries.push({
370
+ featureEntryId: "feature:subparts:headers-footers-lossy",
371
+ featureKey: "headers-footers-lossy",
372
+ featureClass: "unsupported-fatal",
373
+ message: "Headers or footers contain content the current sub-part serializer cannot re-emit safely.",
374
+ details: {
375
+ issues: headerFooterLossy,
376
+ },
377
+ });
378
+ }
379
+
380
+ const noteLossy = [
381
+ ...Object.values(subParts.footnoteCollection?.footnotes ?? {}).flatMap((note) =>
382
+ collectLossyBlocks(note.blocks, `footnote:${note.noteId}`),
383
+ ),
384
+ ...Object.values(subParts.footnoteCollection?.endnotes ?? {}).flatMap((note) =>
385
+ collectLossyBlocks(note.blocks, `endnote:${note.noteId}`),
386
+ ),
387
+ ];
388
+ if (noteLossy.length > 0) {
389
+ entries.push({
390
+ featureEntryId: "feature:subparts:notes-lossy",
391
+ featureKey: "notes-lossy",
392
+ featureClass: "unsupported-fatal",
393
+ message: "Footnotes or endnotes contain content the current sub-part serializer cannot re-emit safely.",
394
+ details: {
395
+ issues: noteLossy,
396
+ },
397
+ });
398
+ }
399
+
400
+ return entries;
401
+ }
402
+
403
+ function collectLossyBlocks(
404
+ blocks: readonly BlockNode[],
405
+ surface: string,
406
+ ): string[] {
407
+ const issues: string[] = [];
408
+
409
+ for (const block of blocks) {
410
+ if (block.type !== "paragraph") {
411
+ issues.push(`${surface}:${block.type}`);
412
+ continue;
413
+ }
414
+
415
+ if (
416
+ block.numbering !== undefined ||
417
+ block.spacing !== undefined ||
418
+ block.indentation !== undefined ||
419
+ block.tabStops !== undefined ||
420
+ block.keepNext !== undefined ||
421
+ block.keepLines !== undefined ||
422
+ block.outlineLevel !== undefined ||
423
+ block.pageBreakBefore !== undefined ||
424
+ block.widowControl !== undefined ||
425
+ block.borders !== undefined ||
426
+ block.shading !== undefined ||
427
+ block.bidi !== undefined ||
428
+ block.suppressLineNumbers !== undefined ||
429
+ block.cnfStyle !== undefined
430
+ ) {
431
+ issues.push(`${surface}:paragraph-properties`);
432
+ }
433
+
434
+ for (const child of block.children) {
435
+ issues.push(...collectLossyInlineContent(child, surface));
436
+ }
437
+ }
438
+
439
+ return [...new Set(issues)];
440
+ }
441
+
442
+ function collectLossyInlineContent(
443
+ node: InlineNode,
444
+ surface: string,
445
+ ): string[] {
446
+ switch (node.type) {
447
+ case "text": {
448
+ const unsupportedMarks = (node.marks ?? [])
449
+ .filter(
450
+ (mark) =>
451
+ mark.type !== "bold" &&
452
+ mark.type !== "italic" &&
453
+ mark.type !== "underline" &&
454
+ mark.type !== "strikethrough" &&
455
+ mark.type !== "doubleStrikethrough",
456
+ )
457
+ .map((mark) => `${surface}:mark:${mark.type}`);
458
+ return unsupportedMarks;
459
+ }
460
+ case "tab":
461
+ case "hard_break":
462
+ case "footnote_ref":
463
+ return [];
464
+ default:
465
+ return [`${surface}:${node.type}`];
466
+ }
467
+ }
468
+
261
469
  function collectDiagnosticWarnings(
262
470
  document: CanonicalDocumentEnvelope,
263
471
  ): EditorWarning[] {