layoutbridge-light 1.0.0 → 1.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 (35) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.de.md +31 -2
  3. package/README.md +31 -3
  4. package/THIRD-PARTY-NOTICES.md +2 -2
  5. package/dist/src/mcp/catalog.js +9 -4
  6. package/dist/src/mcp/content-credentials.js +65 -0
  7. package/dist/src/mcp/dispatch.js +8 -1
  8. package/dist/src/mcp/license/polar-ids.js +1 -1
  9. package/dist/src/mcp/pdf-postprocess.js +798 -0
  10. package/dist/src/mcp/server.js +1 -1
  11. package/dist/src/mcp/target-folder.js +25 -0
  12. package/dist/src/mcp/tools.js +275 -20
  13. package/dist/src/mcp/typography-rules.js +1 -1
  14. package/dist/src/transport/com.js +224 -38
  15. package/dist/src/transport/transport.js +22 -0
  16. package/indesign/commands/25-export.js +258 -11
  17. package/indesign/commands/26-export-image.js +1 -1
  18. package/indesign/commands/31-view.js +100 -0
  19. package/indesign/commands/35-parents.js +44 -24
  20. package/indesign/commands/40-frames.js +35 -0
  21. package/indesign/commands/47-anchored.js +142 -0
  22. package/indesign/commands/50-modify.js +25 -3
  23. package/indesign/commands/60-images.js +28 -2
  24. package/indesign/commands/69-placeholders.js +7 -2
  25. package/indesign/commands/70-text.js +39 -4
  26. package/indesign/commands/75-tables.js +27 -1
  27. package/indesign/commands/78-frame-lines.js +59 -0
  28. package/indesign/commands/79-check-typography.js +143 -4
  29. package/indesign/commands/80-styles.js +105 -10
  30. package/indesign/commands/89-toc-index.js +61 -4
  31. package/indesign/commands/91-epub.js +92 -9
  32. package/indesign/commands/92-accessibility.js +1131 -0
  33. package/indesign/interpreter.js +51 -1
  34. package/package.json +1 -1
  35. package/rules/typography-rules.json +3 -2
@@ -274,6 +274,7 @@ IDC.register('anchor_frame_in_text', { needs_document: true, read_only: false, r
274
274
  if (Number(anchoredItem.id) !== Number(args.frame_id)) {
275
275
  ctx.warn('InDesign gave the anchored object a new id (' + Number(anchoredItem.id) + '); use it from now on.');
276
276
  }
277
+ idcWarnAnchoredOutsideFrame(ctx, doc, anchoredItem);
277
278
  if (result.story_overset === true) {
278
279
  ctx.warn('The story overflows after anchoring (Übersatz) – the object may sit in the overset text.');
279
280
  }
@@ -281,6 +282,27 @@ IDC.register('anchor_frame_in_text', { needs_document: true, read_only: false, r
281
282
  return result;
282
283
  });
283
284
 
285
+ /*
286
+ * A-68 Befund, Sitzung 81: Ein Objekt ueber der Zeile kann oben aus seinem
287
+ * Textrahmen ragen (live 11.11.2026: Rahmen oben 15 mm, Objekt bei 4,4 mm,
288
+ * ohne Warnung). Vergleich der Oberkanten auf derselben Seite.
289
+ */
290
+ function idcWarnAnchoredOutsideFrame(ctx, doc, item) {
291
+ var parent = null, frames = null, frame, ib, fb, li, lf;
292
+ try { parent = item.parent; frames = parent.parentTextFrames; } catch (e0) { return; }
293
+ if (!frames || frames.length === 0) { return; }
294
+ frame = IDC.concrete(frames[0]);
295
+ ib = idcItemBounds(doc, item);
296
+ fb = idcItemBounds(doc, frame);
297
+ if (ib === null || fb === null) { return; }
298
+ li = IDC.landing(item);
299
+ lf = IDC.landing(frame);
300
+ if (li.page_id === null || li.page_id !== lf.page_id) { return; }
301
+ if (ib.y < fb.y - 0.05) {
302
+ ctx.warn('The anchored object reaches ' + IDC.round(fb.y - ib.y, 2) + ' mm above the top of its text frame ' + Number(frame.id) + ' – above the line at the start of a frame the space before and the object height do not fit; it may overlap the content above. Reduce space_before_mm or move the anchor.');
303
+ }
304
+ }
305
+
284
306
  IDC.register('unanchor_frame', { needs_document: true, read_only: false, ruler: 'page' },
285
307
  function (ctx) {
286
308
  var args = ctx.args;
@@ -329,3 +351,123 @@ IDC.register('unanchor_frame', { needs_document: true, read_only: false, ruler:
329
351
  ctx.changes = { modified_frame_ids: [Number(released.id)] };
330
352
  return result;
331
353
  });
354
+
355
+ /* ------------------------------------------------ set_anchored_object_options */
356
+
357
+ /*
358
+ * A-68 (6), Sitzung 81: Ankeroptionen eines schon verankerten Objekts
359
+ * aendern, ohne es zu loesen und neu zu verankern (Probe InDesign 21.6 am
360
+ * 11.11.2026: anchoredPosition laesst sich am verankerten Objekt von inline
361
+ * auf above_line und custom umstellen, das Objekt bleibt im Text;
362
+ * horizontalAlignment und anchorSpaceAbove wirken sofort). Parameter wie
363
+ * anchor_frame_in_text; was fehlt, bleibt.
364
+ */
365
+ var IDC_ANCHOR_OPTION_KEYS = ['position', 'offset_mm', 'anchor_point', 'horizontal_reference', 'vertical_reference', 'horizontal_alignment',
366
+ 'vertical_alignment', 'keep_within_frame', 'lock_position', 'space_before_mm', 'space_after_mm'];
367
+
368
+ IDC.register('set_anchored_object_options', { needs_document: true, read_only: false, ruler: 'page' },
369
+ function (ctx) {
370
+ var args = ctx.args;
371
+ var doc = ctx.doc;
372
+ var found = idcFrameArg(doc, args, 'frame_id');
373
+ var item = IDC.concrete(found.item);
374
+ var info = idcAnchorInfo(item);
375
+ var settings = null, position, positionEnum = null, given = false, i, value, ignored = [], story = null, landing, boundsMm = null, result;
376
+ var spaceBefore, spaceAfter, aboveAlignment = null, set = [];
377
+
378
+ if (info === null) {
379
+ IDC.fail('invalid_state', 'Object ' + args.frame_id + ' is not anchored in text; anchor it with anchor_frame_in_text.', { frame_id: args.frame_id, code: 'not_anchored' });
380
+ }
381
+ for (i = 0; i < IDC_ANCHOR_OPTION_KEYS.length; i++) {
382
+ if (idcGiven(args[IDC_ANCHOR_OPTION_KEYS[i]])) { given = true; }
383
+ }
384
+ if (!given) {
385
+ IDC.fail('invalid_argument', 'Pass at least one option: ' + IDC_ANCHOR_OPTION_KEYS.join(', ') + '.', null);
386
+ }
387
+ position = idcGiven(args.position) ? String(args.position) : info.position;
388
+ if (idcGiven(args.position)) {
389
+ positionEnum = idcEnumFor(IDC_ANCHOR_POSITIONS, idcEnumObject('AnchorPosition'), args.position, 'position');
390
+ if (positionEnum === null) { IDC.fail('invalid_state', 'This runtime has no AnchorPosition enumeration.', null); }
391
+ }
392
+ /* Alles vorher pruefen (Aufzaehlungen, Zahlen), dann schreiben. */
393
+ spaceBefore = idcAnchorSpaceArg(args.space_before_mm, 'space_before_mm');
394
+ spaceAfter = idcAnchorSpaceArg(args.space_after_mm, 'space_after_mm');
395
+ if (idcGiven(args.offset_mm)) { idcOffsetArg(args.offset_mm, 'offset_mm'); }
396
+ if (idcGiven(args.anchor_point)) { idcEnumFor(IDC_ANCHOR_POINTS, idcEnumObject('AnchorPoint'), args.anchor_point, 'anchor_point'); }
397
+ if (idcGiven(args.horizontal_reference)) { idcEnumFor(IDC_H_REFERENCES, idcEnumObject('AnchoredRelativeTo'), args.horizontal_reference, 'horizontal_reference'); }
398
+ if (idcGiven(args.vertical_reference)) { idcEnumFor(IDC_V_REFERENCES, idcEnumObject('VerticallyRelativeTo'), args.vertical_reference, 'vertical_reference'); }
399
+ if (idcGiven(args.horizontal_alignment)) { aboveAlignment = idcEnumFor(IDC_H_ALIGNMENTS, idcEnumObject('HorizontalAlignment'), args.horizontal_alignment, 'horizontal_alignment'); }
400
+ if (idcGiven(args.vertical_alignment)) { idcEnumFor(IDC_V_ALIGNMENTS, idcEnumObject('VerticalAlignment'), args.vertical_alignment, 'vertical_alignment'); }
401
+ if (idcGiven(args.keep_within_frame) && typeof args.keep_within_frame !== 'boolean') { IDC.fail('invalid_argument', 'keep_within_frame must be true or false.', null); }
402
+ if (idcGiven(args.lock_position) && typeof args.lock_position !== 'boolean') { IDC.fail('invalid_argument', 'lock_position must be true or false.', null); }
403
+
404
+ try { settings = item.anchoredObjectSettings; } catch (e0) { settings = null; }
405
+ if (settings === null || settings === undefined) {
406
+ IDC.fail('invalid_state', 'InDesign gives no anchored object settings for object ' + args.frame_id + '.', { frame_id: args.frame_id });
407
+ }
408
+ try {
409
+ if (positionEnum !== null) { settings.anchoredPosition = positionEnum; set.push('position'); }
410
+ if (position === 'custom') {
411
+ if (idcGiven(args.anchor_point)) { settings.anchorPoint = idcEnumFor(IDC_ANCHOR_POINTS, idcEnumObject('AnchorPoint'), args.anchor_point, 'anchor_point'); set.push('anchor_point'); }
412
+ if (idcGiven(args.horizontal_reference)) { settings.horizontalReferencePoint = idcEnumFor(IDC_H_REFERENCES, idcEnumObject('AnchoredRelativeTo'), args.horizontal_reference, 'horizontal_reference'); set.push('horizontal_reference'); }
413
+ if (idcGiven(args.vertical_reference)) { settings.verticalReferencePoint = idcEnumFor(IDC_V_REFERENCES, idcEnumObject('VerticallyRelativeTo'), args.vertical_reference, 'vertical_reference'); set.push('vertical_reference'); }
414
+ if (aboveAlignment !== null) { settings.horizontalAlignment = aboveAlignment; set.push('horizontal_alignment'); }
415
+ if (idcGiven(args.vertical_alignment)) { settings.verticalAlignment = idcEnumFor(IDC_V_ALIGNMENTS, idcEnumObject('VerticalAlignment'), args.vertical_alignment, 'vertical_alignment'); set.push('vertical_alignment'); }
416
+ if (idcGiven(args.offset_mm)) {
417
+ if (idcGiven(args.offset_mm.x)) { settings.anchorXoffset = args.offset_mm.x; }
418
+ if (idcGiven(args.offset_mm.y)) { settings.anchorYoffset = args.offset_mm.y; }
419
+ set.push('offset_mm');
420
+ }
421
+ if (idcGiven(args.keep_within_frame)) { settings.pinPosition = args.keep_within_frame === true; set.push('keep_within_frame'); }
422
+ if (idcGiven(args.lock_position)) { settings.lockPosition = args.lock_position === true; set.push('lock_position'); }
423
+ if (spaceBefore !== null) { ignored.push('space_before_mm'); }
424
+ if (spaceAfter !== null) { ignored.push('space_after_mm'); }
425
+ } else if (position === 'above_line') {
426
+ if (aboveAlignment !== null) { settings.horizontalAlignment = aboveAlignment; set.push('horizontal_alignment'); }
427
+ if (spaceBefore !== null) { settings.anchorSpaceAbove = spaceBefore; set.push('space_before_mm'); }
428
+ if (spaceAfter !== null) { settings.anchorYoffset = spaceAfter; set.push('space_after_mm'); }
429
+ for (i = 0; i < IDC_ANCHOR_OPTION_KEYS.length; i++) {
430
+ value = IDC_ANCHOR_OPTION_KEYS[i];
431
+ if (value !== 'position' && value !== 'horizontal_alignment' && value !== 'space_before_mm' && value !== 'space_after_mm' && idcGiven(args[value])) { ignored.push(value); }
432
+ }
433
+ } else {
434
+ for (i = 0; i < IDC_ANCHOR_OPTION_KEYS.length; i++) {
435
+ value = IDC_ANCHOR_OPTION_KEYS[i];
436
+ if (value !== 'position' && idcGiven(args[value])) { ignored.push(value); }
437
+ }
438
+ }
439
+ } catch (e1) {
440
+ if (e1 && e1.isIdcError) { throw e1; }
441
+ IDC.fail('script_error', 'InDesign could not change the anchored object settings of object ' + args.frame_id + ': ' + String(e1.message ? e1.message : e1),
442
+ { frame_id: args.frame_id, done: set });
443
+ }
444
+ if (ignored.length > 0) {
445
+ ctx.warn(ignored.join(', ') + ' do not apply to position "' + position + '" and were ignored (custom: offset, reference points, alignment, keep_within_frame, lock_position; above_line: horizontal_alignment, space_before_mm, space_after_mm).');
446
+ }
447
+
448
+ /* Wie anchor_frame_in_text: erst umbrechen, dann Seite und Lage lesen (A-67 Punkt 4). */
449
+ try { story = item.parent.parentStory; } catch (e2) { story = null; }
450
+ try { if (story !== null) { story.recompose(); } } catch (e3) { /* Lesen wie bisher */ }
451
+ landing = IDC.landing(item);
452
+ try { boundsMm = idcItemBounds(doc, item); } catch (e4) { boundsMm = null; }
453
+ result = {
454
+ frame_id: Number(item.id),
455
+ type: IDC.itemType(item),
456
+ story_id: info.story_id,
457
+ index: info.index,
458
+ applied: set,
459
+ settings: idcDescribeAnchoredSettings(item),
460
+ previous_position: info.position,
461
+ bounds_mm: boundsMm,
462
+ page_id: landing.page_id,
463
+ page_name: landing.page_name,
464
+ story_overset: null
465
+ };
466
+ try { if (story !== null) { result.story_overset = story.overflows === true; } } catch (e5) { /* null */ }
467
+ idcWarnAnchoredOutsideFrame(ctx, doc, item);
468
+ if (result.story_overset === true) {
469
+ ctx.warn('The story overflows after the change (Übersatz) – the object may sit in the overset text; autoflow_story adds pages.');
470
+ }
471
+ ctx.changes = { modified_frame_ids: [Number(item.id)] };
472
+ return result;
473
+ });
@@ -582,10 +582,21 @@ IDC.register('set_frame_inset', { needs_document: true, read_only: false, ruler:
582
582
  var vj = args.vertical_justification;
583
583
  var hasVj = vj !== undefined && vj !== null;
584
584
  var inset = null, vjEnum = null, back = null, vjBack = null;
585
+ var hasFbo = idcGiven(args.first_baseline_offset), hasMin = idcGiven(args.min_first_baseline_pt), fboEnum = null, baseline = null;
585
586
 
586
587
  /* Sitzung 24 (E-79): vertikale Ausrichtung als optionaler Parameter; dann darf inset_mm fehlen. */
587
- if (!hasInset && !hasVj) {
588
- IDC.fail('invalid_argument', 'Pass inset_mm, vertical_justification or both.', null);
588
+ /* A-68 (2), Sitzung 81: erste Grundlinie und Mindestwert fuer bestehende Rahmen (wie create_text_frame). */
589
+ if (!hasInset && !hasVj && !hasFbo && !hasMin) {
590
+ IDC.fail('invalid_argument', 'Pass inset_mm, vertical_justification, first_baseline_offset or min_first_baseline_pt.', null);
591
+ }
592
+ if (hasFbo) {
593
+ fboEnum = idcFirstBaselineFor(args.first_baseline_offset);
594
+ if (fboEnum === null) {
595
+ IDC.fail('invalid_argument', 'first_baseline_offset must be ascent, cap_height, leading, x_height, fixed or embox_top.', { first_baseline_offset: args.first_baseline_offset });
596
+ }
597
+ }
598
+ if (hasMin && (typeof args.min_first_baseline_pt !== 'number' || !isFinite(args.min_first_baseline_pt) || args.min_first_baseline_pt < 0)) {
599
+ IDC.fail('invalid_argument', 'min_first_baseline_pt must be a number of 0 or more (points).', { min_first_baseline_pt: args.min_first_baseline_pt });
589
600
  }
590
601
  if (hasInset) { inset = idcEdgeObjectArg(args.inset_mm, 'inset_mm', false); }
591
602
  if (hasVj) {
@@ -607,10 +618,21 @@ IDC.register('set_frame_inset', { needs_document: true, read_only: false, ruler:
607
618
  IDC.fail('script_error', 'InDesign could not set the vertical justification (vertikale Ausrichtung) of text frame ' + args.frame_id + ': ' + String(e1.message ? e1.message : e1),
608
619
  { frame_id: args.frame_id });
609
620
  }
621
+ try {
622
+ if (fboEnum !== null) { frame.textFramePreferences.firstBaselineOffset = fboEnum; }
623
+ if (hasMin) { IDC.inPoints(function () { frame.textFramePreferences.minimumFirstBaselineOffset = args.min_first_baseline_pt; }); }
624
+ } catch (e1b) {
625
+ IDC.fail('script_error', 'InDesign could not set the first baseline options of text frame ' + args.frame_id + ': ' + String(e1b.message ? e1b.message : e1b),
626
+ { frame_id: args.frame_id });
627
+ }
610
628
  try { back = idcEdgesFromArray(frame.textFramePreferences.insetSpacing); } catch (e2) { back = null; }
611
629
  try { vjBack = idcVerticalJustificationName(frame.textFramePreferences.verticalJustification); } catch (e3) { vjBack = null; }
630
+ baseline = { first_baseline_offset: null, min_first_baseline_pt: null };
631
+ try { baseline.first_baseline_offset = idcFirstBaselineName(frame.textFramePreferences.firstBaselineOffset); } catch (e4) { /* null */ }
632
+ try { IDC.inPoints(function () { baseline.min_first_baseline_pt = IDC.round(Number(frame.textFramePreferences.minimumFirstBaselineOffset), 3); }); } catch (e5) { /* null */ }
612
633
  ctx.changes = { modified_frame_ids: [Number(frame.id)] };
613
- return { frame_id: Number(frame.id), inset_mm: back, vertical_justification: vjBack };
634
+ return { frame_id: Number(frame.id), inset_mm: back, vertical_justification: vjBack,
635
+ first_baseline_offset: baseline.first_baseline_offset, min_first_baseline_pt: baseline.min_first_baseline_pt };
614
636
  });
615
637
 
616
638
  /* ------------------------------------------------------------ set_text_wrap */
@@ -732,7 +732,7 @@ function idcLinkWhereabouts(link) {
732
732
  function idcDescribeLink(doc, link) {
733
733
  var placement = idcLinkPlacement(doc, link);
734
734
  var entry = { link_id: null, file_name: null, file_path: null, status: null, page_id: placement.page_id, page_name: placement.page_name, frame_id: placement.frame_id };
735
- var where = idcLinkWhereabouts(link).where, key;
735
+ var info = idcLinkWhereabouts(link), where = info.where, key;
736
736
  try { entry.link_id = Number(link.id); } catch (e0) { entry.link_id = null; }
737
737
  try { entry.file_name = String(link.name); } catch (e1) { entry.file_name = null; }
738
738
  try { entry.file_path = String(link.filePath); } catch (e2) { entry.file_path = null; }
@@ -740,9 +740,28 @@ function idcDescribeLink(doc, link) {
740
740
  for (key in where) {
741
741
  if (IDC.hasOwn(where, key)) { entry[key] = where[key]; }
742
742
  }
743
+ /* A-42 (Sitzung 81): Verknuepfung ohne Objekt (Waise) – weder Grafik noch Story erreichbar. */
744
+ entry.without_object = info.held.graphic === null && info.held.story === null;
743
745
  return entry;
744
746
  }
745
747
 
748
+ /*
749
+ * A-42 (Sitzung 81) – Verknuepfungen ohne Objekt, etwa Firefly-Bilder, deren
750
+ * Rahmen geloescht wurde. Probe an der Kopie eines Kundenhefts am
751
+ * 11.11.2026 (InDesign 21.6): das Link-Objekt ist gueltig, status NORMAL,
752
+ * aber parent wirft schon in InDesign, linkXmp ebenso; ein GenAI-Kennzeichen
753
+ * gibt es im Objektmodell nicht (nur Name GenAIImage_... und Ordner
754
+ * InDesign-GenAI-Assets). Ein IDML-Export und Wiederoeffnen entfernt genau
755
+ * diese Verknuepfung (61 -> 60, 44 Seiten, Preflight 0 Fehler, PDF 44 Seiten
756
+ * wie vorher); package_document kopiert die Datei nicht.
757
+ */
758
+ var IDC_ORPHAN_HINT = 'it does not print and package_document does not copy its file. To remove it, export the document as IDML (File > Export > InDesign Markup) and open the IDML – that drops exactly such links and keeps pages and content (checked on a copy of a magazine, 11.11.2026); save the result under a new name and compare before replacing the original.';
759
+
760
+ function idcLooksGenerative(entry) {
761
+ var text = String(entry.file_name === null ? '' : entry.file_name) + '|' + String(entry.file_path === null ? '' : entry.file_path);
762
+ return /GenAIImage_|InDesign-GenAI-Assets/.test(text);
763
+ }
764
+
746
765
  IDC.register('list_links', { needs_document: true, read_only: true, ruler: 'page' },
747
766
  function (ctx) {
748
767
  var args = ctx.args;
@@ -760,6 +779,13 @@ IDC.register('list_links', { needs_document: true, read_only: true, ruler: 'page
760
779
  }
761
780
  out.push(entry);
762
781
  }
782
+ var orphans = 0, generative = 0;
783
+ for (i = 0; i < out.length; i++) {
784
+ if (out[i].without_object === true) { orphans += 1; if (idcLooksGenerative(out[i])) { generative += 1; } }
785
+ }
786
+ if (orphans > 0) {
787
+ ctx.warn(orphans + ' link(s) without an object (without_object true' + (generative > 0 ? '; ' + generative + ' of them a Firefly/GenAI image whose frame was deleted' : '') + '): ' + IDC_ORPHAN_HINT);
788
+ }
763
789
  return { links: out, count: out.length, total: Number(doc.links.length), path_contains: filter };
764
790
  });
765
791
 
@@ -812,7 +838,7 @@ IDC.register('delete_link', { needs_document: true, read_only: false, ruler: 'pa
812
838
  { code: 'story_link', link_id: linkId, location_before: before });
813
839
  }
814
840
  if (held.graphic === null) {
815
- IDC.fail('invalid_state', 'Link ' + linkId + ' is not attached to any object InDesign can reach; remove it in the Links panel of InDesign.',
841
+ IDC.fail('invalid_state', 'Link ' + linkId + ' is not attached to any object InDesign can reach (a link without an object, e.g. a Firefly image whose frame was deleted); ' + IDC_ORPHAN_HINT,
816
842
  { code: 'link_without_object', link_id: linkId, location_before: before });
817
843
  }
818
844
  onPage = where.location === 'page' || where.location === 'parent_page' || (where.location === 'anchored' && where.in_overset === false);
@@ -22,7 +22,9 @@ var IDC_PLACEHOLDERS = [
22
22
  ['tab', '\t'],
23
23
  ['column_break', '\u000E'],
24
24
  ['frame_break', '\u000F'],
25
- ['page_break', '\u0010']
25
+ ['page_break', '\u0010'],
26
+ ['odd_page_break', '\u0011'],
27
+ ['even_page_break', '\u0012']
26
28
  ];
27
29
 
28
30
  function idcPlaceholderChar(name) {
@@ -90,7 +92,7 @@ function idcPlaceholderNames() {
90
92
  }
91
93
 
92
94
  /*
93
- * Umbruchzeichen (Spalte 0x0E, Rahmen 0x0F, Seite 0x10) verwirft InDesign,
95
+ * Umbruchzeichen (Spalte 0x0E, Rahmen 0x0F, Seite 0x10, ungerade 0x11, gerade 0x12) verwirft InDesign,
94
96
  * wenn sie ueber contents geschrieben werden (Live-Fund 05.09.2026: 110
95
97
  * geschrieben, 107 gelesen). Sie werden deshalb aus dem Text genommen und
96
98
  * nach dem Schreiben ueber insertionPoints als SpecialCharacters
@@ -100,6 +102,9 @@ var IDC_BREAK_ENUMS = {};
100
102
  IDC_BREAK_ENUMS['\u000E'] = 'COLUMN_BREAK';
101
103
  IDC_BREAK_ENUMS['\u000F'] = 'FRAME_BREAK';
102
104
  IDC_BREAK_ENUMS['\u0010'] = 'PAGE_BREAK';
105
+ /* A-68 (4), Sitzung 81: Umbruch auf ungerade/gerade Seite – ueber contents ebenso verworfen (Probe 11.11.2026). */
106
+ IDC_BREAK_ENUMS['\u0011'] = 'ODD_PAGE_BREAK';
107
+ IDC_BREAK_ENUMS['\u0012'] = 'EVEN_PAGE_BREAK';
103
108
 
104
109
  /* Text ohne Umbruchzeichen plus Liste {offset, name}; offset = Stelle im bereinigten Text. */
105
110
  function idcSplitBreaks(text) {
@@ -553,6 +553,25 @@ function idcChangeInStory(story, grep) {
553
553
  return grep ? text.changeGrep() : text.changeText();
554
554
  }
555
555
 
556
+ /* Geht die Story ueber diesen Rahmen hinaus (Folgerahmen oder Uebersatz)? */
557
+ function idcStoryGoesOn(frame) {
558
+ var other;
559
+ try { if (frame.overflows === true) { return true; } } catch (e0) { /* weiter */ }
560
+ try { other = frame.nextTextFrame; if (other !== null && other !== undefined && other.isValid === true) { return true; } } catch (e1) { /* weiter */ }
561
+ try { other = frame.previousTextFrame; if (other !== null && other !== undefined && other.isValid === true) { return true; } } catch (e2) { /* weiter */ }
562
+ return false;
563
+ }
564
+
565
+ /* Entfernt die Ersetzung Absatzmarken? Absatzmarke im Muster (GREP \\r oder ~b, literal CR), keine in der Ersetzung. */
566
+ function idcRemovesParagraphMarks(find, replace, mode) {
567
+ var f = String(find), r = String(replace === undefined || replace === null ? '' : replace);
568
+ if (mode === 'grep') {
569
+ if (f.indexOf('\\r') < 0 && f.indexOf('~b') < 0 && f.indexOf('\r') < 0) { return false; }
570
+ return r.indexOf('\\r') < 0 && r.indexOf('~b') < 0 && r.indexOf('\r') < 0 && !/[$][0-9]/.test(r);
571
+ }
572
+ return f.indexOf('\r') >= 0 && r.indexOf('\r') < 0;
573
+ }
574
+
556
575
  IDC.register('find_replace', { needs_document: true, read_only: false, ruler: 'none' },
557
576
  function (ctx) {
558
577
  var args = ctx.args;
@@ -573,13 +592,21 @@ IDC.register('find_replace', { needs_document: true, read_only: false, ruler: 'n
573
592
  if (mode !== 'literal' && mode !== 'grep') {
574
593
  IDC.fail('invalid_argument', 'mode must be literal or grep.', { mode: mode });
575
594
  }
576
- if (scope !== 'document' && scope !== 'frame') {
577
- IDC.fail('invalid_argument', 'scope must be document or frame.', { scope: scope });
595
+ if (scope !== 'document' && scope !== 'frame' && scope !== 'story') {
596
+ IDC.fail('invalid_argument', 'scope must be document, frame or story.', { scope: scope });
578
597
  }
579
598
  if (scope === 'frame') {
580
599
  frame = idcTextFrameArg(doc, args, 'frame_id').item;
581
600
  try { target = frame.texts.item(0); } catch (e0) { target = null; }
582
601
  if (target === null || target === undefined) { target = idcStoryOf(frame); }
602
+ /* A-68 Befund (18), Sitzung 81: scope frame sucht nur im Rahmenanteil, nicht im Uebersatz und nicht in weiteren Rahmen der Kette. */
603
+ if (idcStoryGoesOn(frame)) {
604
+ ctx.warn('scope frame searches only the text of this frame - the story continues in other frames or in overset (\u00dcbersatz), which are not searched; scope story covers the whole story.');
605
+ }
606
+ } else if (scope === 'story') {
607
+ /* A-68 Befund (18): die ganze Story samt Uebersatz und aller Rahmen der Kette. */
608
+ frame = idcTextFrameArg(doc, args, 'frame_id').item;
609
+ target = idcStoryOf(frame);
583
610
  } else {
584
611
  target = doc;
585
612
  }
@@ -634,6 +661,14 @@ IDC.register('find_replace', { needs_document: true, read_only: false, ruler: 'n
634
661
  if (mode === 'literal' && / /.test(args.find)) {
635
662
  ctx.warn('The search text contains a non-breaking space (U+00A0); literal search does not find it reliably – use mode grep with ~S.');
636
663
  }
664
+ /* A-68 Befund (16), Sitzung 81: Umbruchzeichen im Lookbehind trifft InDesigns GREP nicht (live 11.11.2026: (?<=~P) 42 Fundstellen an Absatzanfaengen, keine hinter dem Seitenumbruch; ~PN dagegen richtig). */
665
+ if (mode === 'grep' && /\(\?<[=!][^)]*~[PMRLE]/.test(String(args.find))) {
666
+ ctx.warn('InDesign GREP does not match break characters (~P, ~M, ~R, ~L, ~E) inside a lookbehind (?<=…) – it reports wrong places. Match the break itself and keep it with a group instead, e.g. find (~P)X and replace $1Y.');
667
+ }
668
+ /* A-68 Befund (18): Absatzmarke entfernt - InDesign fuehrt die Absaetze mit dem Format des oberen weiter. */
669
+ if (!dryRun && count > 0 && idcRemovesParagraphMarks(args.find, args.replace, mode)) {
670
+ ctx.warn('The replacement removes paragraph marks: the merged paragraphs keep the paragraph style of the first one - reapply the intended style (apply_paragraph_style_to_range) where needed.');
671
+ }
637
672
  for (i = 0; i < split.skipped.length; i++) { skippedMatches += split.skipped[i].matches; }
638
673
  if (split.skipped.length > 0) {
639
674
  ctx.warn(skippedMatches + ' match(es) in ' + split.skipped.length + ' locked story/stories were ' + (dryRun ? 'left out of the count' : 'not replaced') +
@@ -641,7 +676,7 @@ IDC.register('find_replace', { needs_document: true, read_only: false, ruler: 'n
641
676
  }
642
677
  if (args.include_locked_layers !== true) {
643
678
  /* Ohne include_locked_layers sucht InDesign gesperrte Stories nicht ab: darauf hinweisen. */
644
- if (scope === 'frame') {
679
+ if (scope === 'frame' || scope === 'story') {
645
680
  frameLock = idcStoryLockInfo(idcStoryOf(frame));
646
681
  if (frameLock.write_locked) { lockedInScope.push(frameLock); }
647
682
  } else {
@@ -664,7 +699,7 @@ IDC.register('find_replace', { needs_document: true, read_only: false, ruler: 'n
664
699
  dry_run: dryRun,
665
700
  scope: scope,
666
701
  mode: mode,
667
- frame_id: scope === 'frame' ? Number(frame.id) : null,
702
+ frame_id: scope === 'frame' || scope === 'story' ? Number(frame.id) : null,
668
703
  master_pages_included: args.include_master_pages === true,
669
704
  hidden_layers_included: args.include_hidden_layers === true,
670
705
  locked_layers_included: args.include_locked_layers === true,
@@ -202,9 +202,18 @@ function idcDescribeTable(table, story, index, count) {
202
202
  try { out.story_id = Number(story.id); } catch (e1) { /* null */ }
203
203
  try { out.width_mm = IDC.round(Number(table.width), 3); } catch (e2) { /* null */ }
204
204
  try { out.height_mm = IDC.round(Number(table.height), 3); } catch (e3) { /* null */ }
205
+ /* A-80, Sitzung 82: Kopfspalten gibt es erst ab InDesign 2026 (Beleg 09/15 der Vorabpruefung); sonst null. */
206
+ out.header_columns = idcHeaderColumns(table);
205
207
  return out;
206
208
  }
207
209
 
210
+ function idcHeaderColumns(table) {
211
+ var value;
212
+ try { value = table.numHeaderColumns; } catch (e) { return null; }
213
+ if (value === undefined || value === null || isNaN(Number(value))) { return null; }
214
+ return Number(value);
215
+ }
216
+
208
217
  /* Tabelle ueber frame_id (ein Rahmen der Story) und table_index (Vorgabe 0). */
209
218
  function idcTableArg(doc, args) {
210
219
  var found = idcTextFrameArg(doc, args, 'frame_id');
@@ -563,8 +572,20 @@ IDC.register('set_table_style', { needs_document: true, read_only: false, ruler:
563
572
  }
564
573
  if (args.header_rows !== undefined && args.header_rows !== null) { idcCountArg(args.header_rows, 'header_rows', 0, 20, 0); given += 1; }
565
574
  if (args.footer_rows !== undefined && args.footer_rows !== null) { idcCountArg(args.footer_rows, 'footer_rows', 0, 20, 0); given += 1; }
575
+ if (args.header_columns !== undefined && args.header_columns !== null) {
576
+ idcCountArg(args.header_columns, 'header_columns', 0, 20, 0);
577
+ given += 1;
578
+ if (idcHeaderColumns(table) === null) {
579
+ IDC.fail('invalid_state', 'header_columns needs InDesign 2026 or later (this is ' + String(app.version) + '); older versions have no header columns.',
580
+ { code: 'needs_indesign_2026', app_version: String(app.version) });
581
+ }
582
+ if (args.header_columns >= idcTableCounts(table).columns) {
583
+ IDC.fail('invalid_argument', 'header_columns must leave at least one other column (table has ' + idcTableCounts(table).columns + ' columns).',
584
+ { header_columns: args.header_columns, columns: idcTableCounts(table).columns });
585
+ }
586
+ }
566
587
  if (given === 0) {
567
- IDC.fail('invalid_argument', 'Pass at least one of table_style, cell_style, column_widths_mm, width_mm, header_rows, footer_rows.', null);
588
+ IDC.fail('invalid_argument', 'Pass at least one of table_style, cell_style, column_widths_mm, width_mm, header_rows, footer_rows, header_columns.', null);
568
589
  }
569
590
 
570
591
  if (tableStyle !== null) {
@@ -585,6 +606,11 @@ IDC.register('set_table_style', { needs_document: true, read_only: false, ruler:
585
606
  changed = true;
586
607
  }
587
608
  }
609
+ if (args.header_columns !== undefined && args.header_columns !== null && idcHeaderColumns(table) !== args.header_columns) {
610
+ try { table.numHeaderColumns = args.header_columns; changed = true; } catch (eHc) {
611
+ IDC.fail('script_error', 'InDesign could not set header_columns: ' + String(eHc.message ? eHc.message : eHc), null);
612
+ }
613
+ }
588
614
  if (args.column_widths_mm !== undefined && args.column_widths_mm !== null) {
589
615
  idcApplyColumnWidths(table, args.column_widths_mm, 'column_widths_mm');
590
616
  changed = true;
@@ -288,6 +288,39 @@ function idcMarkSoftHyphens(lines, texts) {
288
288
  }
289
289
  }
290
290
 
291
+ /*
292
+ * A-68 Befund, Sitzung 81: Bei einer Initiale ueber mehrere Zeilen meldet
293
+ * InDesign 21.6 fuer die erste Zeile des Absatzes die Grundlinie der
294
+ * Initiale, also die der letzten Initialzeile (live 11.11.2026: Zeile 0
295
+ * 35,14 mm, Zeile 1 27,38, Zeile 2 35,14 bei dreizeiliger Initiale). Liegt
296
+ * die erste Zeile eines Absatzes nicht ueber der folgenden Zeile desselben
297
+ * Absatzes, wird ihre Grundlinie am ersten Zeichen hinter der Initiale
298
+ * gelesen (nur dann ein Zugriff, das gebuendelte Lesen bleibt).
299
+ */
300
+ function idcFixDropCapBaselines(textObject, entries) {
301
+ var i, a, b, line = null, paragraph, dropLines = 0, dropChars = 0, chars, value;
302
+ for (i = 0; i + 1 < entries.length; i++) {
303
+ a = entries[i];
304
+ b = entries[i + 1];
305
+ if (!a.is_first_line_of_paragraph || a.is_last_line_of_paragraph) { continue; }
306
+ if (a.paragraph_index === null || a.paragraph_index !== b.paragraph_index) { continue; }
307
+ if (a.baseline_mm === null || b.baseline_mm === null || a.baseline_mm < b.baseline_mm) { continue; }
308
+ try {
309
+ line = textObject.lines.item(i);
310
+ paragraph = line.paragraphs.item(0);
311
+ dropLines = Number(paragraph.dropCapLines);
312
+ dropChars = Number(paragraph.dropCapCharacters);
313
+ } catch (e0) { continue; }
314
+ if (!(dropLines >= 2) || !(dropChars >= 1)) { continue; }
315
+ try {
316
+ chars = line.characters;
317
+ if (Number(chars.length) <= dropChars) { continue; }
318
+ value = Number(chars.item(dropChars).baseline);
319
+ } catch (e1) { continue; }
320
+ if (isFinite(value)) { a.baseline_mm = IDC.round(value); }
321
+ }
322
+ }
323
+
291
324
  function idcColumnFigures(lines) {
292
325
  var out = {
293
326
  line_count: lines.length,
@@ -394,6 +427,7 @@ function idcFrameLineData(doc, frame, story, includeText, maxLines, ctx, cache,
394
427
  texts.push({ first: raw[i].charAt(0), last: brokenByBreak ? '' : rawLast });
395
428
  }
396
429
  idcMarkSoftHyphens(entries, texts);
430
+ idcFixDropCapBaselines(columnObjects[c], entries);
397
431
  var figures = idcColumnFigures(entries);
398
432
  figures.column_index = c;
399
433
  figures.bounds_mm = bounds[c] || null;
@@ -423,6 +457,30 @@ function idcFrameLineData(doc, frame, story, includeText, maxLines, ctx, cache,
423
457
  };
424
458
  }
425
459
 
460
+ /*
461
+ * A-68 Befund, Sitzung 81: „Abstand davor“ eines Absatzes, der eine Spalte
462
+ * oder einen Rahmen beginnt, setzt InDesign nicht (live 11.11.2026: 30 pt
463
+ * davor, erste Grundlinie wie ohne). Hinweis je Spalte, ein Zugriff je
464
+ * Spalte, die mit einem Absatzanfang beginnt.
465
+ */
466
+ function idcWarnSpaceBeforeAtColumnTop(ctx, frame, data) {
467
+ var c, column, start, space, story = null;
468
+ try { story = frame.parentStory; } catch (e0) { return; }
469
+ for (c = 0; c < data.columns.length; c++) {
470
+ column = data.columns[c];
471
+ if (!column.lines || column.lines.length === 0 || !column.lines[0].is_first_line_of_paragraph || column.lines[0].is_empty) { continue; }
472
+ start = column.lines[0].start_index;
473
+ if (start === null) { continue; }
474
+ space = null;
475
+ try {
476
+ space = IDC.inPoints(function () { return Number(story.characters.item(start).paragraphs.item(0).spaceBefore); });
477
+ } catch (e1) { space = null; }
478
+ if (space !== null && space > 0.01) {
479
+ ctx.warn('Column ' + c + ' starts with a paragraph that has space before (Abstand davor) of ' + IDC.round(space, 2) + ' pt – InDesign does not apply it at the top of a column or frame; move the frame or use the first baseline offset (set_frame_inset) instead.');
480
+ }
481
+ }
482
+ }
483
+
426
484
  IDC.register('get_frame_lines', { needs_document: true, read_only: true, ruler: 'spread' },
427
485
  function (ctx) {
428
486
  var args = ctx.args;
@@ -437,6 +495,7 @@ IDC.register('get_frame_lines', { needs_document: true, read_only: true, ruler:
437
495
  IDC.fail('invalid_argument', 'max_lines must be a whole number between 1 and 5000.', { max_lines: args.max_lines });
438
496
  }
439
497
  var data = idcFrameLineData(doc, frame, story, includeText, maxLines, ctx);
498
+ idcWarnSpaceBeforeAtColumnTop(ctx, frame, data);
440
499
  return {
441
500
  frame_id: data.frame_id,
442
501
  story_id: data.story_id,