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
@@ -151,7 +151,29 @@ function idcPush(findings, state, finding) {
151
151
  findings.push(finding);
152
152
  }
153
153
 
154
- function idcRunLineRule(rule, data, args, findings, state) {
154
+ /*
155
+ * A-68 Befund, Sitzung 81: Eine einzeilige Dialog- oder Textzeile am
156
+ * Spaltenende ist keine Ueberschrift (live 11.11.2026: »Ja.« im Grundtext
157
+ * wurde gemeldet). Ausgenommen wird eine Zeile, die mit Satzzeichen oder
158
+ * schliessendem Anfuehrungszeichen endet und dasselbe Absatzformat hat wie
159
+ * der folgende Absatz; ein Zwischentitel im Grundtextformat ohne
160
+ * Schlusszeichen bleibt ein Fund. Wenige Zugriffe je Kandidat.
161
+ */
162
+ var IDC_DIALOGUE_END = /[.!?…:;»«“”‘’"')›‹]$/;
163
+
164
+ function idcDialogueLine(story, line) {
165
+ var a, b, lastChar;
166
+ if (story === null || story === undefined || line.start_index === null || line.end_index === null || line.end_index < 2) { return false; }
167
+ try {
168
+ lastChar = String(story.characters.item(line.end_index - 2).contents);
169
+ if (!IDC_DIALOGUE_END.test(lastChar)) { return false; }
170
+ a = story.characters.item(line.start_index).appliedParagraphStyle;
171
+ b = story.characters.item(line.end_index).appliedParagraphStyle;
172
+ return Number(a.id) === Number(b.id);
173
+ } catch (e) { return false; }
174
+ }
175
+
176
+ function idcRunLineRule(rule, data, args, findings, state, story) {
155
177
  var c, column, lines, i, run, runStart, maxHyphens, minChars, first, last, text, prev;
156
178
  maxHyphens = idcGiven(args.max_consecutive_hyphens) ? args.max_consecutive_hyphens : 3;
157
179
  minChars = idcGiven(args.min_last_line_chars) ? args.min_last_line_chars : 0;
@@ -182,7 +204,8 @@ function idcRunLineRule(rule, data, args, findings, state) {
182
204
  if (column.ends_with_first_line_of_paragraph) { idcPush(findings, state, idcLineFinding(rule, data, column, last, null)); }
183
205
  } else if (rule.check === 'heading_at_column_end') {
184
206
  if (last.is_first_line_of_paragraph && last.is_last_line_of_paragraph && !last.is_empty
185
- && last.paragraph_index !== null && last.paragraph_index < data.paragraph_count - 1) {
207
+ && last.paragraph_index !== null && last.paragraph_index < data.paragraph_count - 1
208
+ && !idcDialogueLine(story, last)) {
186
209
  idcPush(findings, state, idcLineFinding(rule, data, column, last, null));
187
210
  }
188
211
  } else if (rule.check === 'empty_line_at_column_start') {
@@ -213,6 +236,113 @@ function idcRunLineRule(rule, data, args, findings, state) {
213
236
  }
214
237
  }
215
238
 
239
+ /*
240
+ * A-78 (Sitzung 81): Regelart language – Textbereiche, deren Sprache von der
241
+ * Sprache ihres Absatzformats abweicht (etwa en-US aus einer Word-Datei in
242
+ * einem deutschen Text: falsche Trennung). Grundlage: Probe InDesign 21.6
243
+ * am 11.11.2026 – story.textStyleRanges trennen bei jedem Sprach- und
244
+ * Formatwechsel, Text in Tabellenzellen gehoert nicht dazu, everyItem()
245
+ * liest appliedLanguage, appliedParagraphStyle und index gebuendelt.
246
+ */
247
+ function idcEveryList(value, count) {
248
+ if (value !== null && value !== undefined && !IDC.isArray(value)) {
249
+ try { if (typeof value.getElements === 'function') { value = value.getElements(); } } catch (e) { return null; }
250
+ }
251
+ return idcBundled(value, count);
252
+ }
253
+
254
+ function idcLanguageName(lang) {
255
+ if (lang === null || lang === undefined) { return null; }
256
+ if (typeof lang === 'string') { return lang; }
257
+ try { return String(lang.name); } catch (e) { return null; }
258
+ }
259
+
260
+ /* Texte, deren Bereiche geprueft werden: je Story (Dokument, Story) oder der Rahmentext. */
261
+ function idcLanguageTargets(doc, scope, frame, story) {
262
+ var out = [], i, stories;
263
+ if (scope === 'frame') {
264
+ try { out.push(frame.texts.item(0)); } catch (e0) { out.push(story); }
265
+ return out;
266
+ }
267
+ if (scope === 'story') { return [story]; }
268
+ try { stories = doc.stories; } catch (e1) { return out; }
269
+ for (i = 0; i < stories.length; i++) { out.push(stories.item(i)); }
270
+ return out;
271
+ }
272
+
273
+ /* Sprache des Absatzformats, je Format einmal gelesen. */
274
+ function idcStyleLanguage(style, cache) {
275
+ var key, name;
276
+ try { key = 's' + Number(style.id); } catch (e0) { key = null; }
277
+ if (key !== null && IDC.hasOwn(cache, key)) { return cache[key]; }
278
+ try { name = idcLanguageName(style.appliedLanguage); } catch (e1) { name = null; }
279
+ if (key !== null) { cache[key] = name; }
280
+ return name;
281
+ }
282
+
283
+ function idcRunLanguageRule(doc, targets, rule, args, findings, state) {
284
+ var t, ranges, count, langs, styles, starts, i, range, lang, styleLang, style, cache = {}, text, start, frames, frame, landing, layer, entry;
285
+ for (t = 0; t < targets.length; t++) {
286
+ try { ranges = targets[t].textStyleRanges; count = Number(ranges.length); } catch (e0) { continue; }
287
+ if (count === 0) { continue; }
288
+ langs = null; styles = null; starts = null;
289
+ try {
290
+ langs = idcEveryList(ranges.everyItem().appliedLanguage, count);
291
+ styles = idcEveryList(ranges.everyItem().appliedParagraphStyle, count);
292
+ starts = idcBundled(ranges.everyItem().index, count);
293
+ } catch (e1) { langs = null; }
294
+ for (i = 0; i < count; i++) {
295
+ range = null;
296
+ try {
297
+ if (langs === null || styles === null || starts === null) {
298
+ range = ranges.item(i);
299
+ lang = idcLanguageName(range.appliedLanguage);
300
+ style = range.appliedParagraphStyle;
301
+ } else {
302
+ lang = idcLanguageName(langs[i]);
303
+ style = styles[i];
304
+ }
305
+ } catch (e2) { continue; }
306
+ styleLang = idcStyleLanguage(style, cache);
307
+ if (lang === null || styleLang === null || lang === styleLang) { continue; }
308
+ if (range === null) { try { range = ranges.item(i); } catch (e3) { continue; } }
309
+ try { text = String(range.contents); } catch (e4) { text = ''; }
310
+ if (/^[\s  -​ ]*$/.test(text)) { continue; }
311
+ if (idcMatchInTable(range)) { state.tables_skipped += 1; continue; }
312
+ frame = null;
313
+ try { frames = range.parentTextFrames; if (frames && frames.length > 0) { frame = IDC.concrete(frames[0]); } } catch (e5) { frame = null; }
314
+ if (frame !== null && args.include_master_pages !== true && IDC.isOnParentSpread(frame)) { continue; }
315
+ if (frame !== null && args.include_locked_layers !== true) {
316
+ try { layer = frame.itemLayer; if (layer !== null && layer !== undefined && layer.locked === true) { continue; } } catch (e6) { /* weiter */ }
317
+ }
318
+ entry = { frame_id: null, page_id: null, page_name: '', story_id: null };
319
+ try { entry.story_id = Number(range.parentStory.id); } catch (e7) { entry.story_id = null; }
320
+ if (frame !== null) {
321
+ entry.frame_id = Number(frame.id);
322
+ try { landing = IDC.landing(frame); entry.page_id = landing.page_id; entry.page_name = landing.page_name; } catch (e8) { /* bleibt null */ }
323
+ }
324
+ if (state.page_ids !== null && (entry.page_id === null || !state.page_ids[entry.page_id])) { continue; }
325
+ if (starts === null) { start = Number(range.index); } else { start = Number(starts[i]); }
326
+ idcPush(findings, state, {
327
+ rule_id: rule.id,
328
+ severity: rule.severity,
329
+ category: rule.category,
330
+ message: rule.message + ' – ' + lang + ' instead of ' + styleLang,
331
+ story_id: entry.story_id,
332
+ frame_id: entry.frame_id,
333
+ page_id: entry.page_id,
334
+ page_name: entry.page_name,
335
+ start_index: start,
336
+ end_index: start + text.length,
337
+ snippet: (text.length > 80 ? text.substring(0, 80) + '…' : text).replace(/[\r\n]/g, '¶'),
338
+ suggestion: rule.suggestion,
339
+ language: lang,
340
+ paragraph_style_language: styleLang
341
+ });
342
+ }
343
+ }
344
+ }
345
+
216
346
  IDC.register('check_typography', { needs_document: true, read_only: true, ruler: 'spread' },
217
347
  function (ctx) {
218
348
  var args = ctx.args;
@@ -223,7 +353,7 @@ IDC.register('check_typography', { needs_document: true, read_only: true, ruler:
223
353
  var maxFindings = idcGiven(args.max_findings) ? args.max_findings : 500;
224
354
  var severityMin = idcGiven(args.severity_min) ? String(args.severity_min) : 'info';
225
355
  var frame = null, story = null, frames, target, i, r, rule, findings = [], state, counts = {}, out, kept = [], data, framesChecked = 0;
226
- var grepRules = [], lineRules = [];
356
+ var grepRules = [], lineRules = [], languageRules = [];
227
357
  var rangeText = null, rangePages, pageIds = null, storyCaches = {}, storyKey, frameStory;
228
358
 
229
359
  if (scope !== 'document' && scope !== 'frame' && scope !== 'story') {
@@ -254,6 +384,7 @@ IDC.register('check_typography', { needs_document: true, read_only: true, ruler:
254
384
  for (r = 0; r < rows.length; r++) {
255
385
  if (rows[r].kind === 'grep') { grepRules.push(rows[r]); }
256
386
  else if (rows[r].kind === 'lines') { lineRules.push(rows[r]); }
387
+ else if (rows[r].kind === 'language') { languageRules.push(rows[r]); }
257
388
  }
258
389
 
259
390
  state = { max: maxFindings, truncated: false, failed: [], tables_skipped: 0, dropped: 0, page_ids: pageIds };
@@ -271,6 +402,14 @@ IDC.register('check_typography', { needs_document: true, read_only: true, ruler:
271
402
  }
272
403
  }
273
404
 
405
+ /* Sprachregel (A-78): je Story die Formatlaeufe mit der Sprache ihres Absatzformats vergleichen. */
406
+ if (languageRules.length > 0) {
407
+ var languageTargets = idcLanguageTargets(doc, scope, frame, story);
408
+ for (r = 0; r < languageRules.length; r++) {
409
+ idcRunLanguageRule(doc, languageTargets, languageRules[r], args, findings, state);
410
+ }
411
+ }
412
+
274
413
  /*
275
414
  * Umbruchregeln: je Rahmen die Zeilen lesen, dann jede Regel darauf.
276
415
  * Absaetze und Rahmenfolge einer Story nur einmal je Story (A-69: je
@@ -297,7 +436,7 @@ IDC.register('check_typography', { needs_document: true, read_only: true, ruler:
297
436
  }
298
437
  framesChecked += 1;
299
438
  for (r = 0; r < lineRules.length; r++) {
300
- idcRunLineRule(lineRules[r], data, args, findings, state);
439
+ idcRunLineRule(lineRules[r], data, args, findings, state, frameStory);
301
440
  }
302
441
  }
303
442
  }
@@ -22,6 +22,19 @@ function idcIsNothing(value) {
22
22
  return false;
23
23
  }
24
24
 
25
+ /*
26
+ * Schnitt oder null. A-68 Befund (15), Sitzung 81: ein Zeichenformat ohne
27
+ * Schnitt las sich live als "NOTHING" (Zeichenkette des Enumerators, an
28
+ * idcIsNothing vorbei) – auch "", "undefined" und "NOTHING" sind kein Schnitt.
29
+ */
30
+ function idcFontStyleOrNull(value) {
31
+ var text;
32
+ if (idcIsNothing(value)) { return null; }
33
+ text = String(value);
34
+ if (text === '' || text === 'undefined' || text === 'NOTHING') { return null; }
35
+ return text;
36
+ }
37
+
25
38
  /* Schrift eines Text- oder Formatobjekts; nicht gesetzte Attribute bleiben null. */
26
39
  function idcReadFontLoose(target) {
27
40
  var raw;
@@ -29,12 +42,12 @@ function idcReadFontLoose(target) {
29
42
  if (idcIsNothing(raw)) {
30
43
  var style = null;
31
44
  try { style = target.fontStyle; } catch (e2) { style = null; }
32
- return { family: null, style: idcIsNothing(style) ? null : String(style) };
45
+ return { family: null, style: idcFontStyleOrNull(style) };
33
46
  }
34
47
  var read = idcReadDefaultFont(target);
35
48
  /* Nicht gesetzte Familie eines Zeichenformats liest sich als "" oder "undefined" (Live-Fund 04.09.2026). */
36
49
  if (read.family === '' || read.family === 'undefined') { read.family = null; }
37
- if (read.style === '' || read.style === 'undefined') { read.style = null; }
50
+ read.style = idcFontStyleOrNull(read.style);
38
51
  return read;
39
52
  }
40
53
 
@@ -299,6 +312,49 @@ function idcIsNoneCharacterStyle(doc, style) {
299
312
  try { return String(style.name).charAt(0) === '['; } catch (e2) { return null; }
300
313
  }
301
314
 
315
+ /*
316
+ * A-68 (1) und (3), Sitzung 81 – Grundlage Probe InDesign 21.6 am 11.11.2026:
317
+ * ParagraphStyle.gridAlignment (GridAlignment NONE, ALIGN_BASELINE, ...) und
318
+ * capitalization an Absatz- und Zeichenformaten (Capitalization NORMAL,
319
+ * SMALL_CAPS, ALL_CAPS, CAP_TO_SMALL_CAP, LOWER_CASE; am neuen Zeichenformat
320
+ * NothingEnum.NOTHING = nicht festgelegt).
321
+ */
322
+ var IDC_CAPITALIZATIONS = [
323
+ ['normal', 'NORMAL'], ['small_caps', 'SMALL_CAPS'], ['all_caps', 'ALL_CAPS'],
324
+ ['cap_to_small_caps', 'CAP_TO_SMALL_CAP'], ['lower_case', 'LOWER_CASE']
325
+ ];
326
+
327
+ function idcCapitalizationFor(name) {
328
+ var i, names = [];
329
+ if (typeof Capitalization === 'undefined') { IDC.fail('invalid_state', 'This runtime has no Capitalization enumeration.', null); }
330
+ for (i = 0; i < IDC_CAPITALIZATIONS.length; i++) {
331
+ if (IDC_CAPITALIZATIONS[i][0] === name) { return Capitalization[IDC_CAPITALIZATIONS[i][1]]; }
332
+ names.push(IDC_CAPITALIZATIONS[i][0]);
333
+ }
334
+ IDC.fail('invalid_argument', 'capitalization must be one of ' + names.join(', ') + '.', { value: name });
335
+ return null;
336
+ }
337
+
338
+ function idcReadCapitalization(style) {
339
+ var value, i;
340
+ try { value = style.capitalization; } catch (e0) { return null; }
341
+ if (value === undefined || value === null || idcIsNothing(value) || typeof Capitalization === 'undefined') { return null; }
342
+ for (i = 0; i < IDC_CAPITALIZATIONS.length; i++) {
343
+ try { if (IDC.enumIs(value, Capitalization[IDC_CAPITALIZATIONS[i][1]])) { return IDC_CAPITALIZATIONS[i][0]; } } catch (e1) { /* weiter */ }
344
+ }
345
+ return String(value);
346
+ }
347
+
348
+ /* true bei Ausrichtung an der Grundlinie, false bei keiner, sonst der Name (Em-Box, ICF – nur ueber InDesign gesetzt). */
349
+ function idcReadGridAlignment(style) {
350
+ var value;
351
+ try { value = style.gridAlignment; } catch (e0) { return null; }
352
+ if (value === undefined || value === null || typeof GridAlignment === 'undefined') { return null; }
353
+ if (IDC.enumIs(value, GridAlignment.ALIGN_BASELINE)) { return true; }
354
+ if (IDC.enumIs(value, GridAlignment.NONE)) { return false; }
355
+ return String(value);
356
+ }
357
+
302
358
  /* Beschreibung eines Absatzformats fuer list_paragraph_styles und debug_inspect_run. */
303
359
  function idcDescribeParagraphStyle(style) {
304
360
  var font = idcReadFontLoose(style);
@@ -330,7 +386,12 @@ function idcDescribeParagraphStyle(style) {
330
386
  /* Sitzung 10, Demo-Heft-Befund 3 (83-indents-tabs.js) */
331
387
  indents: idcReadIndentTabLanguage(style),
332
388
  /* A-67 Punkt 1, Sitzung 72 (83-drop-caps.js) */
333
- drop_cap: idcReadDropCap(style)
389
+ drop_cap: idcReadDropCap(style),
390
+ /* A-68 (1) und (3), Sitzung 81 */
391
+ align_to_baseline_grid: idcReadGridAlignment(style),
392
+ capitalization: idcReadCapitalization(style),
393
+ /* A-79, Sitzung 82 (92-accessibility.js): "" = kein PDF-Tag */
394
+ pdf_tag: idcReadPdfTag(style)
334
395
  };
335
396
  }
336
397
 
@@ -349,7 +410,11 @@ function idcDescribeCharacterStyle(style) {
349
410
  font_style: font.style,
350
411
  point_size: idcReadPt(style, 'pointSize'),
351
412
  fill_swatch: idcReadSwatchName(style, 'fillColor'),
352
- tracking: idcReadNumberLoose(style, 'tracking', 1)
413
+ tracking: idcReadNumberLoose(style, 'tracking', 1),
414
+ /* A-68 (3), Sitzung 81: null, wenn das Zeichenformat die Schreibweise nicht festlegt */
415
+ capitalization: idcReadCapitalization(style),
416
+ /* A-79, Sitzung 82 (92-accessibility.js): "" = kein PDF-Tag */
417
+ pdf_tag: idcReadPdfTag(style)
353
418
  };
354
419
  }
355
420
 
@@ -625,6 +690,23 @@ function idcApplyStyleAttributes(ctx, doc, style, args, created, kind) {
625
690
  style.hyphenation = args.hyphenation;
626
691
  applied.push('hyphenation');
627
692
  }
693
+ if (idcGiven(args.capitalization)) {
694
+ style.capitalization = idcCapitalizationFor(args.capitalization);
695
+ applied.push('capitalization');
696
+ }
697
+ if (kind === 'paragraph' && idcGiven(args.align_to_baseline_grid)) {
698
+ if (typeof args.align_to_baseline_grid !== 'boolean') {
699
+ IDC.fail('invalid_argument', 'align_to_baseline_grid must be true or false.', { value: args.align_to_baseline_grid });
700
+ }
701
+ if (typeof GridAlignment === 'undefined') { IDC.fail('invalid_state', 'This runtime has no GridAlignment enumeration.', null); }
702
+ style.gridAlignment = args.align_to_baseline_grid ? GridAlignment.ALIGN_BASELINE : GridAlignment.NONE;
703
+ applied.push('align_to_baseline_grid');
704
+ }
705
+ /* A-79, Sitzung 82 (E-117 Nachtrag 1): PDF-Tag des Formats */
706
+ if (idcGiven(args.pdf_tag)) {
707
+ idcApplyPdfTag(style, args.pdf_tag);
708
+ applied.push('pdf_tag');
709
+ }
628
710
  if (kind === 'paragraph') {
629
711
  applied = applied.concat(idcApplyCompositionOptions(style, args));
630
712
  applied = applied.concat(idcApplyIndentTabLanguage(doc, style, args, ctx));
@@ -851,9 +933,12 @@ function idcDefineStyle(ctx, kind) {
851
933
  var applied = [];
852
934
  var changes, groupIds;
853
935
 
854
- if (kind === 'character' && !idcGiven(args.font_family) && !idcGiven(args.font_style) && !idcGiven(args.point_size) && !idcGiven(args.fill_hex) && !idcGiven(args.swatch_name) && !idcGiven(args.tracking) && !idcGiven(args.based_on)) {
855
- IDC.fail('invalid_argument', 'At least one attribute is required: font_family, font_style, point_size, fill_hex, swatch_name, tracking or based_on.', null);
936
+ if (kind === 'character' && !idcGiven(args.font_family) && !idcGiven(args.font_style) && !idcGiven(args.point_size) && !idcGiven(args.fill_hex) && !idcGiven(args.swatch_name) && !idcGiven(args.tracking) && !idcGiven(args.based_on) && !idcGiven(args.capitalization) && !idcGiven(args.pdf_tag)) {
937
+ IDC.fail('invalid_argument', 'At least one attribute is required: font_family, font_style, point_size, fill_hex, swatch_name, tracking, capitalization, pdf_tag or based_on.', null);
856
938
  }
939
+ /* A-68 (3): Schreibweise vor jeder Aenderung pruefen. */
940
+ if (idcGiven(args.capitalization)) { idcCapitalizationFor(args.capitalization); }
941
+ if (idcGiven(args.pdf_tag)) { idcPdfTagArg(args.pdf_tag); }
857
942
  if (idcGiven(args.based_on)) { parent = idcParentStyleArg(ctx, doc, args.based_on, kind); }
858
943
 
859
944
  if (segments === null) {
@@ -1242,14 +1327,16 @@ IDC.register('apply_character_format_to_range', { needs_document: true, read_onl
1242
1327
  var frame = found.item;
1243
1328
  var story = idcStoryOf(frame);
1244
1329
  var applied = [];
1245
- var range, swatch, first, font, sizeApplied = null, fill = null;
1330
+ var range, swatch, first, font, sizeApplied = null, fill = null, lang = null, langRead = null;
1246
1331
 
1247
1332
  if (!idcGiven(args.font_family) && !idcGiven(args.font_style) && !idcGiven(args.swatch_name) && !idcGiven(args.superscript) &&
1248
- !idcGiven(args.tracking) && !idcGiven(args.point_size)) {
1249
- IDC.fail('invalid_argument', 'At least one attribute is required: font_style, swatch_name, superscript, tracking or point_size.', null);
1333
+ !idcGiven(args.tracking) && !idcGiven(args.point_size) && !idcGiven(args.language)) {
1334
+ IDC.fail('invalid_argument', 'At least one attribute is required: font_style, swatch_name, superscript, tracking, point_size or language.', null);
1250
1335
  }
1251
1336
  range = idcRangeArg(story, args, false);
1252
1337
  idcRejectStoryLocked(story, Number(frame.id), 'apply_character_format_to_range');
1338
+ /* A-78: Sprache vor jeder Aenderung aufloesen (gleicher Loeser wie define_paragraph_style). */
1339
+ if (idcGiven(args.language)) { lang = idcLanguageArg(doc, args.language, 'language', ctx); }
1253
1340
 
1254
1341
  if (idcGiven(args.font_family) || idcGiven(args.font_style)) {
1255
1342
  applied = applied.concat(idcApplyFont(ctx, range.text, args.font_family, args.font_style, idcFontLookup(), idcFirstCharacter(story, range.start)));
@@ -1292,12 +1379,19 @@ IDC.register('apply_character_format_to_range', { needs_document: true, read_onl
1292
1379
  }
1293
1380
  applied.push('point_size');
1294
1381
  }
1382
+ if (lang !== null) {
1383
+ try { range.text.appliedLanguage = lang; } catch (e5) {
1384
+ IDC.fail('script_error', 'InDesign could not set the language: ' + String(e5.message ? e5.message : e5), { language: args.language });
1385
+ }
1386
+ applied.push('language');
1387
+ }
1295
1388
 
1296
1389
  first = idcFirstCharacter(story, range.start);
1297
1390
  if (first !== null) {
1298
1391
  sizeApplied = idcReadPt(first, 'pointSize');
1299
1392
  font = idcReadFontLoose(first);
1300
1393
  fill = idcReadSwatchName(first, 'fillColor');
1394
+ langRead = idcReadLanguage(first);
1301
1395
  } else {
1302
1396
  font = { family: null, style: null };
1303
1397
  }
@@ -1311,7 +1405,8 @@ IDC.register('apply_character_format_to_range', { needs_document: true, read_onl
1311
1405
  point_size_applied: sizeApplied,
1312
1406
  font_family: font.family,
1313
1407
  font_style: font.style,
1314
- fill_color: fill
1408
+ fill_color: fill,
1409
+ language: langRead
1315
1410
  };
1316
1411
  });
1317
1412
 
@@ -144,6 +144,14 @@ function idcStoryIsGenerated(story) {
144
144
  }
145
145
 
146
146
  /* Rahmen einer Story als Array konkreter Objekte. */
147
+ function idcStoryAmong(story, stories) {
148
+ var i;
149
+ for (i = 0; i < stories.length; i++) {
150
+ try { if (Number(stories[i].id) === Number(story.id)) { return true; } } catch (e) { /* weiter */ }
151
+ }
152
+ return false;
153
+ }
154
+
147
155
  function idcGeneratedFrames(story) {
148
156
  var out = [], containers = null, i;
149
157
  try { containers = story.textContainers; } catch (e) { containers = null; }
@@ -329,10 +337,7 @@ IDC.register('define_toc_style', { needs_document: true, read_only: false, ruler
329
337
  for (var key in props) {
330
338
  if (IDC.hasOwn(props, key)) { style[key] = props[key]; }
331
339
  }
332
- if (entries !== null) {
333
- while (style.tocStyleEntries.length > 0) { style.tocStyleEntries.item(0).remove(); }
334
- for (i = 0; i < entries.length; i++) { style.tocStyleEntries.add(entries[i].style_name, entries[i].props); }
335
- }
340
+ if (entries !== null) { idcReplaceTocEntries(style, entries); }
336
341
  } catch (e2) {
337
342
  if (created) { try { style.remove(); } catch (e3) { /* bleibt */ } }
338
343
  IDC.fail('script_error', 'InDesign could not set the TOC style "' + name + '": ' + String(e2.message ? e2.message : e2), { name: name });
@@ -511,6 +516,7 @@ IDC.register('create_toc', { needs_document: true, read_only: false, ruler: 'pag
511
516
  }
512
517
  if (story === null) { IDC.fail('script_error', 'InDesign created no table of contents story.', { toc_style: args.toc_style }); }
513
518
  if (!replace) { idcSettleGeneratedFrame(ctx, doc, story, placement); }
519
+ else if (!idcStoryAmong(story, existing)) { idcTocIntoOldPlace(ctx, doc, story, existing[0]); }
514
520
 
515
521
  out = idcGeneratedResult(doc, story, replace, 500);
516
522
  try { title = String(style.title); } catch (e2) { title = ''; }
@@ -529,6 +535,57 @@ IDC.register('create_toc', { needs_document: true, read_only: false, ruler: 'pag
529
535
  return out;
530
536
  });
531
537
 
538
+ /*
539
+ * A-68 (11), Sitzung 81: InDesign lehnt ein Inhaltsverzeichnisformat ohne
540
+ * Eintrag ab (live: „Jedes Inhaltsverzeichnisformat sollte mindestens einen
541
+ * Inhaltsverzeichniseintrag enthalten“), sobald der letzte Eintrag entfernt
542
+ * wird. Deshalb bleibt ein alter Eintrag stehen, bis die neuen angelegt
543
+ * sind; nennt die neue Liste sein Absatzformat, wird er weiterverwendet.
544
+ */
545
+ function idcReplaceTocEntries(style, entries) {
546
+ var old = [], i, keep = null, first = null, firstName = null, key;
547
+ for (i = 0; i < style.tocStyleEntries.length; i++) { old.push(style.tocStyleEntries.item(i)); }
548
+ for (i = old.length - 1; i >= 1; i--) { old[i].remove(); }
549
+ if (old.length > 0) { first = old[0]; try { firstName = String(first.name); } catch (e0) { firstName = null; } }
550
+ for (i = 0; i < entries.length; i++) {
551
+ if (keep === null && first !== null && entries[i].style_name === firstName) {
552
+ for (key in entries[i].props) {
553
+ if (IDC.hasOwn(entries[i].props, key)) { first[key] = entries[i].props[key]; }
554
+ }
555
+ keep = first;
556
+ } else {
557
+ style.tocStyleEntries.add(entries[i].style_name, entries[i].props);
558
+ }
559
+ }
560
+ if (first !== null && keep === null) { first.remove(); }
561
+ }
562
+
563
+ /*
564
+ * A-68 (12), Sitzung 81: createTOC(replacing) ersetzt nur ein Verzeichnis
565
+ * desselben Formats; mit einem anderen Format legte InDesign ein zweites an
566
+ * (live auf Seite 1). Dann kommt das neue an Seite, Lage, Groesse und Ebene
567
+ * des ersten Rahmens des alten, und die Rahmen des alten werden entfernt.
568
+ */
569
+ function idcTocIntoOldPlace(ctx, doc, story, oldStory) {
570
+ var oldFrames = idcGeneratedFrames(oldStory), first, page = null, rect = null, layer = null, frames, i;
571
+ if (oldFrames.length === 0) { return false; }
572
+ first = oldFrames[0];
573
+ try { page = first.parentPage; } catch (e0) { page = null; }
574
+ if (page === null || page === undefined) { return false; }
575
+ rect = idcItemBounds(doc, first);
576
+ try { layer = first.itemLayer; } catch (e1) { layer = null; }
577
+ idcSettleGeneratedFrame(ctx, doc, story, { page: page, rect: rect, point: rect === null ? null : [rect.x, rect.y] });
578
+ frames = idcGeneratedFrames(story);
579
+ if (layer !== null && layer !== undefined && frames.length > 0) {
580
+ try { frames[0].itemLayer = layer; } catch (e2) { /* bleibt auf der aktiven Ebene */ }
581
+ }
582
+ for (i = oldFrames.length - 1; i >= 0; i--) {
583
+ try { oldFrames[i].remove(); } catch (e3) { ctx.warn('A frame of the previous table of contents could not be removed: ' + String(e3.message ? e3.message : e3)); }
584
+ }
585
+ ctx.warn('The existing table of contents was made with another TOC style; InDesign added a new one, which now takes the place of the old frame (the old one was removed).');
586
+ return true;
587
+ }
588
+
532
589
  /* ------------------------------------------------------------------ Register */
533
590
 
534
591
  function idcIndexOf(doc, create) {
@@ -29,6 +29,9 @@ function idcEpubTables() {
29
29
  order: { layout: ExportOrder.LAYOUT_ORDER, articles: ExportOrder.ARTICLE_PANEL_ORDER, xml: ExportOrder.XML_STRUCTURE_ORDER },
30
30
  resolution: { '72': ImageResolution.PPI_72, '96': ImageResolution.PPI_96, '150': ImageResolution.PPI_150, '300': ImageResolution.PPI_300 },
31
31
  format: { automatic: ImageConversion.AUTOMATIC, jpeg: ImageConversion.JPEG, png: ImageConversion.PNG, gif: ImageConversion.GIF },
32
+ /* A-68 (8), Sitzung 81: Ausrichtung und Groesse der Objekte (Probe 11.11.2026). */
33
+ alignment: typeof ImageAlignmentType === 'undefined' ? {} : { left: ImageAlignmentType.ALIGN_LEFT, center: ImageAlignmentType.ALIGN_CENTER, right: ImageAlignmentType.ALIGN_RIGHT },
34
+ size: typeof ImageSizeOption === 'undefined' ? {} : { none: ImageSizeOption.SIZE_NONE, fixed: ImageSizeOption.SIZE_FIXED, relative: ImageSizeOption.SIZE_RELATIVE_TO_TEXT_FLOW },
32
35
  quality: { low: JPEGOptionsQuality.LOW, medium: JPEGOptionsQuality.MEDIUM, high: JPEGOptionsQuality.HIGH, maximum: JPEGOptionsQuality.MAXIMUM },
33
36
  spread: { document: EpubFixedLayoutSpreadControl.SPREADS_BASED_ON_DOC, physical: EpubFixedLayoutSpreadControl.PHYSICAL_SPREADS,
34
37
  synthetic: EpubFixedLayoutSpreadControl.SYNTHETIC_SPREADS, none: EpubFixedLayoutSpreadControl.NO_SPREADS },
@@ -106,7 +109,10 @@ function idcEpubTopItems(page) {
106
109
  var out = [];
107
110
  var i, items;
108
111
  try { items = page.pageItems; } catch (e) { return out; }
109
- for (i = 0; i < items.length; i++) { out.push(items.item(i)); }
112
+ /* A-68 Befund, Sitzung 81: pageItems liefert allgemeine PageItem-Objekte; ein
113
+ Textrahmen mit verankerter Vignette galt so als nicht verankertes Objekt
114
+ (Klasse PageItem, Inhalt ueber allGraphics). Deshalb die konkrete Klasse. */
115
+ for (i = 0; i < items.length; i++) { out.push(IDC.concrete(items.item(i))); }
110
116
  return out;
111
117
  }
112
118
 
@@ -119,6 +125,53 @@ function idcEpubItemHasContent(item) {
119
125
  return cls === 'Group';
120
126
  }
121
127
 
128
+ /* A-77: split_by_paragraph_style als Name oder Liste -> Liste ohne Doppelte. */
129
+ function idcEpubSplitNames(value) {
130
+ var list = [], out = [], i, j, seen;
131
+ if (IDC.isArray(value)) { list = value; } else { list = [value]; }
132
+ if (list.length === 0) { IDC.fail('invalid_argument', 'split_by_paragraph_style must name at least one paragraph style.', null); }
133
+ for (i = 0; i < list.length; i++) {
134
+ if (typeof list[i] !== 'string' || list[i].length === 0) {
135
+ IDC.fail('invalid_argument', 'split_by_paragraph_style must be a paragraph style name or a list of names.', { value: list[i] });
136
+ }
137
+ seen = false;
138
+ for (j = 0; j < out.length; j++) { if (out[j] === list[i]) { seen = true; } }
139
+ if (!seen) { out.push(list[i]); }
140
+ }
141
+ return out;
142
+ }
143
+
144
+ /* Alle Absatzformate und welche davon teilen sollen; fehlende Namen = not_found (alle auf einmal). */
145
+ function idcEpubSplitStyles(doc, names) {
146
+ var all = [], chosen = [], missing = [], found, i, k, coll;
147
+ try { coll = doc.allParagraphStyles; } catch (e) { coll = []; }
148
+ for (i = 0; i < coll.length; i++) { all.push(coll[i]); chosen.push(false); }
149
+ for (k = 0; k < names.length; k++) {
150
+ found = false;
151
+ for (i = 0; i < all.length; i++) {
152
+ if (String(all[i].name) === names[k]) { chosen[i] = true; found = true; }
153
+ }
154
+ if (!found) { missing.push(names[k]); }
155
+ }
156
+ if (missing.length > 0) {
157
+ IDC.fail('not_found', 'Paragraph style(s) not in this document: ' + missing.join(', ') + '.',
158
+ { code: 'paragraph_style_not_found', paragraph_style: missing[0], missing: missing });
159
+ }
160
+ return { all: all, chosen: chosen };
161
+ }
162
+
163
+ /* Womit geteilt wird: null ohne Teilung, sonst die Formatnamen (ein Name oder die Exporttags). */
164
+ function idcEpubSplitUsed(doc, prefs) {
165
+ var out = [], coll, i;
166
+ if (prefs.breakDocument !== true) { return null; }
167
+ if (String(prefs.paragraphStyleName) !== '') { return [String(prefs.paragraphStyleName)]; }
168
+ try { coll = doc.allParagraphStyles; } catch (e) { coll = []; }
169
+ for (i = 0; i < coll.length; i++) {
170
+ try { if (coll[i].splitDocument === true) { out.push(String(coll[i].name)); } } catch (e2) { /* weiter */ }
171
+ }
172
+ return out;
173
+ }
174
+
122
175
  /* ------------------------------------------------------------ export_epub */
123
176
 
124
177
  IDC.register('export_epub', { needs_document: true, read_only: false, ruler: 'none' },
@@ -127,6 +180,7 @@ IDC.register('export_epub', { needs_document: true, read_only: false, ruler: 'no
127
180
  var doc = ctx.doc;
128
181
  var fixed, prefs, file, existed, overwrite, changes = [], problems = [], used = {}, meta, key, value;
129
182
  var tocNames, coverMode, coverFile, bytes, modifiedBefore, language, i, j, items, item, landing, unanchored = 0, fonts, f, font, status;
183
+ var splitNames = null, splitStyles = null;
130
184
 
131
185
  idcEpubAvailable();
132
186
  var t = idcEpubTables();
@@ -160,6 +214,9 @@ IDC.register('export_epub', { needs_document: true, read_only: false, ruler: 'no
160
214
  if (args.export_order !== undefined && fixed) { ctx.warn('export_order applies to reflowable EPUB only and was ignored.'); }
161
215
  if (args.split_by_paragraph_style !== undefined && fixed) { ctx.warn('split_by_paragraph_style applies to reflowable EPUB only and was ignored.'); }
162
216
  if (args.embed_fonts !== undefined && fixed) { ctx.warn('embed_fonts applies to reflowable EPUB only; a fixed-layout EPUB embeds the fonts.'); }
217
+ if ((args.image_alignment !== undefined || args.image_size !== undefined) && fixed) {
218
+ ctx.warn('image_alignment and image_size apply to reflowable EPUB only and were ignored.');
219
+ }
163
220
  if ((args.page_range !== undefined || args.spread_control !== undefined) && !fixed) {
164
221
  ctx.warn('page_range and spread_control apply to fixed-layout EPUB only and were ignored.');
165
222
  }
@@ -175,11 +232,17 @@ IDC.register('export_epub', { needs_document: true, read_only: false, ruler: 'no
175
232
  { code: 'toc_style_not_found', toc_style: args.toc_style, available: tocNames });
176
233
  }
177
234
  if (args.split_by_paragraph_style !== undefined && !fixed) {
178
- var splitStyle = null;
179
- try { splitStyle = doc.paragraphStyles.itemByName(String(args.split_by_paragraph_style)); } catch (eS) { splitStyle = null; }
180
- if (splitStyle === null || splitStyle.isValid !== true) {
181
- IDC.fail('not_found', 'Paragraph style "' + args.split_by_paragraph_style + '" does not exist in this document.',
182
- { code: 'paragraph_style_not_found', paragraph_style: args.split_by_paragraph_style });
235
+ splitNames = idcEpubSplitNames(args.split_by_paragraph_style);
236
+ if (splitNames.length === 1) {
237
+ var splitStyle = null;
238
+ try { splitStyle = doc.paragraphStyles.itemByName(splitNames[0]); } catch (eS) { splitStyle = null; }
239
+ if (splitStyle === null || splitStyle.isValid !== true) { splitStyle = IDC.paragraphStyleByName(doc, splitNames[0]); }
240
+ if (splitStyle === null || splitStyle.isValid !== true) {
241
+ IDC.fail('not_found', 'Paragraph style "' + splitNames[0] + '" does not exist in this document.',
242
+ { code: 'paragraph_style_not_found', paragraph_style: splitNames[0] });
243
+ }
244
+ } else {
245
+ splitStyles = idcEpubSplitStyles(doc, splitNames);
183
246
  }
184
247
  }
185
248
  coverMode = args.cover === undefined ? null : String(args.cover);
@@ -219,11 +282,27 @@ IDC.register('export_epub', { needs_document: true, read_only: false, ruler: 'no
219
282
  if (args.export_order !== undefined) {
220
283
  idcEpubSet(prefs, 'exportOrder', idcEpubPick('export_order', args.export_order, t.order), changes);
221
284
  }
222
- if (args.split_by_paragraph_style !== undefined) {
285
+ if (args.split_by_paragraph_style !== undefined && splitStyles === null) {
286
+ idcEpubSet(prefs, 'breakDocument', true, changes);
287
+ idcEpubSet(prefs, 'paragraphStyleName', splitNames[0], changes);
288
+ } else if (splitStyles !== null) {
289
+ /* A-77: mehrere Formate ueber die Exporttags (splitDocument) der
290
+ Absatzformate; InDesign teilt danach nur bei leerem
291
+ paragraphStyleName (Probe 11.11.2026). Genau die genannten
292
+ Formate teilen, alle anderen voruebergehend nicht. */
293
+ for (i = 0; i < splitStyles.all.length; i++) {
294
+ idcEpubSet(splitStyles.all[i], 'splitDocument', splitStyles.chosen[i] === true, changes);
295
+ }
223
296
  idcEpubSet(prefs, 'breakDocument', true, changes);
224
- idcEpubSet(prefs, 'paragraphStyleName', String(args.split_by_paragraph_style), changes);
297
+ idcEpubSet(prefs, 'paragraphStyleName', '', changes);
225
298
  }
226
299
  if (args.embed_fonts !== undefined) { idcEpubSet(prefs, 'embedFont', args.embed_fonts === true, changes); }
300
+ if (args.image_alignment !== undefined) {
301
+ idcEpubSet(prefs, 'imageAlignment', idcEpubPick('image_alignment', args.image_alignment, t.alignment), changes);
302
+ }
303
+ if (args.image_size !== undefined) {
304
+ idcEpubSet(prefs, 'customImageSizeOption', idcEpubPick('image_size', args.image_size, t.size), changes);
305
+ }
227
306
  }
228
307
  /* Nur fixed layout */
229
308
  if (fixed) {
@@ -273,8 +352,12 @@ IDC.register('export_epub', { needs_document: true, read_only: false, ruler: 'no
273
352
  used.jpeg_quality = idcEpubNameOf(prefs.jpegOptionsQuality, t.quality);
274
353
  if (!fixed) {
275
354
  used.export_order = idcEpubNameOf(prefs.exportOrder, t.order);
276
- used.split_by_paragraph_style = prefs.breakDocument === true ? String(prefs.paragraphStyleName) : null;
355
+ used.split_by_paragraph_styles = idcEpubSplitUsed(doc, prefs);
356
+ used.split_by_paragraph_style = used.split_by_paragraph_styles === null ? null
357
+ : (String(prefs.paragraphStyleName) !== '' ? String(prefs.paragraphStyleName) : (used.split_by_paragraph_styles.length > 0 ? used.split_by_paragraph_styles[0] : null));
277
358
  used.embed_fonts = prefs.embedFont === true;
359
+ try { used.image_alignment = idcEpubNameOf(prefs.imageAlignment, t.alignment); } catch (eIa) { used.image_alignment = null; }
360
+ try { used.image_size = idcEpubNameOf(prefs.customImageSizeOption, t.size); } catch (eIs) { used.image_size = null; }
278
361
  } else {
279
362
  used.page_range = Number(prefs.epubPageRangeFormat) === Number(PageRangeFormat.EXPORT_PAGE_RANGE) ? String(prefs.epubPageRange) : null;
280
363
  used.spread_control = idcEpubNameOf(prefs.epubSpreadControlOptions, t.spread);