layoutbridge-light 0.0.0-stage → 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.
- package/CHANGELOG.md +105 -0
- package/LICENSE +26 -0
- package/README.de.md +367 -0
- package/README.md +353 -2
- package/THIRD-PARTY-NOTICES.md +2878 -0
- package/dist/src/mcp/catalog.js +131 -0
- package/dist/src/mcp/config.js +211 -0
- package/dist/src/mcp/content-credentials.js +65 -0
- package/dist/src/mcp/dispatch.js +171 -0
- package/dist/src/mcp/epub-check.js +331 -0
- package/dist/src/mcp/epub-orientation.js +132 -0
- package/dist/src/mcp/license/free-tools.js +16 -0
- package/dist/src/mcp/license/gate.js +210 -0
- package/dist/src/mcp/license/index.js +107 -0
- package/dist/src/mcp/license/instance-name.js +57 -0
- package/dist/src/mcp/license/key-source.js +59 -0
- package/dist/src/mcp/license/lemonsqueezy-verifier.js +207 -0
- package/dist/src/mcp/license/placeholder-verifier.js +10 -0
- package/dist/src/mcp/license/polar-ids.js +100 -0
- package/dist/src/mcp/license/polar-verifier.js +285 -0
- package/dist/src/mcp/license/product-ids.js +73 -0
- package/dist/src/mcp/license/state-store.js +91 -0
- package/dist/src/mcp/license/types.js +11 -0
- package/dist/src/mcp/logging.js +271 -0
- package/dist/src/mcp/pdf-postprocess.js +798 -0
- package/dist/src/mcp/product-info.js +54 -0
- package/dist/src/mcp/product.js +56 -0
- package/dist/src/mcp/server.js +253 -0
- package/dist/src/mcp/setup.js +680 -0
- package/dist/src/mcp/start.cjs +117 -0
- package/dist/src/mcp/target-folder.js +25 -0
- package/dist/src/mcp/tool-definition.js +2 -0
- package/dist/src/mcp/tools.js +2680 -0
- package/dist/src/mcp/typography-rules.js +70 -0
- package/dist/src/transport/com.js +436 -0
- package/dist/src/transport/osascript.js +171 -0
- package/dist/src/transport/queue.js +19 -0
- package/dist/src/transport/script-bundle.js +92 -0
- package/dist/src/transport/transport.js +52 -0
- package/indesign/commands/10-app.js +6 -0
- package/indesign/commands/20-documents.js +517 -0
- package/indesign/commands/22-document-setup.js +204 -0
- package/indesign/commands/23-save-package.js +258 -0
- package/indesign/commands/25-export.js +695 -0
- package/indesign/commands/26-export-image.js +202 -0
- package/indesign/commands/27-textdefaults.js +151 -0
- package/indesign/commands/28-selection.js +174 -0
- package/indesign/commands/30-pages.js +1032 -0
- package/indesign/commands/31-view.js +310 -0
- package/indesign/commands/32-sections.js +203 -0
- package/indesign/commands/35-parents.js +448 -0
- package/indesign/commands/37-layout.js +837 -0
- package/indesign/commands/40-frames.js +886 -0
- package/indesign/commands/41-shapes.js +505 -0
- package/indesign/commands/42-groups.js +117 -0
- package/indesign/commands/43-layers.js +310 -0
- package/indesign/commands/44-effects.js +274 -0
- package/indesign/commands/45-frame-options.js +247 -0
- package/indesign/commands/47-anchored.js +473 -0
- package/indesign/commands/50-modify.js +803 -0
- package/indesign/commands/52-align.js +139 -0
- package/indesign/commands/55-fit.js +106 -0
- package/indesign/commands/60-images.js +1154 -0
- package/indesign/commands/62-embed.js +120 -0
- package/indesign/commands/69-placeholders.js +132 -0
- package/indesign/commands/70-text.js +710 -0
- package/indesign/commands/71-outlines.js +92 -0
- package/indesign/commands/72-place-text.js +476 -0
- package/indesign/commands/73-story-lock.js +157 -0
- package/indesign/commands/74-stories.js +121 -0
- package/indesign/commands/75-tables.js +702 -0
- package/indesign/commands/76-text-variables.js +260 -0
- package/indesign/commands/77-story-options.js +86 -0
- package/indesign/commands/78-frame-lines.js +511 -0
- package/indesign/commands/79-check-typography.js +480 -0
- package/indesign/commands/80-styles.js +1960 -0
- package/indesign/commands/81-composition.js +355 -0
- package/indesign/commands/82-object-styles.js +263 -0
- package/indesign/commands/83-drop-caps.js +87 -0
- package/indesign/commands/83-indents-tabs.js +264 -0
- package/indesign/commands/84-import-styles.js +142 -0
- package/indesign/commands/85-swatches.js +112 -0
- package/indesign/commands/86-gradients.js +185 -0
- package/indesign/commands/87-replace-swatch.js +180 -0
- package/indesign/commands/88-replace-font.js +212 -0
- package/indesign/commands/89-toc-index.js +1002 -0
- package/indesign/commands/90-print-on-demand.js +495 -0
- package/indesign/commands/91-epub.js +629 -0
- package/indesign/commands/92-accessibility.js +1131 -0
- package/indesign/interpreter.js +829 -0
- package/package.json +41 -4
- package/rules/typography-rules.json +32 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { PRODUCT } from './product.js';
|
|
2
|
+
import { TOOLS } from './tools.js';
|
|
3
|
+
/** Die 85 Werkzeuge von LayoutBridge Light (Wahl des Anbieters 30.10.2026, a; dazu set_anchored_object_options, Wahl des Anbieters 11.11.2026, Rueckfrage 2 b; dazu die sieben Werkzeuge fuer barrierefreie PDFs, A-71, Entscheidung des Anbieters 06.11.2026), in der Reihenfolge des Katalogs. */
|
|
4
|
+
export const LIGHT_TOOLS = [
|
|
5
|
+
'get_app_version', 'list_documents', 'open_document', 'close_document', 'create_document', 'save_document',
|
|
6
|
+
'export_pdf', 'preflight_document', 'document_text_defaults', 'set_document_bleed',
|
|
7
|
+
'list_pages', 'get_page_state', 'add_page', 'delete_page', 'list_parent_pages', 'create_parent_page',
|
|
8
|
+
'apply_parent_to_page', 'insert_page_number_marker',
|
|
9
|
+
'get_layout_grid', 'set_layout_grid',
|
|
10
|
+
'create_text_frame', 'create_image_frame', 'create_rectangle', 'delete_frame',
|
|
11
|
+
'set_frame_bounds', 'set_frame_appearance', 'set_frame_columns', 'set_frame_inset', 'set_text_wrap', 'thread_text_frames',
|
|
12
|
+
'place_image', 'list_links', 'update_links',
|
|
13
|
+
'get_frame_text', 'set_text', 'set_text_in_range', 'find_replace',
|
|
14
|
+
'list_paragraph_styles', 'define_paragraph_style', 'define_character_style', 'apply_paragraph_style_to_range', 'apply_character_style_to_range', 'apply_character_format_to_range',
|
|
15
|
+
'set_paragraph_options', 'get_font_info_at_index', 'find_font_usage', 'debug_inspect_run', 'create_swatch',
|
|
16
|
+
'get_frame_options', 'list_swatches', 'export_page_image', 'place_text_file', 'fit_frame',
|
|
17
|
+
'list_stories', 'anchor_frame_in_text', 'set_anchored_object_options', 'list_sections', 'set_section',
|
|
18
|
+
'set_document_setup', 'list_text_variables', 'create_text_variable', 'insert_text_variable',
|
|
19
|
+
'save_document_as', 'set_story_options', 'get_frame_lines', 'check_typography',
|
|
20
|
+
'position_image_in_frame', 'list_toc_styles', 'define_toc_style', 'create_toc', 'add_index_entry', 'list_index', 'generate_index',
|
|
21
|
+
'check_print_on_demand', 'create_book_cover', 'export_epub', 'check_epub', 'autoflow_story',
|
|
22
|
+
/* A-71 (Sitzung 82): barrierefreie PDFs, in beiden Produkten */
|
|
23
|
+
'list_alt_texts', 'set_alt_text', 'list_articles', 'define_article', 'delete_article', 'check_accessibility', 'export_interactive_pdf',
|
|
24
|
+
];
|
|
25
|
+
/** Die 47 Werkzeuge, die Light ausdruecklich nicht hat. */
|
|
26
|
+
export const LIGHT_EXCLUDED = [
|
|
27
|
+
/* Gestaltung */
|
|
28
|
+
'create_ellipse', 'create_polygon', 'set_corner_options', 'create_gradient_swatch', 'set_frame_effects',
|
|
29
|
+
'convert_text_to_outlines',
|
|
30
|
+
/* Fremddokumente und Verknuepfungen */
|
|
31
|
+
'replace_font', 'replace_swatch', 'embed_link', 'unembed_link', 'delete_link', 'list_pasteboard_items',
|
|
32
|
+
/* Ebenen, Gruppen, Objektformate */
|
|
33
|
+
'list_layers', 'set_frame_layer', 'set_layer_visibility', 'group_frames', 'ungroup_frame',
|
|
34
|
+
'list_object_styles', 'apply_object_style',
|
|
35
|
+
/* Heftlayout */
|
|
36
|
+
'move_pages', 'move_pages_to_new_spread', 'move_frame_to_page', 'set_frame_rotation', 'set_frame_stacking',
|
|
37
|
+
'align_objects', 'list_guides', 'delete_guides', 'clear_frame_content', 'get_selection', 'set_selection',
|
|
38
|
+
/* Tabellen */
|
|
39
|
+
'create_table', 'set_table_cells', 'set_table_style', 'get_table',
|
|
40
|
+
/* Im Buchsatz mit Umweg erledigt (Wahl des Anbieters 30.10.2026, a) */
|
|
41
|
+
'duplicate_parent_page', 'override_parent_item_on_page', 'show_page', 'duplicate_frame', 'create_line',
|
|
42
|
+
'unanchor_frame', 'import_styles', 'package_document', 'relink_links', 'get_image_link_info',
|
|
43
|
+
'scale_image_in_frame', 'apply_paragraph_style',
|
|
44
|
+
/* Sitzung 81 (A-68): nur im Vollprodukt (Wahl des Anbieters 11.11.2026, Rueckfrage 2 b) */
|
|
45
|
+
'set_view_options',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Gezielte Ersetzungen fuer Light: Halbsaetze, die auf ein fehlendes
|
|
49
|
+
* Werkzeug verweisen, werden gekuerzt, der Rest des Satzes bleibt. Jede
|
|
50
|
+
* Ersetzung muss im Katalog genau ihren Text finden (Waechtertest) – aendert
|
|
51
|
+
* sich eine Beschreibung des Vollprodukts, faellt das auf.
|
|
52
|
+
*/
|
|
53
|
+
export const LIGHT_TEXT_REPLACEMENTS = [
|
|
54
|
+
[' (Ebene; see list_layers for locked or hidden layers)', ' (Ebene)'],
|
|
55
|
+
['; repair with move_pages_to_new_spread)', ')'],
|
|
56
|
+
[' (see list_swatches, create_gradient_swatch)', ' (see list_swatches)'],
|
|
57
|
+
['Existing layer to create the object on (see list_layers).', 'Existing layer to create the object on.'],
|
|
58
|
+
[' Per-corner settings: set_corner_options.', ''],
|
|
59
|
+
[' – to put it on another page use move_frame_to_page', ''],
|
|
60
|
+
[' (their graphics would stay behind) – use move_frame_to_page.', ' (their graphics would stay behind).'],
|
|
61
|
+
[' in its rotated space; set set_frame_rotation 0 first.', ' in its rotated space.'],
|
|
62
|
+
["(print: the document's CMYK swatches, see list_swatches, create_gradient_swatch)", "(print: the document's CMYK swatches, see list_swatches)"],
|
|
63
|
+
[' – delete_link removes it, list_pasteboard_items lists all pasteboard objects.', '.'],
|
|
64
|
+
[' – it does not print, delete_link cannot reach it, and a warning names', ' – it does not print, and a warning names'],
|
|
65
|
+
['exact name from list_layers.', 'exact name of an existing layer.'],
|
|
66
|
+
[', or call show_page.', '.'],
|
|
67
|
+
[' to stay on the master, or call show_page with one of the page_ids to work on it visibly.', ' to stay on the master.'],
|
|
68
|
+
[' to switch it off, or build borders from four create_line calls.', ' to switch it off.'],
|
|
69
|
+
[' – use scale_image_in_frame or fit_frame when', ' – use fit_frame when'],
|
|
70
|
+
[' – to merely swap the file use relink_links)', ')'],
|
|
71
|
+
['check get_image_link_info before repeating.', 'check get_page_state before repeating.'],
|
|
72
|
+
['To change the size use scale_image_in_frame or fit_frame.', 'To change the size use fit_frame.'],
|
|
73
|
+
['(create_line, create_rectangle, set_frame_appearance,', '(create_rectangle, set_frame_appearance,'],
|
|
74
|
+
];
|
|
75
|
+
/** Einen Text in Saetze teilen (Satzende . ! ? gefolgt von Leerraum). */
|
|
76
|
+
function sentences(text) {
|
|
77
|
+
return text.split(/(?<=[.!?])\s+/);
|
|
78
|
+
}
|
|
79
|
+
export function mentionsAny(text, names) {
|
|
80
|
+
return names.some((name) => new RegExp('(^|[^A-Za-z0-9_])' + name + '([^A-Za-z0-9_]|$)').test(text));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Verweise auf fehlende Werkzeuge entfernen: zuerst die gezielten
|
|
84
|
+
* Ersetzungen, danach als Rueckfall ganze Saetze, die noch eines nennen.
|
|
85
|
+
* Bleibt nichts uebrig, gilt der Originaltext (dann schlaegt der
|
|
86
|
+
* Waechtertest an).
|
|
87
|
+
*/
|
|
88
|
+
export function withoutMentions(text, names) {
|
|
89
|
+
if (!mentionsAny(text, names))
|
|
90
|
+
return text;
|
|
91
|
+
let result = text;
|
|
92
|
+
for (const [from, to] of LIGHT_TEXT_REPLACEMENTS)
|
|
93
|
+
result = result.split(from).join(to);
|
|
94
|
+
if (!mentionsAny(result, names))
|
|
95
|
+
return result;
|
|
96
|
+
const kept = sentences(result).filter((sentence) => !mentionsAny(sentence, names));
|
|
97
|
+
return kept.length === 0 ? text : kept.join(' ');
|
|
98
|
+
}
|
|
99
|
+
/** Ein Parameterschema mit neuer Beschreibung; das Original bleibt unberuehrt. */
|
|
100
|
+
function describedAs(schema, description) {
|
|
101
|
+
return schema.describe(description);
|
|
102
|
+
}
|
|
103
|
+
/** Eine Kopie der Definition, deren Texte keines der fehlenden Werkzeuge nennen. */
|
|
104
|
+
export function lightDefinition(definition, missing = LIGHT_EXCLUDED) {
|
|
105
|
+
let shapeChanged = false;
|
|
106
|
+
const shape = {};
|
|
107
|
+
for (const [key, schema] of Object.entries(definition.inputShape)) {
|
|
108
|
+
const current = schema.description;
|
|
109
|
+
if (typeof current === 'string' && mentionsAny(current, missing)) {
|
|
110
|
+
shape[key] = describedAs(schema, withoutMentions(current, missing));
|
|
111
|
+
shapeChanged = true;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
shape[key] = schema;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const description = withoutMentions(definition.description, missing);
|
|
118
|
+
if (!shapeChanged && description === definition.description)
|
|
119
|
+
return definition;
|
|
120
|
+
return { ...definition, description, inputShape: shapeChanged ? shape : definition.inputShape };
|
|
121
|
+
}
|
|
122
|
+
/** Der Katalog eines Produkts. */
|
|
123
|
+
export function catalogFor(product = PRODUCT, tools = TOOLS) {
|
|
124
|
+
if (product.id !== 'light')
|
|
125
|
+
return [...tools];
|
|
126
|
+
const allowed = new Set(LIGHT_TOOLS);
|
|
127
|
+
return tools.filter((tool) => allowed.has(tool.name)).map((tool) => lightDefinition(tool));
|
|
128
|
+
}
|
|
129
|
+
/** Der Katalog dieses Pakets. */
|
|
130
|
+
export const CATALOG = catalogFor();
|
|
131
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serverkonfiguration (E-54): Hauswerte, die nicht in die Werkzeug-
|
|
3
|
+
* beschreibungen gehoeren – Toleranzen, Preset-Namen, Schwellen. Eine
|
|
4
|
+
* JSON-Datei, einmal beim Start gelesen. Ein Parameter im Aufruf geht
|
|
5
|
+
* immer vor einem konfigurierten Wert.
|
|
6
|
+
*
|
|
7
|
+
* Ort der Datei: Umgebungsvariable LAYOUTBRIDGE_SPM_CONFIG (voller Pfad),
|
|
8
|
+
* sonst macOS ~/Library/Application Support/layoutbridge-spm/config.json,
|
|
9
|
+
* sonst ~/.layoutbridge-spm/config.json. Fehlt die Datei, gilt keine
|
|
10
|
+
* Konfiguration – der Server laeuft mit den neutralen Vorgaben.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { homedir, platform } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { envName, PRODUCT } from './product.js';
|
|
16
|
+
const KNOWN_KEYS = ['tracking_warn_beyond', 'min_ppi', 'export_pdf', 'print_on_demand', 'epub'];
|
|
17
|
+
const POD_NUMBER_KEYS = [
|
|
18
|
+
['min_page_count', 1], ['max_page_count', 1], ['page_count_multiple', 1], ['min_outside_margin_mm', 0], ['bleed_mm', 0],
|
|
19
|
+
['min_font_size_pt', 0], ['spine_per_page_mm', 0], ['safety_margin_mm', 0], ['spine_safety_mm', 0],
|
|
20
|
+
];
|
|
21
|
+
const KNOWN_POD_KEYS = [...POD_NUMBER_KEYS.map(([key]) => key), 'inside_margin_tiers', 'barcode_area'];
|
|
22
|
+
const BARCODE_KEYS = ['width_mm', 'height_mm', 'offset_x_mm', 'offset_y_mm'];
|
|
23
|
+
const KNOWN_PDF_KEYS = ['preset', 'fallback_preset', 'fallback_over_bytes'];
|
|
24
|
+
export function defaultConfigPath(env = process.env, os = platform(), home = homedir(), product = PRODUCT) {
|
|
25
|
+
const fromEnv = env[envName('CONFIG', product)];
|
|
26
|
+
if (fromEnv !== undefined && fromEnv.length > 0)
|
|
27
|
+
return fromEnv;
|
|
28
|
+
if (os === 'darwin') {
|
|
29
|
+
return join(home, 'Library', 'Application Support', product.dataDirName, 'config.json');
|
|
30
|
+
}
|
|
31
|
+
return join(home, '.' + product.dataDirName, 'config.json');
|
|
32
|
+
}
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function takeNumber(source, key, min, problems, prefix) {
|
|
37
|
+
const value = source[key];
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < min) {
|
|
41
|
+
problems.push(`${prefix}${key}: expected a number of ${min} or more, got ${JSON.stringify(value)}`);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function takeString(source, key, problems, prefix) {
|
|
47
|
+
const value = source[key];
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (typeof value !== 'string') {
|
|
51
|
+
problems.push(`${prefix}${key}: expected a string, got ${JSON.stringify(value)}`);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/** Text einer Konfigurationsdatei pruefen. Unbekannte Schluessel werden gemeldet und ignoriert. */
|
|
57
|
+
export function parseServerConfig(text) {
|
|
58
|
+
const problems = [];
|
|
59
|
+
let raw;
|
|
60
|
+
try {
|
|
61
|
+
raw = JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return { config: {}, problems: ['not valid JSON: ' + String(error)] };
|
|
65
|
+
}
|
|
66
|
+
if (!isRecord(raw)) {
|
|
67
|
+
return { config: {}, problems: ['the top level must be a JSON object'] };
|
|
68
|
+
}
|
|
69
|
+
const config = {};
|
|
70
|
+
for (const key of Object.keys(raw)) {
|
|
71
|
+
if (!KNOWN_KEYS.includes(key))
|
|
72
|
+
problems.push(`unknown key "${key}" ignored`);
|
|
73
|
+
}
|
|
74
|
+
const tracking = takeNumber(raw, 'tracking_warn_beyond', 0, problems, '');
|
|
75
|
+
if (tracking !== undefined)
|
|
76
|
+
config.tracking_warn_beyond = tracking;
|
|
77
|
+
const minPpi = takeNumber(raw, 'min_ppi', 1, problems, '');
|
|
78
|
+
if (minPpi !== undefined)
|
|
79
|
+
config.min_ppi = minPpi;
|
|
80
|
+
if (raw['export_pdf'] !== undefined) {
|
|
81
|
+
const pdf = raw['export_pdf'];
|
|
82
|
+
if (!isRecord(pdf)) {
|
|
83
|
+
problems.push('export_pdf: expected an object');
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
for (const key of Object.keys(pdf)) {
|
|
87
|
+
if (!KNOWN_PDF_KEYS.includes(key))
|
|
88
|
+
problems.push(`unknown key "export_pdf.${key}" ignored`);
|
|
89
|
+
}
|
|
90
|
+
const out = {};
|
|
91
|
+
const preset = takeString(pdf, 'preset', problems, 'export_pdf.');
|
|
92
|
+
if (preset !== undefined)
|
|
93
|
+
out.preset = preset;
|
|
94
|
+
const fallback = takeString(pdf, 'fallback_preset', problems, 'export_pdf.');
|
|
95
|
+
if (fallback !== undefined)
|
|
96
|
+
out.fallback_preset = fallback;
|
|
97
|
+
const bytes = takeNumber(pdf, 'fallback_over_bytes', 0, problems, 'export_pdf.');
|
|
98
|
+
if (bytes !== undefined)
|
|
99
|
+
out.fallback_over_bytes = bytes;
|
|
100
|
+
config.export_pdf = out;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (raw['print_on_demand'] !== undefined) {
|
|
104
|
+
const pod = parsePrintOnDemand(raw['print_on_demand'], problems);
|
|
105
|
+
if (pod !== undefined)
|
|
106
|
+
config.print_on_demand = pod;
|
|
107
|
+
}
|
|
108
|
+
if (raw['epub'] !== undefined) {
|
|
109
|
+
const epub = raw['epub'];
|
|
110
|
+
if (!isRecord(epub)) {
|
|
111
|
+
problems.push('epub: expected an object');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
for (const key of Object.keys(epub)) {
|
|
115
|
+
if (key !== 'min_ppi')
|
|
116
|
+
problems.push(`unknown key "epub.${key}" ignored`);
|
|
117
|
+
}
|
|
118
|
+
const minPpi = takeNumber(epub, 'min_ppi', 1, problems, 'epub.');
|
|
119
|
+
config.epub = minPpi === undefined ? {} : { min_ppi: minPpi };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { config, problems };
|
|
123
|
+
}
|
|
124
|
+
function parsePrintOnDemand(value, problems) {
|
|
125
|
+
const prefix = 'print_on_demand.';
|
|
126
|
+
if (!isRecord(value)) {
|
|
127
|
+
problems.push('print_on_demand: expected an object');
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
for (const key of Object.keys(value)) {
|
|
131
|
+
if (!KNOWN_POD_KEYS.includes(key))
|
|
132
|
+
problems.push(`unknown key "${prefix}${key}" ignored`);
|
|
133
|
+
}
|
|
134
|
+
const out = {};
|
|
135
|
+
for (const [key, min] of POD_NUMBER_KEYS) {
|
|
136
|
+
const n = takeNumber(value, key, min, problems, prefix);
|
|
137
|
+
if (n !== undefined)
|
|
138
|
+
out[key] = n;
|
|
139
|
+
}
|
|
140
|
+
if (value['inside_margin_tiers'] !== undefined) {
|
|
141
|
+
const tiers = value['inside_margin_tiers'];
|
|
142
|
+
const parsed = [];
|
|
143
|
+
if (!Array.isArray(tiers)) {
|
|
144
|
+
problems.push(`${prefix}inside_margin_tiers: expected an array of {max_pages, min_mm}`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
tiers.forEach((tier, index) => {
|
|
148
|
+
const at = `${prefix}inside_margin_tiers[${index}].`;
|
|
149
|
+
if (!isRecord(tier)) {
|
|
150
|
+
problems.push(`${at.slice(0, -1)}: expected an object {max_pages, min_mm}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const maxPages = takeNumber(tier, 'max_pages', 1, problems, at);
|
|
154
|
+
const minMm = takeNumber(tier, 'min_mm', 0, problems, at);
|
|
155
|
+
if (maxPages !== undefined && minMm !== undefined)
|
|
156
|
+
parsed.push({ max_pages: maxPages, min_mm: minMm });
|
|
157
|
+
else if (tier['max_pages'] === undefined || tier['min_mm'] === undefined)
|
|
158
|
+
problems.push(`${at}: max_pages and min_mm are both required`);
|
|
159
|
+
});
|
|
160
|
+
if (parsed.length > 0)
|
|
161
|
+
out.inside_margin_tiers = parsed;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (value['barcode_area'] !== undefined) {
|
|
165
|
+
const area = value['barcode_area'];
|
|
166
|
+
if (!isRecord(area)) {
|
|
167
|
+
problems.push(`${prefix}barcode_area: expected an object {width_mm, height_mm, offset_x_mm, offset_y_mm}`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const parsed = {};
|
|
171
|
+
for (const key of BARCODE_KEYS) {
|
|
172
|
+
const n = takeNumber(area, key, 0, problems, `${prefix}barcode_area.`);
|
|
173
|
+
if (n !== undefined)
|
|
174
|
+
parsed[key] = n;
|
|
175
|
+
}
|
|
176
|
+
if (BARCODE_KEYS.every((key) => parsed[key] !== undefined))
|
|
177
|
+
out.barcode_area = parsed;
|
|
178
|
+
else
|
|
179
|
+
problems.push(`${prefix}barcode_area: width_mm, height_mm, offset_x_mm and offset_y_mm are all required`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
/** Konfigurationsdatei lesen. Keine Datei = leere Konfiguration, kein Fehler. */
|
|
185
|
+
export function loadServerConfig(path = defaultConfigPath()) {
|
|
186
|
+
let text;
|
|
187
|
+
try {
|
|
188
|
+
text = readFileSync(path, 'utf8');
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const code = error.code;
|
|
192
|
+
if (code === 'ENOENT')
|
|
193
|
+
return { config: {}, path: null, problems: [] };
|
|
194
|
+
return { config: {}, path, problems: ['could not read ' + path + ': ' + String(error)] };
|
|
195
|
+
}
|
|
196
|
+
const parsed = parseServerConfig(text);
|
|
197
|
+
return { config: parsed.config, path, problems: parsed.problems };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Konfigurierte Vorgaben in die Befehlsargumente legen – nur dort, wo der
|
|
201
|
+
* Aufruf selbst nichts gesagt hat. Felder ohne Wert werden nicht gesetzt.
|
|
202
|
+
*/
|
|
203
|
+
export function applyConfigDefaults(args, defaults) {
|
|
204
|
+
const out = { ...args };
|
|
205
|
+
for (const key of Object.keys(defaults)) {
|
|
206
|
+
if (out[key] === undefined && defaults[key] !== undefined)
|
|
207
|
+
out[key] = defaults[key];
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A-76 (Sitzung 80): Verweis auf ein entferntes Inhaltsnachweis-Manifest
|
|
3
|
+
* (Content Credentials, C2PA) im XMP einer Bilddatei erkennen.
|
|
4
|
+
*
|
|
5
|
+
* Anlass A-75 (10.11.2026, InDesign 21.6.0.57, Mac): Ein Bild mit
|
|
6
|
+
* `dcterms:provenance` auf ein Manifest im Netz liess InDesign beim Export
|
|
7
|
+
* (JPEG, PDF) minutenlang im Modul fuer Inhaltsnachweise haengen; ohne XMP
|
|
8
|
+
* (Pixel gleich) exportierte dasselbe Dokument in Sekunden. place_image
|
|
9
|
+
* warnt deshalb; die Datei wird nur gelesen, nie veraendert.
|
|
10
|
+
*/
|
|
11
|
+
import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
|
|
12
|
+
/** So viel wird vom Anfang und vom Ende der Datei gelesen (XMP steht dort). */
|
|
13
|
+
const HEAD_BYTES = 4 * 1024 * 1024;
|
|
14
|
+
const TAIL_BYTES = 1024 * 1024;
|
|
15
|
+
const PATTERNS = [
|
|
16
|
+
/dcterms:provenance\s*=\s*["'](https:\/\/[^"'\s<>]+)["']/i,
|
|
17
|
+
/<dcterms:provenance[^>]*>\s*(https:\/\/[^<\s]+)\s*</i,
|
|
18
|
+
];
|
|
19
|
+
/** URL des entfernten Manifests im Text, sonst null. */
|
|
20
|
+
export function findRemoteManifest(text) {
|
|
21
|
+
for (const pattern of PATTERNS) {
|
|
22
|
+
const match = pattern.exec(text);
|
|
23
|
+
if (match !== null && match[1] !== undefined)
|
|
24
|
+
return match[1];
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function readSlice(fd, start, length) {
|
|
29
|
+
const buffer = Buffer.alloc(length);
|
|
30
|
+
const read = readSync(fd, buffer, 0, length, start);
|
|
31
|
+
return buffer.subarray(0, read).toString('latin1');
|
|
32
|
+
}
|
|
33
|
+
/** Liest Anfang und Ende der Datei und sucht den Verweis; Lesefehler ergeben null. */
|
|
34
|
+
export function remoteManifestOfFile(path) {
|
|
35
|
+
let fd = null;
|
|
36
|
+
try {
|
|
37
|
+
fd = openSync(path, 'r');
|
|
38
|
+
const size = fstatSync(fd).size;
|
|
39
|
+
const head = readSlice(fd, 0, Math.min(size, HEAD_BYTES));
|
|
40
|
+
const found = findRemoteManifest(head);
|
|
41
|
+
if (found !== null || size <= HEAD_BYTES)
|
|
42
|
+
return found;
|
|
43
|
+
const tailStart = Math.max(HEAD_BYTES, size - TAIL_BYTES);
|
|
44
|
+
return findRemoteManifest(readSlice(fd, tailStart, size - tailStart));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (fd !== null) {
|
|
51
|
+
try {
|
|
52
|
+
closeSync(fd);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* egal */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function remoteManifestWarning(url) {
|
|
61
|
+
return 'The image refers in its XMP metadata (dcterms:provenance) to a remote Content Credentials manifest (' + url +
|
|
62
|
+
'). InDesign can hang for minutes while it loads such a manifest on export; place a copy of the image without XMP metadata ' +
|
|
63
|
+
'(pixels unchanged) if exports stall. The file itself was not changed.';
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=content-credentials.js.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bindeglied: geprueften Parametersatz in einen Befehl uebersetzen, ueber den
|
|
3
|
+
* Transport schicken, protokollieren, Ergebnisumschlag zurueckgeben.
|
|
4
|
+
* Kennt InDesign nicht, kennt den Transport nur als Schnittstelle.
|
|
5
|
+
*/
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { applyConfigDefaults } from './config.js';
|
|
9
|
+
import { maskHome } from './logging.js';
|
|
10
|
+
import { takeTiming, transportError, } from '../transport/transport.js';
|
|
11
|
+
/** Entfernt Felder ohne Wert, damit der Befehl keine leeren Schluessel traegt. */
|
|
12
|
+
export function dropUndefined(input) {
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const key of Object.keys(input)) {
|
|
15
|
+
if (input[key] !== undefined)
|
|
16
|
+
out[key] = input[key];
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
/** Parameter pruefen und daraus den Befehl bauen – ohne InDesign. */
|
|
21
|
+
export function buildCommand(definition, rawInput, config = {}) {
|
|
22
|
+
/*
|
|
23
|
+
* strictObject: unbekannte Parameter sind ein Fehler, kein stilles Weglassen.
|
|
24
|
+
* Sonst wuerde z. B. add_page mit "position" statt "at" die Seite still ans
|
|
25
|
+
* Ende haengen (Spezifikation nennt die verwechselbaren Namen ausdruecklich).
|
|
26
|
+
*/
|
|
27
|
+
const schema = z.strictObject(definition.inputShape);
|
|
28
|
+
const parsed = schema.safeParse(rawInput ?? {});
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
const issues = parsed.error.issues.map((issue) => (issue.path.length > 0 ? issue.path.join('.') + ': ' : '') + issue.message);
|
|
31
|
+
return {
|
|
32
|
+
error: transportError('invalid_argument', issues.join('; '), { tool: definition.name, issues }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const values = dropUndefined(parsed.data);
|
|
36
|
+
let args = definition.buildArgs ? definition.buildArgs(values) : values;
|
|
37
|
+
/* Konfigurierte Vorgaben erst nach der Pruefung: der Aufruf geht vor (E-54). */
|
|
38
|
+
if (definition.configArgs)
|
|
39
|
+
args = applyConfigDefaults(args, definition.configArgs(config));
|
|
40
|
+
return { command: { cmd: definition.command, args: dropUndefined(args) } };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Ersetzt das Benutzerverzeichnis in der Fehlerantwort durch "~" – in
|
|
44
|
+
* `error.message` und in `error.details` (Auftrag A-8, Punkt 4; Entscheidung
|
|
45
|
+
* des Vermarktungsprojekts vom 14.09.2026, Vermerk E-68). Der Pfad bleibt
|
|
46
|
+
* lesbar, allein der Benutzername faellt weg. Aufbau und Felder der
|
|
47
|
+
* Fehlerantwort bleiben unveraendert (A-5).
|
|
48
|
+
*/
|
|
49
|
+
export function maskErrorPaths(result, home = homedir()) {
|
|
50
|
+
if (result.error === undefined)
|
|
51
|
+
return result;
|
|
52
|
+
return {
|
|
53
|
+
...result,
|
|
54
|
+
error: {
|
|
55
|
+
kind: result.error.kind,
|
|
56
|
+
message: maskHome(result.error.message, home),
|
|
57
|
+
details: maskDeep(result.error.details, home, 0),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Wie maskErrorPaths, aber fuer beliebig verschachtelte Details. */
|
|
62
|
+
function maskDeep(value, home, depth) {
|
|
63
|
+
if (typeof value === 'string')
|
|
64
|
+
return maskHome(value, home);
|
|
65
|
+
if (depth >= 6)
|
|
66
|
+
return value;
|
|
67
|
+
if (Array.isArray(value))
|
|
68
|
+
return value.map((item) => maskDeep(item, home, depth + 1));
|
|
69
|
+
if (value !== null && typeof value === 'object') {
|
|
70
|
+
const out = {};
|
|
71
|
+
for (const key of Object.keys(value)) {
|
|
72
|
+
out[key] = maskDeep(value[key], home, depth + 1);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sitzung 38 (A-22): Pfade der Redaktionsdateien (`managed_file`, in
|
|
80
|
+
* list_stories, get_frame_text, find_replace.skipped_locked[]) mit "~" statt
|
|
81
|
+
* des Benutzerverzeichnisses – wie die Fehlerantworten (E-68). Nur dieses
|
|
82
|
+
* Feld; andere Felder (Text, Pfade anderer Werkzeuge) bleiben unveraendert.
|
|
83
|
+
*/
|
|
84
|
+
export function maskManagedFiles(result, home = homedir()) {
|
|
85
|
+
return maskManagedDeep(result, home, 0);
|
|
86
|
+
}
|
|
87
|
+
function maskManagedDeep(value, home, depth) {
|
|
88
|
+
if (depth >= 6)
|
|
89
|
+
return value;
|
|
90
|
+
if (Array.isArray(value))
|
|
91
|
+
return value.map((item) => maskManagedDeep(item, home, depth + 1));
|
|
92
|
+
if (value !== null && typeof value === 'object') {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const key of Object.keys(value)) {
|
|
95
|
+
const entry = value[key];
|
|
96
|
+
out[key] = key === 'managed_file' && typeof entry === 'string' ? maskHome(entry, home) : maskManagedDeep(entry, home, depth + 1);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
/** Ein Werkzeugaufruf von Anfang bis Ende. */
|
|
103
|
+
export async function runTool(definition, rawInput, transport, logger, options = {}) {
|
|
104
|
+
const built = buildCommand(definition, rawInput, options.config ?? {});
|
|
105
|
+
if ('error' in built) {
|
|
106
|
+
const masked = maskErrorPaths(built.error);
|
|
107
|
+
logger.log({
|
|
108
|
+
tool: definition.name,
|
|
109
|
+
command: definition.command,
|
|
110
|
+
args: {},
|
|
111
|
+
duration_ms: 0,
|
|
112
|
+
ok: false,
|
|
113
|
+
error_kind: masked.error?.kind ?? 'invalid_argument',
|
|
114
|
+
error_message: masked.error?.message ?? '',
|
|
115
|
+
warnings: [],
|
|
116
|
+
result_keys: [],
|
|
117
|
+
});
|
|
118
|
+
return masked;
|
|
119
|
+
}
|
|
120
|
+
const startedAt = Date.now();
|
|
121
|
+
let result;
|
|
122
|
+
try {
|
|
123
|
+
/* Sitzung 60 (E-102): Werkzeuge mit lokalem Teil antworten ohne InDesign, wenn local ein Ergebnis liefert. */
|
|
124
|
+
const local = definition.local ? definition.local(built.command.args) : undefined;
|
|
125
|
+
if (local !== undefined) {
|
|
126
|
+
result = local;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const sendOptions = options.timeoutMs === undefined ? undefined : { timeoutMs: options.timeoutMs };
|
|
130
|
+
result = await transport.send(built.command, sendOptions);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
result = transportError('script_error', 'Transport failed: ' + String(error), null);
|
|
135
|
+
}
|
|
136
|
+
const duration = Date.now() - startedAt;
|
|
137
|
+
/* A-74: Zeitanteile des Transports nur ins Protokoll, nie in die Antwort. */
|
|
138
|
+
const timing = takeTiming(result);
|
|
139
|
+
if (timing !== null && typeof timing['begin'] === 'number') {
|
|
140
|
+
timing['wait'] = Math.max(0, timing['begin'] - startedAt);
|
|
141
|
+
delete timing['begin'];
|
|
142
|
+
}
|
|
143
|
+
if (result.ok && definition.after) {
|
|
144
|
+
try {
|
|
145
|
+
result = definition.after(built.command.args, result);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
result = { ...result, warnings: [...(Array.isArray(result.warnings) ? result.warnings : []), 'Post-processing failed: ' + String(error)] };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
result = maskErrorPaths(result);
|
|
152
|
+
if (result.ok)
|
|
153
|
+
result = maskManagedFiles(result);
|
|
154
|
+
if (result.ok && definition.mapResult) {
|
|
155
|
+
result = { ...result, ...definition.mapResult(result) };
|
|
156
|
+
}
|
|
157
|
+
logger.log({
|
|
158
|
+
tool: definition.name,
|
|
159
|
+
command: built.command.cmd,
|
|
160
|
+
args: built.command.args,
|
|
161
|
+
duration_ms: duration,
|
|
162
|
+
ok: result.ok === true,
|
|
163
|
+
error_kind: result.error?.kind ?? null,
|
|
164
|
+
error_message: result.error?.message ?? null,
|
|
165
|
+
warnings: Array.isArray(result.warnings) ? result.warnings : [],
|
|
166
|
+
result_keys: Object.keys(result).filter((key) => key !== 'ok' && key !== 'warnings'),
|
|
167
|
+
...(timing !== null ? { timing_ms: timing } : {}),
|
|
168
|
+
});
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=dispatch.js.map
|