layoutbridge-light 0.0.0-stage → 1.0.0

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 (88) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +26 -0
  3. package/README.de.md +338 -0
  4. package/README.md +325 -2
  5. package/THIRD-PARTY-NOTICES.md +2878 -0
  6. package/dist/src/mcp/catalog.js +126 -0
  7. package/dist/src/mcp/config.js +211 -0
  8. package/dist/src/mcp/dispatch.js +164 -0
  9. package/dist/src/mcp/epub-check.js +331 -0
  10. package/dist/src/mcp/epub-orientation.js +132 -0
  11. package/dist/src/mcp/license/free-tools.js +16 -0
  12. package/dist/src/mcp/license/gate.js +210 -0
  13. package/dist/src/mcp/license/index.js +107 -0
  14. package/dist/src/mcp/license/instance-name.js +57 -0
  15. package/dist/src/mcp/license/key-source.js +59 -0
  16. package/dist/src/mcp/license/lemonsqueezy-verifier.js +207 -0
  17. package/dist/src/mcp/license/placeholder-verifier.js +10 -0
  18. package/dist/src/mcp/license/polar-ids.js +100 -0
  19. package/dist/src/mcp/license/polar-verifier.js +285 -0
  20. package/dist/src/mcp/license/product-ids.js +73 -0
  21. package/dist/src/mcp/license/state-store.js +91 -0
  22. package/dist/src/mcp/license/types.js +11 -0
  23. package/dist/src/mcp/logging.js +271 -0
  24. package/dist/src/mcp/product-info.js +54 -0
  25. package/dist/src/mcp/product.js +56 -0
  26. package/dist/src/mcp/server.js +253 -0
  27. package/dist/src/mcp/setup.js +680 -0
  28. package/dist/src/mcp/start.cjs +117 -0
  29. package/dist/src/mcp/tool-definition.js +2 -0
  30. package/dist/src/mcp/tools.js +2425 -0
  31. package/dist/src/mcp/typography-rules.js +70 -0
  32. package/dist/src/transport/com.js +250 -0
  33. package/dist/src/transport/osascript.js +171 -0
  34. package/dist/src/transport/queue.js +19 -0
  35. package/dist/src/transport/script-bundle.js +92 -0
  36. package/dist/src/transport/transport.js +30 -0
  37. package/indesign/commands/10-app.js +6 -0
  38. package/indesign/commands/20-documents.js +517 -0
  39. package/indesign/commands/22-document-setup.js +204 -0
  40. package/indesign/commands/23-save-package.js +258 -0
  41. package/indesign/commands/25-export.js +448 -0
  42. package/indesign/commands/26-export-image.js +202 -0
  43. package/indesign/commands/27-textdefaults.js +151 -0
  44. package/indesign/commands/28-selection.js +174 -0
  45. package/indesign/commands/30-pages.js +1032 -0
  46. package/indesign/commands/31-view.js +210 -0
  47. package/indesign/commands/32-sections.js +203 -0
  48. package/indesign/commands/35-parents.js +428 -0
  49. package/indesign/commands/37-layout.js +837 -0
  50. package/indesign/commands/40-frames.js +851 -0
  51. package/indesign/commands/41-shapes.js +505 -0
  52. package/indesign/commands/42-groups.js +117 -0
  53. package/indesign/commands/43-layers.js +310 -0
  54. package/indesign/commands/44-effects.js +274 -0
  55. package/indesign/commands/45-frame-options.js +247 -0
  56. package/indesign/commands/47-anchored.js +331 -0
  57. package/indesign/commands/50-modify.js +781 -0
  58. package/indesign/commands/52-align.js +139 -0
  59. package/indesign/commands/55-fit.js +106 -0
  60. package/indesign/commands/60-images.js +1128 -0
  61. package/indesign/commands/62-embed.js +120 -0
  62. package/indesign/commands/69-placeholders.js +127 -0
  63. package/indesign/commands/70-text.js +675 -0
  64. package/indesign/commands/71-outlines.js +92 -0
  65. package/indesign/commands/72-place-text.js +476 -0
  66. package/indesign/commands/73-story-lock.js +157 -0
  67. package/indesign/commands/74-stories.js +121 -0
  68. package/indesign/commands/75-tables.js +676 -0
  69. package/indesign/commands/76-text-variables.js +260 -0
  70. package/indesign/commands/77-story-options.js +86 -0
  71. package/indesign/commands/78-frame-lines.js +452 -0
  72. package/indesign/commands/79-check-typography.js +341 -0
  73. package/indesign/commands/80-styles.js +1865 -0
  74. package/indesign/commands/81-composition.js +355 -0
  75. package/indesign/commands/82-object-styles.js +263 -0
  76. package/indesign/commands/83-drop-caps.js +87 -0
  77. package/indesign/commands/83-indents-tabs.js +264 -0
  78. package/indesign/commands/84-import-styles.js +142 -0
  79. package/indesign/commands/85-swatches.js +112 -0
  80. package/indesign/commands/86-gradients.js +185 -0
  81. package/indesign/commands/87-replace-swatch.js +180 -0
  82. package/indesign/commands/88-replace-font.js +212 -0
  83. package/indesign/commands/89-toc-index.js +945 -0
  84. package/indesign/commands/90-print-on-demand.js +495 -0
  85. package/indesign/commands/91-epub.js +546 -0
  86. package/indesign/interpreter.js +779 -0
  87. package/package.json +41 -4
  88. package/rules/typography-rules.json +31 -0
@@ -0,0 +1,675 @@
1
+ /* Bereich G – Text lesen und schreiben. */
2
+
3
+ /*
4
+ * Zeilenumbrueche im Parameter: ein echter Zeilenumbruch (LF, CRLF) wird zur
5
+ * Absatzmarke (CR, wie InDesign sie fuehrt). U+2028 bleibt ein weicher
6
+ * Zeilenumbruch, alles andere unveraendert (E-30).
7
+ */
8
+ function idcNormaliseText(text) {
9
+ var s = String(text);
10
+ var out = '';
11
+ var i, ch;
12
+ for (i = 0; i < s.length; i++) {
13
+ ch = s.charAt(i);
14
+ if (ch === '\r') {
15
+ out += '\r';
16
+ if (i + 1 < s.length && s.charAt(i + 1) === '\n') { i += 1; }
17
+ } else if (ch === '\n') {
18
+ out += '\r';
19
+ } else {
20
+ out += ch;
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+
26
+ /* Enumeratorname des letzten Zeichens (SpecialCharacters) in die Absatzende-Art. */
27
+ function idcBreakTypeFromEnum(name, fallback) {
28
+ if (name === 'COLUMN_BREAK') { return 'column'; }
29
+ if (name === 'FRAME_BREAK') { return 'frame'; }
30
+ if (name === 'PAGE_BREAK') { return 'page'; }
31
+ if (name === 'ODD_PAGE_BREAK') { return 'odd_page'; }
32
+ if (name === 'EVEN_PAGE_BREAK') { return 'even_page'; }
33
+ return fallback;
34
+ }
35
+
36
+ /* Absatzende-Art am letzten Zeichen eines Absatzes. */
37
+ function idcBreakType(lastChar) {
38
+ var code = lastChar.length === 0 ? -1 : lastChar.charCodeAt(0);
39
+ if (code === 13) { return 'paragraph'; }
40
+ if (code === 14) { return 'column'; }
41
+ if (code === 15) { return 'frame'; }
42
+ if (code === 16) { return 'page'; }
43
+ if (code === 17) { return 'odd_page'; }
44
+ if (code === 18) { return 'even_page'; }
45
+ return 'none';
46
+ }
47
+
48
+ function idcStoryOf(frame) {
49
+ var story = null;
50
+ try { story = frame.parentStory; } catch (e) { story = null; }
51
+ if (story === null || story === undefined) {
52
+ IDC.fail('invalid_state', 'Text frame ' + Number(frame.id) + ' has no story.', { frame_id: Number(frame.id) });
53
+ }
54
+ return story;
55
+ }
56
+
57
+ function idcOverflows(frame) {
58
+ try { return frame.overflows === true; } catch (e) { return null; }
59
+ }
60
+
61
+ function idcParagraphStyleName(paragraph) {
62
+ try { return String(paragraph.appliedParagraphStyle.name); } catch (e) { return null; }
63
+ }
64
+
65
+ /* ----------------------------------------------------------- get_frame_text */
66
+
67
+ IDC.register('get_frame_text', { needs_document: true, read_only: true, ruler: 'none' },
68
+ function (ctx) {
69
+ var args = ctx.args;
70
+ var doc = ctx.doc;
71
+ var found = idcTextFrameArg(doc, args, 'frame_id');
72
+ var frame = found.item;
73
+ var story = idcStoryOf(frame);
74
+ var paragraphs = [];
75
+ var storyLength = 0;
76
+ var i, p, text, length, last, breakType, position = 0, endExcl, frameStart = null, frameEnd = null, frameChars = null;
77
+ var onlyPortion = args.only_frame_portion === true;
78
+ var showPlaceholders = args.show_placeholders === true;
79
+ var maxChars = args.max_chars === undefined || args.max_chars === null ? null : args.max_chars;
80
+ var kept, used = 0, omitted = 0, truncated = false, entry, from, to;
81
+ var lockInfo = idcStoryLockInfo(story);
82
+
83
+ if (args.include_overset !== undefined) {
84
+ IDC.fail('invalid_argument', 'get_frame_text has no include_overset parameter; use only_frame_portion true for the part of the story in this frame.', null);
85
+ }
86
+ if (args.only_frame_portion !== undefined && args.only_frame_portion !== null && typeof args.only_frame_portion !== 'boolean') {
87
+ IDC.fail('invalid_argument', 'only_frame_portion must be true or false.', { only_frame_portion: args.only_frame_portion });
88
+ }
89
+ if (maxChars !== null && (typeof maxChars !== 'number' || maxChars < 1 || Math.floor(maxChars) !== maxChars)) {
90
+ IDC.fail('invalid_argument', 'max_chars must be a whole number of 1 or more.', { max_chars: maxChars });
91
+ }
92
+ try { storyLength = Number(story.characters.length); } catch (e0) { storyLength = 0; }
93
+
94
+ try {
95
+ for (i = 0; i < story.paragraphs.length; i++) {
96
+ p = story.paragraphs.item(i);
97
+ text = '';
98
+ try { text = String(p.contents); } catch (e1) { text = ''; }
99
+ try { length = Number(p.characters.length); } catch (e2) { length = text.length; }
100
+ last = text.length > 0 ? text.charAt(text.length - 1) : '';
101
+ breakType = idcBreakType(last);
102
+ /*
103
+ * Live 05.09.2026: im Absatztext (String) erscheinen Spalten-, Rahmen-
104
+ * und Seitenumbruch als "\r"; erst das letzte Zeichen einzeln gelesen
105
+ * nennt den Enumerator (COLUMN_BREAK ...). Deshalb nachfassen.
106
+ */
107
+ if (breakType === 'paragraph' && length > 0) {
108
+ try { breakType = idcBreakTypeFromEnum(String(p.characters.item(length - 1).contents), breakType); } catch (eBT) { /* bleibt */ }
109
+ }
110
+ endExcl = breakType === 'none' ? position + length : position + length - 1;
111
+ paragraphs.push({
112
+ index: i,
113
+ start_index: position,
114
+ end_index: endExcl,
115
+ end_index_incl_break: position + length,
116
+ paragraph_style: idcParagraphStyleName(p),
117
+ text: (function () { var t = breakType === 'none' ? text : text.substr(0, text.length - 1); return showPlaceholders ? idcShowPlaceholders(t) : t; })(),
118
+ is_empty: endExcl === position,
119
+ break_type: breakType
120
+ });
121
+ position += length;
122
+ }
123
+ } catch (e) {
124
+ IDC.fail('script_error', 'InDesign could not read the paragraphs of frame ' + args.frame_id + ': ' + String(e.message ? e.message : e),
125
+ { frame_id: args.frame_id, paragraphs_read: paragraphs.length });
126
+ }
127
+
128
+ /* Anteil dieses Rahmens an der Story (verkettete Rahmen). */
129
+ try {
130
+ var portion = frame.texts.item(0);
131
+ frameChars = Number(portion.characters.length);
132
+ if (frameChars > 0) {
133
+ frameStart = Number(portion.characters.item(0).index);
134
+ frameEnd = frameStart + frameChars;
135
+ } else {
136
+ /* Leerer Rahmen am Ende der Kette: Anteil 0, Indizes ohne Aussage. */
137
+ frameStart = null; frameEnd = null;
138
+ }
139
+ } catch (e3) { frameStart = null; frameEnd = null; frameChars = null; }
140
+
141
+ /*
142
+ * only_frame_portion: nur Absaetze, die diesen Rahmen beruehren; Text an
143
+ * den Rahmengrenzen beschnitten (Fund 5, Hefttest). max_chars: Textbudget
144
+ * ueber alle gelieferten Absaetze, danach nur noch Zaehlung.
145
+ */
146
+ if (onlyPortion) {
147
+ kept = [];
148
+ if (frameStart !== null) {
149
+ for (i = 0; i < paragraphs.length; i++) {
150
+ entry = paragraphs[i];
151
+ if (entry.end_index_incl_break <= frameStart || entry.start_index >= frameEnd) { continue; }
152
+ from = Math.max(entry.start_index, frameStart);
153
+ to = Math.min(entry.end_index, frameEnd);
154
+ entry.clipped_start = from > entry.start_index;
155
+ entry.clipped_end = to < entry.end_index;
156
+ if (entry.clipped_start || entry.clipped_end) {
157
+ entry.text = entry.text.substr(from - entry.start_index, Math.max(0, to - from));
158
+ }
159
+ kept.push(entry);
160
+ }
161
+ }
162
+ paragraphs = kept;
163
+ }
164
+ if (maxChars !== null) {
165
+ kept = [];
166
+ for (i = 0; i < paragraphs.length; i++) {
167
+ entry = paragraphs[i];
168
+ if (used >= maxChars) { omitted += 1; truncated = true; continue; }
169
+ if (used + entry.text.length > maxChars) {
170
+ entry.text = entry.text.substr(0, maxChars - used);
171
+ entry.clipped_end = true;
172
+ truncated = true;
173
+ }
174
+ used += entry.text.length;
175
+ kept.push(entry);
176
+ }
177
+ paragraphs = kept;
178
+ }
179
+
180
+ return {
181
+ frame_id: Number(frame.id),
182
+ story_id: (function () { try { return Number(story.id); } catch (e4) { return null; } })(),
183
+ story_length: storyLength,
184
+ paragraphs: paragraphs,
185
+ paragraph_count: paragraphs.length,
186
+ only_frame_portion: onlyPortion,
187
+ show_placeholders: showPlaceholders,
188
+ paragraphs_omitted: omitted,
189
+ truncated: truncated,
190
+ overset: idcOverflows(frame),
191
+ threaded: idcIsThreaded(frame),
192
+ frame_start_index: frameStart,
193
+ frame_end_index: frameEnd,
194
+ frame_character_count: frameChars,
195
+ lock_state: lockInfo.lock_state,
196
+ managed_file: lockInfo.managed_file
197
+ };
198
+ });
199
+
200
+ /* ----------------------------------------------------------------- set_text */
201
+
202
+ IDC.register('set_text', { needs_document: true, read_only: false, ruler: 'none' },
203
+ function (ctx) {
204
+ var args = ctx.args;
205
+ var doc = ctx.doc;
206
+ var found = idcTextFrameArg(doc, args, 'frame_id');
207
+ var frame = found.item;
208
+ var story = idcStoryOf(frame);
209
+ var text, before = 0, after = 0;
210
+
211
+ if (typeof args.text !== 'string') {
212
+ IDC.fail('invalid_argument', 'text is required (a string; "" empties the frame).', null);
213
+ }
214
+ /* A-8.8: erst Absatzmarken normalisieren, dann Platzhalter – {{line_break}} bleibt so ein Zeilenumbruch. */
215
+ var expansion = idcExpandPlaceholders(idcNormaliseText(args.text), args.placeholders !== false);
216
+ var split = idcSplitBreaks(expansion.text);
217
+ text = split.plain;
218
+ idcPlaceholderWarnings(ctx, expansion);
219
+ idcRejectStoryLocked(story, Number(frame.id), 'set_text');
220
+ try { before = Number(story.characters.length); } catch (e0) { before = 0; }
221
+
222
+ /* Wirklich ersetzen: die ganze Story (0 … Laenge), nicht anhaengen. */
223
+ try {
224
+ story.contents = text;
225
+ } catch (e) {
226
+ try {
227
+ if (before > 0) { story.characters.itemByRange(0, before - 1).contents = text; }
228
+ else { story.insertionPoints.item(0).contents = text; }
229
+ } catch (e2) {
230
+ IDC.fail('script_error', 'InDesign could not set the text of frame ' + args.frame_id + ': ' + String(e.message ? e.message : e) +
231
+ ' / ' + String(e2.message ? e2.message : e2), { frame_id: args.frame_id });
232
+ }
233
+ }
234
+ try { idcInsertBreaks(story, 0, split.breaks); } catch (eB) {
235
+ if (eB && eB.isIdcError) { throw eB; }
236
+ ctx.warn('Column/frame/page breaks could not be inserted: ' + String(eB.message ? eB.message : eB));
237
+ }
238
+ text = expansion.text;
239
+ try { after = Number(story.characters.length); } catch (e3) { after = null; }
240
+ if (after !== null && after !== text.length) {
241
+ ctx.warn('The story reads ' + after + ' characters after writing ' + text.length + ' – InDesign may have converted or dropped characters.');
242
+ }
243
+ if (idcIsThreaded(frame)) {
244
+ ctx.warn('The frame is threaded (verkettet); the whole story across all its frames was replaced.');
245
+ }
246
+ ctx.changes = { modified_frame_ids: [Number(frame.id)] };
247
+ return {
248
+ frame_id: Number(frame.id),
249
+ character_count: after,
250
+ previous_character_count: before,
251
+ overset: idcOverflows(frame),
252
+ story_id: (function () { try { return Number(story.id); } catch (e4) { return null; } })(),
253
+ placeholders_replaced: expansion.replaced,
254
+ placeholders_unknown: expansion.unknown
255
+ };
256
+ });
257
+
258
+ /* -------------------------------------------------------- set_text_in_range */
259
+
260
+ /*
261
+ * Sitzung 24 (Befund aus dem Praxiseinsatz): contents = auf einem Bereich loescht
262
+ * ihn und setzt am Einfuegepunkt neu ein. Der Einfuegepunkt traegt das
263
+ * Format des Zeichens davor – beginnt der Bereich an einem Absatzanfang, ist
264
+ * das die Absatzmarke des vorigen Absatzes, und der neue Text erbt dessen
265
+ * Format. keep_format_of:
266
+ * start (Vorgabe) neuen Text hinter das erste ersetzte Zeichen setzen –
267
+ * er erbt dessen Format –, dann die alten Zeichen entfernen;
268
+ * before bisheriger Weg (InDesigns eigene Wahl, das Zeichen davor);
269
+ * none bisheriger Weg, danach Zeichenformat [None] und keine lokalen
270
+ * Zeichenabweichungen im neuen Text (Format des Absatzformats).
271
+ */
272
+ function idcKeepFormatOf(args) {
273
+ var mode = args.keep_format_of;
274
+ if (mode === undefined || mode === null) { return 'start'; }
275
+ if (mode !== 'start' && mode !== 'before' && mode !== 'none') {
276
+ IDC.fail('invalid_argument', 'keep_format_of must be "start", "before" or "none".', { keep_format_of: mode });
277
+ }
278
+ return mode;
279
+ }
280
+
281
+ /*
282
+ * Bereich [start, end) durch text ersetzen, absatzweise (Nacharbeit 2 zu
283
+ * Sitzung 24, Lauf S24). Vorbild je ersetztem Absatz ist sein erstes Zeichen
284
+ * im Bereich: der Bereichsanfang, dann jede Stelle hinter einer Absatzmarke
285
+ * im Bereich. Der neue Text wird an den Absatzmarken geteilt; Teil 0 (bis
286
+ * einschliesslich der ersten neuen Absatzmarke) bekommt das Format des
287
+ * ersten ersetzten Zeichens, Teil i das des i-ten ersetzten Absatzes, jeder
288
+ * weitere Teil das des letzten ersetzten Absatzes.
289
+ *
290
+ * Jeder Teil wird hinter sein Vorbild eingefuegt und erbt so dessen Format.
291
+ * Ist das Vorbild eine Absatzmarke, liegt der Einfuegepunkt dahinter schon
292
+ * im naechsten Absatz (Lauf S24: 9 statt 20 pt) – dann vor ihr einfuegen,
293
+ * also im Absatz, dessen Marke ersetzt wird. Danach die alten Zeichen
294
+ * entfernen (von hinten) und jedem neuen Absatz das Absatzformat seines
295
+ * Vorbilds zuweisen, wenn InDesign beim Verschmelzen ein anderes gewaehlt
296
+ * hat (lokale Abweichungen bleiben).
297
+ */
298
+ function idcReplaceKeepingStart(story, start, end, text) {
299
+ var old = '', anchors = [], segments = [], groups = [], i, k, c, shift, ip, piece, first, last, runEnd, offset, para, style;
300
+ try { old = String(story.characters.itemByRange(start, end - 1).contents); } catch (e0) { old = ''; }
301
+ if (old.length !== end - start) {
302
+ old = '';
303
+ for (i = start; i < end; i++) { old += String(story.characters.item(i).contents); }
304
+ }
305
+ anchors.push(start);
306
+ for (i = 0; i < old.length - 1; i++) {
307
+ if (old.charAt(i) === '\r') { anchors.push(start + i + 1); }
308
+ }
309
+ /* neuer Text in Teile bis einschliesslich jeder Absatzmarke */
310
+ piece = '';
311
+ for (i = 0; i < text.length; i++) {
312
+ piece += text.charAt(i);
313
+ if (text.charAt(i) === '\r') { segments.push(piece); piece = ''; }
314
+ }
315
+ if (piece.length > 0 || segments.length === 0) { segments.push(piece); }
316
+ /* Gruppen je Vorbild: Text, Einfuegepunkt, Absatzformat des Vorbilds, Anfang jedes Teils im neuen Text */
317
+ for (k = 0; k < anchors.length; k++) {
318
+ groups.push({ anchor: anchors[k], text: '', parts: [], style: null, ip: 0 });
319
+ }
320
+ offset = 0;
321
+ for (i = 0; i < segments.length; i++) {
322
+ k = i < anchors.length ? i : anchors.length - 1;
323
+ groups[k].parts.push(offset);
324
+ groups[k].text += segments[i];
325
+ offset += segments[i].length;
326
+ }
327
+ for (k = 0; k < groups.length; k++) {
328
+ c = old.charAt(groups[k].anchor - start);
329
+ groups[k].ip = c === '\r' ? groups[k].anchor : groups[k].anchor + 1;
330
+ try { groups[k].style = story.characters.item(groups[k].anchor).appliedParagraphStyle; } catch (eS) { groups[k].style = null; }
331
+ }
332
+ /* einfuegen von hinten, damit die vorderen Stellen gelten */
333
+ for (k = groups.length - 1; k >= 0; k--) {
334
+ if (groups[k].text.length > 0) { story.insertionPoints.item(groups[k].ip).contents = groups[k].text; }
335
+ }
336
+ /* alte Zeichen: neue Lage = alte + Laenge aller Teile, die davor eingefuegt wurden; zusammenhaengend von hinten entfernen */
337
+ runEnd = -1;
338
+ last = -1;
339
+ for (i = end - 1; i >= start - 1; i--) {
340
+ ip = -1;
341
+ if (i >= start) {
342
+ shift = 0;
343
+ for (k = 0; k < groups.length; k++) {
344
+ if (groups[k].ip <= i) { shift += groups[k].text.length; }
345
+ }
346
+ ip = i + shift;
347
+ }
348
+ if (ip >= 0 && runEnd >= 0 && ip === last - 1) {
349
+ last = ip;
350
+ } else {
351
+ if (runEnd >= 0) { story.characters.itemByRange(last, runEnd).remove(); }
352
+ runEnd = ip;
353
+ last = ip;
354
+ }
355
+ }
356
+ /* Absatzformat je neuem Teil */
357
+ for (k = 0; k < groups.length; k++) {
358
+ if (groups[k].style === null) { continue; }
359
+ for (i = 0; i < groups[k].parts.length; i++) {
360
+ first = start + groups[k].parts[i];
361
+ if (first >= start + text.length) { continue; }
362
+ try {
363
+ para = story.characters.item(first).paragraphs.item(0);
364
+ style = para.appliedParagraphStyle;
365
+ if (String(style.name) !== String(groups[k].style.name)) {
366
+ try { para.applyParagraphStyle(groups[k].style, false); } catch (eA) { para.appliedParagraphStyle = groups[k].style; }
367
+ }
368
+ } catch (eP) { /* Absatzformat nicht lesbar: InDesigns Wahl bleibt */ }
369
+ }
370
+ }
371
+ }
372
+
373
+ /* Zeichenformat [None] und keine lokalen Zeichenabweichungen im Bereich [start, start + n). */
374
+ function idcClearCharacterFormat(doc, story, start, n) {
375
+ var range, none;
376
+ if (n < 1) { return; }
377
+ range = story.characters.itemByRange(start, start + n - 1);
378
+ none = idcNoneCharacterStyle(doc);
379
+ if (none !== null) {
380
+ try { range.applyCharacterStyle(none); } catch (e1) { range.appliedCharacterStyle = none; }
381
+ }
382
+ if (typeof OverrideType !== 'undefined') { range.clearOverrides(OverrideType.CHARACTER_ONLY); }
383
+ else { range.clearOverrides(); }
384
+ }
385
+
386
+ IDC.register('set_text_in_range', { needs_document: true, read_only: false, ruler: 'none' },
387
+ function (ctx) {
388
+ var args = ctx.args;
389
+ var doc = ctx.doc;
390
+ var found = idcTextFrameArg(doc, args, 'frame_id');
391
+ var frame = found.item;
392
+ var story = idcStoryOf(frame);
393
+ var start = args.start_index;
394
+ var end = args.end_index;
395
+ var text, length = 0, replaced, after;
396
+ var keepFormat = idcKeepFormatOf(args);
397
+
398
+ if (typeof start !== 'number' || Math.floor(start) !== start || start < 0) {
399
+ IDC.fail('invalid_argument', 'start_index must be a whole number of 0 or more (inclusive).', { start_index: start });
400
+ }
401
+ if (typeof end !== 'number' || Math.floor(end) !== end || end < start) {
402
+ IDC.fail('invalid_argument', 'end_index must be a whole number not smaller than start_index (exclusive).', { start_index: start, end_index: end });
403
+ }
404
+ if (typeof args.text !== 'string') {
405
+ IDC.fail('invalid_argument', 'text is required (a string; "" deletes the range).', null);
406
+ }
407
+ var expansion = idcExpandPlaceholders(idcNormaliseText(args.text), args.placeholders !== false);
408
+ var split = idcSplitBreaks(expansion.text);
409
+ text = split.plain;
410
+ idcPlaceholderWarnings(ctx, expansion);
411
+ try { length = Number(story.characters.length); } catch (e0) { length = 0; }
412
+ if (end > length) {
413
+ IDC.fail('invalid_argument', 'end_index ' + end + ' exceeds the story length ' + length + '.', { end_index: end, story_length: length });
414
+ }
415
+ replaced = end - start;
416
+ idcRejectStoryLocked(story, Number(frame.id), 'set_text_in_range');
417
+
418
+ try {
419
+ if (replaced === 0) {
420
+ if (text.length > 0) { story.insertionPoints.item(start).contents = text; }
421
+ } else if (text.length === 0) {
422
+ story.characters.itemByRange(start, end - 1).remove();
423
+ } else if (keepFormat === 'start') {
424
+ idcReplaceKeepingStart(story, start, end, text);
425
+ } else {
426
+ story.characters.itemByRange(start, end - 1).contents = text;
427
+ }
428
+ } catch (e) {
429
+ IDC.fail('script_error', 'InDesign could not change characters ' + start + '–' + end + ' of frame ' + args.frame_id + ': ' +
430
+ String(e.message ? e.message : e), { frame_id: args.frame_id, start_index: start, end_index: end });
431
+ }
432
+ try { idcInsertBreaks(story, start, split.breaks); } catch (eB) {
433
+ if (eB && eB.isIdcError) { throw eB; }
434
+ ctx.warn('Column/frame/page breaks could not be inserted: ' + String(eB.message ? eB.message : eB));
435
+ }
436
+ text = expansion.text;
437
+ if (keepFormat === 'none' && text.length > 0) {
438
+ try { idcClearCharacterFormat(doc, story, start, text.length); } catch (eN) {
439
+ ctx.warn('The new text was written, but its character formatting could not be cleared: ' + String(eN.message ? eN.message : eN));
440
+ }
441
+ }
442
+ try { after = Number(story.characters.length); } catch (e1) { after = null; }
443
+ if (after !== null && after !== length - replaced + text.length) {
444
+ ctx.warn('The story reads ' + after + ' characters, expected ' + (length - replaced + text.length) + '.');
445
+ }
446
+ ctx.changes = { modified_frame_ids: [Number(frame.id)] };
447
+ return {
448
+ frame_id: Number(frame.id),
449
+ replaced_chars: replaced,
450
+ inserted_chars: text.length,
451
+ story_length: after,
452
+ keep_format_of: keepFormat,
453
+ overset: idcOverflows(frame),
454
+ placeholders_replaced: expansion.replaced,
455
+ placeholders_unknown: expansion.unknown
456
+ };
457
+ });
458
+
459
+ /* ------------------------------------------------------------- find_replace */
460
+
461
+ function idcSetFindOptions(options, args) {
462
+ try { options.includeMasterPages = args.include_master_pages === true; } catch (e1) { /* nicht vorhanden */ }
463
+ try { options.includeHiddenLayers = args.include_hidden_layers === true; } catch (e2) { /* nicht vorhanden */ }
464
+ try { options.includeLockedLayersForFind = args.include_locked_layers === true; } catch (e3) { /* nicht vorhanden */ }
465
+ try { options.includeLockedStoriesForFind = args.include_locked_layers === true; } catch (e4) { /* nicht vorhanden */ }
466
+ try { options.includeFootnotes = true; } catch (e5) { /* nicht vorhanden */ }
467
+ try { options.caseSensitive = args.case_sensitive === true; } catch (e6) { /* GREP kennt das nicht */ }
468
+ try { options.wholeWord = args.whole_word === true; } catch (e7) { /* GREP kennt das nicht */ }
469
+ }
470
+
471
+ /* Fundstelle beschreiben: Rahmen, Seite, Index, Text. */
472
+ function idcDescribeMatch(doc, textObject) {
473
+ var entry = { frame_id: null, page_id: null, page_name: '', index: null, text: null, story_id: null };
474
+ var frames, frame, landing;
475
+ try { entry.index = Number(textObject.index); } catch (e0) { entry.index = null; }
476
+ try { entry.text = String(textObject.contents); } catch (e1) { entry.text = null; }
477
+ try { entry.story_id = Number(textObject.parentStory.id); } catch (e2) { entry.story_id = null; }
478
+ try {
479
+ frames = textObject.parentTextFrames;
480
+ if (frames && frames.length > 0) {
481
+ frame = IDC.concrete(frames[0]);
482
+ entry.frame_id = Number(frame.id);
483
+ landing = IDC.landing(frame);
484
+ entry.page_id = landing.page_id;
485
+ entry.page_name = landing.page_name;
486
+ }
487
+ } catch (e3) { /* Text ohne Rahmen (Uebersatz) */ }
488
+ return entry;
489
+ }
490
+
491
+ /*
492
+ * Sitzung 38 (A-23, A-25 Nr. 2 und 4): Fundstellen nach Story ordnen und
493
+ * gesperrte Stories herausnehmen. Liefert { open: [Fundstellen], stories:
494
+ * [freie Stories in Reihenfolge], skipped: [Sperrangaben mit matches] }.
495
+ */
496
+ function idcSplitMatchesByLock(found) {
497
+ var out = { open: [], stories: [], skipped: [] };
498
+ var seen = {}, i, story, id, info, entry, frames;
499
+ for (i = 0; i < found.length; i++) {
500
+ story = null;
501
+ try { story = found[i].parentStory; } catch (e0) { story = null; }
502
+ id = null;
503
+ try { id = Number(story.id); } catch (e1) { id = null; }
504
+ if (story === null || story === undefined || id === null) { out.open.push(found[i]); continue; }
505
+ if (!IDC.hasOwn(seen, String(id))) {
506
+ info = idcStoryLockInfo(story);
507
+ entry = { story: story, info: info, skipped: null };
508
+ if (info.write_locked) {
509
+ entry.skipped = { story_id: id, frame_id: null, lock_state: info.lock_state, managed_file: info.managed_file, matches: 0 };
510
+ try {
511
+ frames = story.textContainers;
512
+ if (frames && frames.length > 0) { entry.skipped.frame_id = Number(IDC.concrete(frames[0]).id); }
513
+ } catch (e2) { /* ohne Rahmen */ }
514
+ out.skipped.push(entry.skipped);
515
+ } else {
516
+ out.stories.push(story);
517
+ }
518
+ seen[String(id)] = entry;
519
+ }
520
+ entry = seen[String(id)];
521
+ if (entry.skipped !== null) { entry.skipped.matches += 1; } else { out.open.push(found[i]); }
522
+ }
523
+ return out;
524
+ }
525
+
526
+ /* Rahmen-IDs der geaenderten Stellen (A-25 Nr. 2), jede einmal, in Fundreihenfolge. */
527
+ function idcFrameIdsOfMatches(found) {
528
+ var ids = [], seen = {}, i, frames, id;
529
+ for (i = 0; i < found.length; i++) {
530
+ try {
531
+ frames = found[i].parentTextFrames;
532
+ if (frames && frames.length > 0) {
533
+ id = Number(IDC.concrete(frames[0]).id);
534
+ if (!IDC.hasOwn(seen, String(id))) { seen[String(id)] = true; ids.push(id); }
535
+ }
536
+ } catch (e) { /* Uebersatz: kein Rahmen */ }
537
+ }
538
+ return ids;
539
+ }
540
+
541
+ /*
542
+ * Ersetzen in einer freien Story: Story.changeText/changeGrep, sonst ueber
543
+ * ihren Text (texts.item(0)) wie im Rahmenbereich. Live zu pruefen (Testplan
544
+ * Sitzung 38): nur dieser Weg laeuft, wenn gesperrte Fundstellen dabei sind.
545
+ */
546
+ function idcChangeInStory(story, grep) {
547
+ var text = null;
548
+ if (typeof story.changeText === 'function') { return grep ? story.changeGrep() : story.changeText(); }
549
+ try { text = story.texts.item(0); } catch (e0) { text = null; }
550
+ if (text === null || text === undefined) {
551
+ IDC.fail('script_error', 'This story offers no find/change.', null);
552
+ }
553
+ return grep ? text.changeGrep() : text.changeText();
554
+ }
555
+
556
+ IDC.register('find_replace', { needs_document: true, read_only: false, ruler: 'none' },
557
+ function (ctx) {
558
+ var args = ctx.args;
559
+ var doc = ctx.doc;
560
+ var mode = args.mode === undefined || args.mode === null ? 'literal' : args.mode;
561
+ var scope = args.scope === undefined || args.scope === null ? 'document' : args.scope;
562
+ var dryRun = args.dry_run === true;
563
+ var maxMatches = 200;
564
+ var target, frame, found, i, matches = [], count = 0, prefsFind, prefsChange, options;
565
+ var split = { open: [], stories: [], skipped: [] }, changed = [], part, j, skippedMatches = 0, lockedInScope = [], unlisted = 0, frameLock;
566
+
567
+ if (typeof args.find !== 'string' || args.find === '') {
568
+ IDC.fail('invalid_argument', 'find is required and must not be empty.', null);
569
+ }
570
+ if (!dryRun && typeof args.replace !== 'string') {
571
+ IDC.fail('invalid_argument', 'replace is required (a string; "" deletes the matches). Use dry_run true to only count.', null);
572
+ }
573
+ if (mode !== 'literal' && mode !== 'grep') {
574
+ IDC.fail('invalid_argument', 'mode must be literal or grep.', { mode: mode });
575
+ }
576
+ if (scope !== 'document' && scope !== 'frame') {
577
+ IDC.fail('invalid_argument', 'scope must be document or frame.', { scope: scope });
578
+ }
579
+ if (scope === 'frame') {
580
+ frame = idcTextFrameArg(doc, args, 'frame_id').item;
581
+ try { target = frame.texts.item(0); } catch (e0) { target = null; }
582
+ if (target === null || target === undefined) { target = idcStoryOf(frame); }
583
+ } else {
584
+ target = doc;
585
+ }
586
+
587
+ if (mode === 'grep') {
588
+ prefsFind = 'findGrepPreferences';
589
+ prefsChange = 'changeGrepPreferences';
590
+ options = app.findChangeGrepOptions;
591
+ } else {
592
+ prefsFind = 'findTextPreferences';
593
+ prefsChange = 'changeTextPreferences';
594
+ options = app.findChangeTextOptions;
595
+ }
596
+
597
+ try {
598
+ app[prefsFind] = NothingEnum.NOTHING;
599
+ app[prefsChange] = NothingEnum.NOTHING;
600
+ idcSetFindOptions(options, args);
601
+ app[prefsFind].findWhat = args.find;
602
+ if (!dryRun) { app[prefsChange].changeTo = args.replace; }
603
+
604
+ /*
605
+ * Erst suchen, dann nur in freien Stories ersetzen: gesperrte Stories
606
+ * (A-23) aendert InDesign im Layout, die Redaktionsdatei bliebe alt –
607
+ * oder es ersetzt still nichts (A-25 Nr. 4). Ohne gesperrte Fundstellen
608
+ * bleibt der Lauf wie bisher ein einziger changeText/changeGrep.
609
+ */
610
+ found = mode === 'grep' ? target.findGrep() : target.findText();
611
+ split = idcSplitMatchesByLock(found);
612
+ if (dryRun) {
613
+ count = split.open.length;
614
+ for (i = 0; i < split.open.length && i < maxMatches; i++) { matches.push(idcDescribeMatch(doc, split.open[i])); }
615
+ if (split.open.length > maxMatches) { ctx.warn('Only the first ' + maxMatches + ' of ' + split.open.length + ' matches are listed.'); }
616
+ } else if (split.skipped.length === 0) {
617
+ changed = mode === 'grep' ? target.changeGrep() : target.changeText();
618
+ count = changed.length;
619
+ } else {
620
+ for (i = 0; i < split.stories.length; i++) {
621
+ part = idcChangeInStory(split.stories[i], mode === 'grep');
622
+ for (j = 0; j < part.length; j++) { changed.push(part[j]); }
623
+ }
624
+ count = changed.length;
625
+ }
626
+ } catch (e) {
627
+ IDC.fail('script_error', 'InDesign could not run find/change (' + mode + '): ' + String(e.message ? e.message : e),
628
+ { find: args.find, mode: mode, scope: scope });
629
+ } finally {
630
+ try { app[prefsFind] = NothingEnum.NOTHING; } catch (er1) { ctx.warn('Could not reset the find preferences.'); }
631
+ try { app[prefsChange] = NothingEnum.NOTHING; } catch (er2) { ctx.warn('Could not reset the change preferences.'); }
632
+ }
633
+
634
+ if (mode === 'literal' && / /.test(args.find)) {
635
+ 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
+ }
637
+ for (i = 0; i < split.skipped.length; i++) { skippedMatches += split.skipped[i].matches; }
638
+ if (split.skipped.length > 0) {
639
+ ctx.warn(skippedMatches + ' match(es) in ' + split.skipped.length + ' locked story/stories were ' + (dryRun ? 'left out of the count' : 'not replaced') +
640
+ ' (see skipped_locked). Check the story out in the editorial system or in InCopy first.');
641
+ }
642
+ if (args.include_locked_layers !== true) {
643
+ /* Ohne include_locked_layers sucht InDesign gesperrte Stories nicht ab: darauf hinweisen. */
644
+ if (scope === 'frame') {
645
+ frameLock = idcStoryLockInfo(idcStoryOf(frame));
646
+ if (frameLock.write_locked) { lockedInScope.push(frameLock); }
647
+ } else {
648
+ lockedInScope = idcWriteLockedStories(doc);
649
+ }
650
+ for (i = 0; i < lockedInScope.length; i++) {
651
+ unlisted += 1;
652
+ for (j = 0; j < split.skipped.length; j++) {
653
+ if (split.skipped[j].story_id === lockedInScope[i].story_id) { unlisted -= 1; break; }
654
+ }
655
+ }
656
+ if (unlisted > 0) {
657
+ ctx.warn(unlisted + ' locked story/stories ' + (scope === 'frame' ? '(this frame) ' : '') + 'were not searched and are never changed by find_replace; include_locked_layers true lists their matches in skipped_locked.');
658
+ }
659
+ }
660
+ if (!dryRun && count > 0) { ctx.changes = { replacements: count, modified_frame_ids: scope === 'frame' ? [Number(frame.id)] : idcFrameIdsOfMatches(changed) }; }
661
+ var out = {
662
+ replacements: dryRun ? 0 : count,
663
+ matches_found: count,
664
+ dry_run: dryRun,
665
+ scope: scope,
666
+ mode: mode,
667
+ frame_id: scope === 'frame' ? Number(frame.id) : null,
668
+ master_pages_included: args.include_master_pages === true,
669
+ hidden_layers_included: args.include_hidden_layers === true,
670
+ locked_layers_included: args.include_locked_layers === true,
671
+ skipped_locked: split.skipped
672
+ };
673
+ if (dryRun) { out.matches = matches; }
674
+ return out;
675
+ });