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,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regelkatalog fuer check_typography (A-7.6): eine Datentabelle im Repo
|
|
3
|
+
* (rules/typography-rules.json), kein Code je Regel. Der Server liest sie
|
|
4
|
+
* einmal, waehlt nach Sprache und Kennungen aus und reicht die Zeilen als
|
|
5
|
+
* Befehlsargument an den Interpreter; der kennt keine Regel von sich aus.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
export const TYPOGRAPHY_LOCALES = ['de', 'de-CH', 'en'];
|
|
11
|
+
export const SEVERITY_RANK = { info: 0, warning: 1, error: 2 };
|
|
12
|
+
/* Diese Datei laeuft aus dist/src/mcp/ – drei Ebenen ueber ihr liegt das Repo bzw. Paket. */
|
|
13
|
+
export function typographyRulesPath() {
|
|
14
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'rules', 'typography-rules.json');
|
|
15
|
+
}
|
|
16
|
+
function isRule(value) {
|
|
17
|
+
if (typeof value !== 'object' || value === null)
|
|
18
|
+
return false;
|
|
19
|
+
const r = value;
|
|
20
|
+
return (typeof r['id'] === 'string' &&
|
|
21
|
+
Array.isArray(r['locales']) &&
|
|
22
|
+
typeof r['category'] === 'string' &&
|
|
23
|
+
typeof r['severity'] === 'string' &&
|
|
24
|
+
(r['kind'] === 'grep' || r['kind'] === 'lines' || r['kind'] === 'language') &&
|
|
25
|
+
(r['kind'] === 'grep' ? typeof r['pattern'] === 'string' : typeof r['check'] === 'string') &&
|
|
26
|
+
typeof r['message'] === 'string' &&
|
|
27
|
+
typeof r['suggestion'] === 'string' &&
|
|
28
|
+
typeof r['source'] === 'string');
|
|
29
|
+
}
|
|
30
|
+
export function parseTypographyRules(text) {
|
|
31
|
+
const raw = JSON.parse(text);
|
|
32
|
+
if (!Array.isArray(raw.rules))
|
|
33
|
+
throw new Error('typography-rules.json: "rules" must be an array');
|
|
34
|
+
const out = [];
|
|
35
|
+
raw.rules.forEach((entry, index) => {
|
|
36
|
+
if (!isRule(entry))
|
|
37
|
+
throw new Error(`typography-rules.json: rule ${index} is malformed`);
|
|
38
|
+
out.push(entry);
|
|
39
|
+
});
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
let cache = null;
|
|
43
|
+
export function loadTypographyRules(path = typographyRulesPath()) {
|
|
44
|
+
if (cache === null)
|
|
45
|
+
cache = parseTypographyRules(readFileSync(path, 'utf8'));
|
|
46
|
+
return cache;
|
|
47
|
+
}
|
|
48
|
+
/** Alle Kennungen des Katalogs (ohne Doppelte, in Katalogreihenfolge). */
|
|
49
|
+
export function typographyRuleIds(rules = loadTypographyRules()) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const rule of rules)
|
|
52
|
+
if (!out.includes(rule.id))
|
|
53
|
+
out.push(rule.id);
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Zeilen fuer einen Aufruf: Sprache, gewuenschte und ausgeschlossene
|
|
58
|
+
* Kennungen. Unbekannte Kennungen bleiben in unknown[] – der Interpreter
|
|
59
|
+
* meldet sie als invalid_argument, damit der Fehler wie alle anderen im
|
|
60
|
+
* Ergebnisumschlag steht.
|
|
61
|
+
*/
|
|
62
|
+
export function selectTypographyRules(locale, wanted, excluded, rules = loadTypographyRules()) {
|
|
63
|
+
const ids = typographyRuleIds(rules);
|
|
64
|
+
const unknown = [...(wanted ?? []), ...(excluded ?? [])].filter((id) => !ids.includes(id));
|
|
65
|
+
const chosen = rules.filter((rule) => rule.locales.includes(locale) &&
|
|
66
|
+
(wanted === undefined || wanted.length === 0 || wanted.includes(rule.id)) &&
|
|
67
|
+
!(excluded ?? []).includes(rule.id));
|
|
68
|
+
return { rules: chosen, unknown };
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=typography-rules.js.map
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport fuer Windows: PowerShell -> COM -> InDesign doScript.
|
|
3
|
+
*
|
|
4
|
+
* Gegenstueck zu osascript.ts. Nur diese Datei weiss, dass hier ein
|
|
5
|
+
* Windows-Rechner steht; der Interpreter und das Befehlsformat {cmd, args}
|
|
6
|
+
* bleiben unveraendert.
|
|
7
|
+
*
|
|
8
|
+
* Ablauf je Aufruf (seit Sitzung 80, A-74):
|
|
9
|
+
* 1. Interpreter und Handler samt festem Rahmen liegen als Skriptdatei in
|
|
10
|
+
* einem festen Arbeitsordner (`%LOCALAPPDATA%\layoutbridge-spm\com`),
|
|
11
|
+
* einmal je Buendelstand geschrieben (Pruefsumme im Namen). Daneben ein
|
|
12
|
+
* festes PowerShell-Skript. Je Aufruf entstehen nur Datendateien: der
|
|
13
|
+
* Befehl als ASCII-JSON und das Ergebnis. Vorher entstanden je Aufruf
|
|
14
|
+
* zwei neue Skriptdateien (.jsx mit ~0,9 MB, .ps1) in einem neuen
|
|
15
|
+
* Temp-Ordner; ein Virenschutz mit Echtzeitpruefung (beim Kunden Avira,
|
|
16
|
+
* per Test bestaetigt 10.11.2026) hielt jede davon an – Aufrufe 12 s bis
|
|
17
|
+
* 122 s statt unter einer Sekunde. Ein fester Ordner laesst sich zudem
|
|
18
|
+
* gezielt als Ausnahme eintragen.
|
|
19
|
+
* 2. PowerShell holt sich die Anwendung ueber COM und uebergibt ihr das
|
|
20
|
+
* Skript per doScript, den Befehl und den Ergebnispfad ueber
|
|
21
|
+
* withArguments (im Skript `arguments`). Erst GetActiveObject (Running
|
|
22
|
+
* Object Table), dann CoCreateInstance (New-Object -ComObject): InDesign
|
|
23
|
+
* traegt sich nicht in die Running Object Table ein, CoCreateInstance
|
|
24
|
+
* verbindet aber mit der laufenden Instanz und startet sie sonst - wie
|
|
25
|
+
* `tell application` beim Mac-Transport (Vermerk E-70, 15.09.2026).
|
|
26
|
+
* 3. Das Skript schreibt sein Ergebnis in eine Datei; der Server liest sie.
|
|
27
|
+
* Umweg ueber die Datei wie beim Mac-Transport: die Rueckgabe ueber COM
|
|
28
|
+
* ist in Laenge und Zeichensatz unzuverlaessig.
|
|
29
|
+
*
|
|
30
|
+
* Gebaut ohne Windows-Rechner gegen eine Attrappe des Prozessstarts
|
|
31
|
+
* (Vermerk E-67). Transportweg am 15.09.2026 mit InDesign CS5 (7.0.0.355)
|
|
32
|
+
* auf Windows 11 ARM64 nachgewiesen (Vermerk E-70). Abgenommen am
|
|
33
|
+
* 15.09.2026 (Vermerk E-71): Integrationssuite 108/108 ueber diesen
|
|
34
|
+
* Transport mit InDesign 21.5.1.73 auf Windows 11 x64 (Build 26200,
|
|
35
|
+
* Windows 365 Cloud-PC, Node 24.21.0); scripts/com-test.ps1 Exit 0,
|
|
36
|
+
* scripts/win-smoke.mjs 7/7. Auch das aktuelle InDesign traegt sich nicht
|
|
37
|
+
* in die Running Object Table ein (GetActiveObject: MK_E_UNAVAILABLE),
|
|
38
|
+
* der Rueckfall auf CoCreateInstance traegt.
|
|
39
|
+
*/
|
|
40
|
+
import { spawn } from 'node:child_process';
|
|
41
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
42
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
43
|
+
import { tmpdir } from 'node:os';
|
|
44
|
+
import { join } from 'node:path';
|
|
45
|
+
import { buildScriptBundle, toAsciiJson } from './script-bundle.js';
|
|
46
|
+
import { attachTiming, transportError, } from './transport.js';
|
|
47
|
+
/** ScriptLanguage.javascript im Objektmodell – als Zahl, weil COM keine Aufzaehlung kennt. */
|
|
48
|
+
export const SCRIPT_LANGUAGE_JAVASCRIPT = 1246973031;
|
|
49
|
+
/** Zeilen, mit denen sich das PowerShell-Skript meldet, wenn es selbst abbricht. */
|
|
50
|
+
export const APP_NOT_AVAILABLE_MARKER = 'IDC-COM-ERROR app_not_available:';
|
|
51
|
+
export const SCRIPT_ERROR_MARKER = 'IDC-COM-ERROR script_error:';
|
|
52
|
+
/** Zeile mit den Zeitanteilen aus PowerShell (nur Protokoll, A-74). */
|
|
53
|
+
export const TIMING_MARKER = 'IDC-COM-TIMING';
|
|
54
|
+
/** Aufgeraeumt wird, was laenger als einen Tag im Arbeitsordner liegt. */
|
|
55
|
+
const STALE_MS = 24 * 60 * 60 * 1000;
|
|
56
|
+
/**
|
|
57
|
+
* Fester Arbeitsordner des Windows-Transports:
|
|
58
|
+
* `%LOCALAPPDATA%\<produkt>\com`, ohne LOCALAPPDATA im Temp-Ordner.
|
|
59
|
+
*/
|
|
60
|
+
export function defaultComWorkDir(dataDirName = 'layoutbridge-spm', env = process.env) {
|
|
61
|
+
const local = env['LOCALAPPDATA'];
|
|
62
|
+
const base = local !== undefined && local.length > 0 ? local : tmpdir();
|
|
63
|
+
return join(base, dataDirName, 'com');
|
|
64
|
+
}
|
|
65
|
+
/** Text in einem PowerShell-Literal mit einfachen Anfuehrungszeichen. */
|
|
66
|
+
export function escapeForPowerShell(text) {
|
|
67
|
+
return text.replace(/'/g, "''");
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Wertet den Ausgang eines Aufrufs aus. Reine Funktion ohne Dateisystem und
|
|
71
|
+
* ohne Prozess, damit die Einheitstests jede Fehlerart ohne Windows pruefen.
|
|
72
|
+
*/
|
|
73
|
+
export function interpretOutcome(outcome, raw, command, timeoutMs) {
|
|
74
|
+
if (outcome.spawnError !== null) {
|
|
75
|
+
return transportError('app_not_available', 'Could not run PowerShell – this transport needs Windows: ' + outcome.spawnError.message, null);
|
|
76
|
+
}
|
|
77
|
+
if (outcome.timedOut) {
|
|
78
|
+
return transportError('timeout', 'InDesign did not answer within ' + timeoutMs +
|
|
79
|
+
' ms. The script may still be running inside InDesign – check the document before repeating the call.', { command: command.cmd, timeout_ms: timeoutMs });
|
|
80
|
+
}
|
|
81
|
+
const stderr = outcome.stderr.trim();
|
|
82
|
+
const marked = markedMessage(stderr, APP_NOT_AVAILABLE_MARKER);
|
|
83
|
+
if (marked !== null) {
|
|
84
|
+
return transportError('app_not_available', marked, { exit_code: outcome.code });
|
|
85
|
+
}
|
|
86
|
+
const scriptMarked = markedMessage(stderr, SCRIPT_ERROR_MARKER);
|
|
87
|
+
if (scriptMarked !== null) {
|
|
88
|
+
return transportError('script_error', scriptMarked, { exit_code: outcome.code });
|
|
89
|
+
}
|
|
90
|
+
if (raw === null || raw.length === 0) {
|
|
91
|
+
return transportError('script_error', stderr.length > 0
|
|
92
|
+
? 'InDesign did not return a result: ' + stderr
|
|
93
|
+
: 'InDesign did not return a result and reported nothing.', { exit_code: outcome.code, stdout: outcome.stdout.trim() });
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(raw);
|
|
97
|
+
if (!Array.isArray(parsed.warnings))
|
|
98
|
+
parsed.warnings = [];
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return transportError('script_error', 'The result of the script is not valid JSON: ' + String(error), {
|
|
103
|
+
raw: raw.slice(0, 2000),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Sucht die Markerzeile und gibt den Text dahinter zurueck, sonst null. */
|
|
108
|
+
function markedMessage(stderr, marker) {
|
|
109
|
+
const at = stderr.indexOf(marker);
|
|
110
|
+
if (at < 0)
|
|
111
|
+
return null;
|
|
112
|
+
const rest = stderr.slice(at + marker.length);
|
|
113
|
+
const end = rest.indexOf('\n');
|
|
114
|
+
const line = (end < 0 ? rest : rest.slice(0, end)).trim();
|
|
115
|
+
return line.length > 0 ? line : marker.replace(':', '');
|
|
116
|
+
}
|
|
117
|
+
/** Zieht die Zeitzeile aus stderr: Zeitanteile und der uebrige Text. */
|
|
118
|
+
export function splitTiming(stderr) {
|
|
119
|
+
const timing = {};
|
|
120
|
+
const kept = [];
|
|
121
|
+
for (const line of stderr.split(/\r?\n/)) {
|
|
122
|
+
const at = line.indexOf(TIMING_MARKER);
|
|
123
|
+
if (at < 0) {
|
|
124
|
+
kept.push(line);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
for (const pair of line.slice(at + TIMING_MARKER.length).trim().split(/\s+/)) {
|
|
128
|
+
const eq = pair.indexOf('=');
|
|
129
|
+
if (eq <= 0)
|
|
130
|
+
continue;
|
|
131
|
+
const value = Number(pair.slice(eq + 1));
|
|
132
|
+
if (Number.isFinite(value))
|
|
133
|
+
timing['ps_' + pair.slice(0, eq)] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { timing, rest: kept.join('\n') };
|
|
137
|
+
}
|
|
138
|
+
/** Kurze Pruefsumme eines Textes fuer Dateinamen. */
|
|
139
|
+
function shortHash(text) {
|
|
140
|
+
return createHash('sha256').update(text, 'utf8').digest('hex').slice(0, 16);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Schreibt eine Datei nur, wenn sie mit diesem Inhalt noch nicht da ist
|
|
144
|
+
* (Pruefsumme im Namen, Groesse als Vollstaendigkeitsprobe). Erst unter
|
|
145
|
+
* eigenem Namen, dann umbenennen: Mehrere Serverinstanzen koennen
|
|
146
|
+
* gleichzeitig starten, keine liest eine halb geschriebene Datei.
|
|
147
|
+
* Liefert true, wenn geschrieben wurde.
|
|
148
|
+
*/
|
|
149
|
+
export function ensureFile(path, content) {
|
|
150
|
+
const size = Buffer.byteLength(content, 'utf8');
|
|
151
|
+
try {
|
|
152
|
+
if (existsSync(path) && statSync(path).size === size)
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
/* dann neu schreiben */
|
|
157
|
+
}
|
|
158
|
+
const temporary = path + '.' + process.pid + '.' + randomBytes(4).toString('hex') + '.tmp';
|
|
159
|
+
writeFileSync(temporary, content, 'utf8');
|
|
160
|
+
try {
|
|
161
|
+
renameSync(temporary, path);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
rmSync(temporary, { force: true });
|
|
165
|
+
/* Eine andere Instanz war schneller und haelt die Datei offen. */
|
|
166
|
+
if (!(existsSync(path) && statSync(path).size === size))
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
export class ComTransport {
|
|
172
|
+
name = 'com';
|
|
173
|
+
progIds;
|
|
174
|
+
powershellPath;
|
|
175
|
+
defaultTimeoutMs;
|
|
176
|
+
keepTempFiles;
|
|
177
|
+
runner;
|
|
178
|
+
workDir;
|
|
179
|
+
sequence = 0;
|
|
180
|
+
cleaned = false;
|
|
181
|
+
/** Zuletzt bereitgelegtes Buendel (gleiches Objekt = gleicher Stand). */
|
|
182
|
+
preparedBundle = null;
|
|
183
|
+
preparedScriptPath = '';
|
|
184
|
+
preparedPowerShellPath = '';
|
|
185
|
+
constructor(options = {}) {
|
|
186
|
+
this.progIds = options.progIds ?? ['InDesign.Application'];
|
|
187
|
+
this.powershellPath = options.powershellPath ?? 'powershell.exe';
|
|
188
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 120_000;
|
|
189
|
+
this.keepTempFiles = options.keepTempFiles ?? false;
|
|
190
|
+
this.runner = options.runner ?? runProcess;
|
|
191
|
+
this.workDir = options.workDir ?? defaultComWorkDir(options.dataDirName);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Das vollstaendige Skript: Buendel plus fester Rahmen. Der Befehl steht
|
|
195
|
+
* nicht darin – er kommt ueber withArguments (arguments[0] Befehl als
|
|
196
|
+
* ASCII-JSON, arguments[1] Ergebnispfad). So bleibt die Datei je
|
|
197
|
+
* Buendelstand gleich.
|
|
198
|
+
*/
|
|
199
|
+
buildScript(bundle = buildScriptBundle()) {
|
|
200
|
+
const frame = [
|
|
201
|
+
'',
|
|
202
|
+
'/* ---- Rahmen des com-Transports (ExtendScript, Windows) ---- */',
|
|
203
|
+
'var __idcArgs = (typeof arguments !== "undefined" && arguments !== null) ? arguments : [];',
|
|
204
|
+
'(function (commandText, resultPath) {',
|
|
205
|
+
' var __text, __file;',
|
|
206
|
+
' if (commandText === undefined || commandText === null || resultPath === undefined || resultPath === null) {',
|
|
207
|
+
' return "missing arguments";',
|
|
208
|
+
' }',
|
|
209
|
+
' __text = IDC.run(eval("(" + String(commandText) + ")"));',
|
|
210
|
+
' __file = new File(String(resultPath));',
|
|
211
|
+
' __file.encoding = "UTF-8";',
|
|
212
|
+
' __file.lineFeed = "Unix";',
|
|
213
|
+
' __file.open("w");',
|
|
214
|
+
' __file.write(__text);',
|
|
215
|
+
' __file.close();',
|
|
216
|
+
' return "ok";',
|
|
217
|
+
'})(__idcArgs[0], __idcArgs[1]);',
|
|
218
|
+
'',
|
|
219
|
+
].join('\n');
|
|
220
|
+
return bundle + '\n' + frame;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Das feste PowerShell-Skript: Anwendung ueber COM holen (GetActiveObject,
|
|
224
|
+
* sonst New-Object -ComObject), Skript- und Befehlsdatei lesen, doScript
|
|
225
|
+
* mit withArguments aufrufen. Pfade kommen als Parameter. Bricht es selbst
|
|
226
|
+
* ab, meldet es sich mit einer Markerzeile, damit die Fehlerart nicht aus
|
|
227
|
+
* Fremdtexten geraten wird. Zeitanteile stehen in einer eigenen Zeile.
|
|
228
|
+
*/
|
|
229
|
+
buildPowerShell() {
|
|
230
|
+
const ids = this.progIds.map((id) => "'" + escapeForPowerShell(id) + "'").join(', ');
|
|
231
|
+
return [
|
|
232
|
+
'param([string]$ScriptPath, [string]$CommandPath, [string]$ResultPath)',
|
|
233
|
+
'$ErrorActionPreference = ' + "'Stop'",
|
|
234
|
+
'$watch = [Diagnostics.Stopwatch]::StartNew()',
|
|
235
|
+
'$marks = @{}',
|
|
236
|
+
'function Write-IdcTiming {',
|
|
237
|
+
" $line = '" + TIMING_MARKER + "'",
|
|
238
|
+
' foreach ($key in $marks.Keys) { $line += (" " + $key + "=" + $marks[$key]) }',
|
|
239
|
+
' [Console]::Error.WriteLine($line)',
|
|
240
|
+
'}',
|
|
241
|
+
'$progIds = @(' + ids + ')',
|
|
242
|
+
'$app = $null',
|
|
243
|
+
'$reasons = @()',
|
|
244
|
+
'foreach ($id in $progIds) {',
|
|
245
|
+
' try {',
|
|
246
|
+
' $app = [Runtime.InteropServices.Marshal]::GetActiveObject($id)',
|
|
247
|
+
' break',
|
|
248
|
+
' } catch {',
|
|
249
|
+
' $reasons += ($id + " (GetActiveObject): " + $_.Exception.Message)',
|
|
250
|
+
' }',
|
|
251
|
+
'}',
|
|
252
|
+
'if ($null -eq $app) {',
|
|
253
|
+
' foreach ($id in $progIds) {',
|
|
254
|
+
' try {',
|
|
255
|
+
' $app = New-Object -ComObject $id',
|
|
256
|
+
' break',
|
|
257
|
+
' } catch {',
|
|
258
|
+
' $reasons += ($id + " (New-Object): " + $_.Exception.Message)',
|
|
259
|
+
' }',
|
|
260
|
+
' }',
|
|
261
|
+
'}',
|
|
262
|
+
"$marks['connect'] = $watch.ElapsedMilliseconds",
|
|
263
|
+
'if ($null -eq $app) {',
|
|
264
|
+
' Write-IdcTiming',
|
|
265
|
+
' [Console]::Error.WriteLine(' + "'" + APP_NOT_AVAILABLE_MARKER +
|
|
266
|
+
" ' + ($reasons -join '; '))",
|
|
267
|
+
' exit 3',
|
|
268
|
+
'}',
|
|
269
|
+
'$scriptText = [IO.File]::ReadAllText($ScriptPath, [Text.Encoding]::UTF8)',
|
|
270
|
+
'$commandText = [IO.File]::ReadAllText($CommandPath, [Text.Encoding]::UTF8)',
|
|
271
|
+
"$marks['read'] = $watch.ElapsedMilliseconds - $marks['connect']",
|
|
272
|
+
'try {',
|
|
273
|
+
' $null = $app.DoScript($scriptText, ' + SCRIPT_LANGUAGE_JAVASCRIPT +
|
|
274
|
+
', [object[]]@($commandText, $ResultPath))',
|
|
275
|
+
'} catch {',
|
|
276
|
+
" $marks['doscript'] = $watch.ElapsedMilliseconds - $marks['connect'] - $marks['read']",
|
|
277
|
+
' Write-IdcTiming',
|
|
278
|
+
' [Console]::Error.WriteLine(' + "'" + SCRIPT_ERROR_MARKER +
|
|
279
|
+
" ' + $_.Exception.Message)",
|
|
280
|
+
' exit 4',
|
|
281
|
+
'}',
|
|
282
|
+
"$marks['doscript'] = $watch.ElapsedMilliseconds - $marks['connect'] - $marks['read']",
|
|
283
|
+
"$marks['total'] = $watch.ElapsedMilliseconds",
|
|
284
|
+
'Write-IdcTiming',
|
|
285
|
+
'exit 0',
|
|
286
|
+
'',
|
|
287
|
+
].join('\n');
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Legt Skript- und PowerShell-Datei in den Arbeitsordner, falls noetig.
|
|
291
|
+
* Liefert true, wenn in diesem Aufruf eine davon geschrieben wurde.
|
|
292
|
+
*/
|
|
293
|
+
prepare() {
|
|
294
|
+
const bundle = buildScriptBundle();
|
|
295
|
+
if (bundle === this.preparedBundle && existsSync(this.preparedScriptPath) && existsSync(this.preparedPowerShellPath)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
mkdirSync(this.workDir, { recursive: true });
|
|
299
|
+
const script = this.buildScript(bundle);
|
|
300
|
+
const powerShell = this.buildPowerShell();
|
|
301
|
+
const scriptPath = join(this.workDir, 'script-' + shortHash(script) + '.jsx');
|
|
302
|
+
const powerShellPath = join(this.workDir, 'run-' + shortHash(powerShell) + '.ps1');
|
|
303
|
+
let written = ensureFile(scriptPath, script);
|
|
304
|
+
written = ensureFile(powerShellPath, powerShell) || written;
|
|
305
|
+
this.preparedBundle = bundle;
|
|
306
|
+
this.preparedScriptPath = scriptPath;
|
|
307
|
+
this.preparedPowerShellPath = powerShellPath;
|
|
308
|
+
if (!this.cleaned) {
|
|
309
|
+
this.cleaned = true;
|
|
310
|
+
this.removeStale([scriptPath, powerShellPath]);
|
|
311
|
+
}
|
|
312
|
+
return written;
|
|
313
|
+
}
|
|
314
|
+
/** Alte Dateien frueherer Staende und liegengebliebener Aufrufe entfernen. */
|
|
315
|
+
removeStale(keep) {
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
let names = [];
|
|
318
|
+
try {
|
|
319
|
+
names = readdirSync(this.workDir);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
for (const name of names) {
|
|
325
|
+
const path = join(this.workDir, name);
|
|
326
|
+
if (keep.includes(path))
|
|
327
|
+
continue;
|
|
328
|
+
if (!/^(script-.*\.jsx|run-.*\.ps1|call-.*\.json|.*\.tmp)$/.test(name))
|
|
329
|
+
continue;
|
|
330
|
+
try {
|
|
331
|
+
if (now - statSync(path).mtimeMs > STALE_MS)
|
|
332
|
+
rmSync(path, { force: true });
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
/* Aufraeumen darf keinen Aufruf scheitern lassen. */
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async send(command, options = {}) {
|
|
340
|
+
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
|
|
341
|
+
const timing = { begin: Date.now() };
|
|
342
|
+
let mark = Date.now();
|
|
343
|
+
const lap = (key) => {
|
|
344
|
+
const now = Date.now();
|
|
345
|
+
timing[key] = now - mark;
|
|
346
|
+
mark = now;
|
|
347
|
+
};
|
|
348
|
+
this.sequence += 1;
|
|
349
|
+
const stem = 'call-' + process.pid + '-' + this.sequence + '-' + randomBytes(4).toString('hex');
|
|
350
|
+
const commandPath = join(this.workDir, stem + '.command.json');
|
|
351
|
+
const resultPath = join(this.workDir, stem + '.result.json');
|
|
352
|
+
try {
|
|
353
|
+
timing['script_written'] = this.prepare() ? 1 : 0;
|
|
354
|
+
writeFileSync(commandPath, toAsciiJson(command), 'utf8');
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
return transportError('io_error', 'Could not prepare the script files of the Windows transport.', String(error));
|
|
358
|
+
}
|
|
359
|
+
lap('prepare');
|
|
360
|
+
try {
|
|
361
|
+
const outcome = await this.runner(this.powershellPath, [
|
|
362
|
+
'-NoProfile',
|
|
363
|
+
'-NonInteractive',
|
|
364
|
+
'-ExecutionPolicy',
|
|
365
|
+
'Bypass',
|
|
366
|
+
'-File',
|
|
367
|
+
this.preparedPowerShellPath,
|
|
368
|
+
'-ScriptPath',
|
|
369
|
+
this.preparedScriptPath,
|
|
370
|
+
'-CommandPath',
|
|
371
|
+
commandPath,
|
|
372
|
+
'-ResultPath',
|
|
373
|
+
resultPath,
|
|
374
|
+
], timeoutMs);
|
|
375
|
+
lap('process');
|
|
376
|
+
const split = splitTiming(outcome.stderr);
|
|
377
|
+
Object.assign(timing, split.timing);
|
|
378
|
+
if (split.timing['ps_total'] !== undefined && timing['process'] !== undefined) {
|
|
379
|
+
timing['ps_start'] = Math.max(0, timing['process'] - split.timing['ps_total']);
|
|
380
|
+
}
|
|
381
|
+
let raw = null;
|
|
382
|
+
try {
|
|
383
|
+
raw = readFileSync(resultPath, 'utf8');
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
raw = null;
|
|
387
|
+
}
|
|
388
|
+
lap('read_result');
|
|
389
|
+
return attachTiming(interpretOutcome({ ...outcome, stderr: split.rest }, raw, command, timeoutMs), timing);
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
if (!this.keepTempFiles) {
|
|
393
|
+
try {
|
|
394
|
+
rmSync(commandPath, { force: true });
|
|
395
|
+
rmSync(resultPath, { force: true });
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
/* Aufraeumen darf keinen Aufruf scheitern lassen. */
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
lap('cleanup');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async close() {
|
|
405
|
+
/* Kein dauerhafter Zustand: nichts zu schliessen. */
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function runProcess(command, args, timeoutMs) {
|
|
409
|
+
return new Promise((resolve) => {
|
|
410
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
411
|
+
let stdout = '';
|
|
412
|
+
let stderr = '';
|
|
413
|
+
let timedOut = false;
|
|
414
|
+
let settled = false;
|
|
415
|
+
const timer = setTimeout(() => {
|
|
416
|
+
timedOut = true;
|
|
417
|
+
child.kill('SIGKILL');
|
|
418
|
+
}, timeoutMs);
|
|
419
|
+
child.stdout.on('data', (chunk) => {
|
|
420
|
+
stdout += chunk.toString('utf8');
|
|
421
|
+
});
|
|
422
|
+
child.stderr.on('data', (chunk) => {
|
|
423
|
+
stderr += chunk.toString('utf8');
|
|
424
|
+
});
|
|
425
|
+
const finish = (code, spawnError) => {
|
|
426
|
+
if (settled)
|
|
427
|
+
return;
|
|
428
|
+
settled = true;
|
|
429
|
+
clearTimeout(timer);
|
|
430
|
+
resolve({ code, stdout, stderr, timedOut, spawnError });
|
|
431
|
+
};
|
|
432
|
+
child.on('error', (error) => finish(null, error));
|
|
433
|
+
child.on('close', (code) => finish(code, null));
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
//# sourceMappingURL=com.js.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport fuer den Mac: osascript -> AppleScript -> InDesign do script.
|
|
3
|
+
*
|
|
4
|
+
* Nur diese Datei weiss, dass hier ein Mac steht. Der Interpreter bleibt frei
|
|
5
|
+
* von Plattformannahmen.
|
|
6
|
+
*
|
|
7
|
+
* Ablauf je Aufruf:
|
|
8
|
+
* 1. Interpreter und Handler zu einem Skript zusammensetzen, den Befehl als
|
|
9
|
+
* ASCII-JSON hineinschreiben und in eine temporaere .jsx-Datei legen.
|
|
10
|
+
* 2. AppleScript-Datei danebenlegen, die InDesign dieses Skript ausfuehren
|
|
11
|
+
* laesst.
|
|
12
|
+
* 3. Das Skript schreibt sein Ergebnis in eine Datei; der Server liest sie.
|
|
13
|
+
* Umweg ueber die Datei, weil die Rueckgabe ueber AppleScript in Laenge
|
|
14
|
+
* und Zeichensatz unzuverlaessig ist.
|
|
15
|
+
*
|
|
16
|
+
* Der kleine Rahmen in Schritt 1 benutzt das ExtendScript-Objekt File. Das ist
|
|
17
|
+
* bewusst hier und nicht im Interpreter: Dateizugriff ist Transportsache.
|
|
18
|
+
*/
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { buildScriptBundle, toAsciiJson } from './script-bundle.js';
|
|
24
|
+
import { transportError, } from './transport.js';
|
|
25
|
+
const APP_NOT_AVAILABLE_HINTS = [
|
|
26
|
+
'-1728',
|
|
27
|
+
'-600',
|
|
28
|
+
'-609',
|
|
29
|
+
'-10814',
|
|
30
|
+
"isn't running",
|
|
31
|
+
'is not running',
|
|
32
|
+
'Application isn',
|
|
33
|
+
];
|
|
34
|
+
function escapeForJsString(text) {
|
|
35
|
+
return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
36
|
+
}
|
|
37
|
+
export class OsascriptTransport {
|
|
38
|
+
name = 'osascript';
|
|
39
|
+
appBundleId;
|
|
40
|
+
defaultTimeoutMs;
|
|
41
|
+
keepTempFiles;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.appBundleId = options.appBundleId ?? 'com.adobe.InDesign';
|
|
44
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 120_000;
|
|
45
|
+
this.keepTempFiles = options.keepTempFiles ?? false;
|
|
46
|
+
}
|
|
47
|
+
/** Das vollstaendige Skript inklusive Rahmen, der das Ergebnis wegschreibt. */
|
|
48
|
+
buildScript(command, resultPath) {
|
|
49
|
+
const bundle = buildScriptBundle();
|
|
50
|
+
const commandLiteral = toAsciiJson(command);
|
|
51
|
+
const target = escapeForJsString(resultPath);
|
|
52
|
+
const frame = [
|
|
53
|
+
'',
|
|
54
|
+
'/* ---- Rahmen des osascript-Transports (ExtendScript, Mac) ---- */',
|
|
55
|
+
'(function () {',
|
|
56
|
+
' var __text = IDC.run(' + commandLiteral + ');',
|
|
57
|
+
' var __file = new File("' + target + '");',
|
|
58
|
+
' __file.encoding = "UTF-8";',
|
|
59
|
+
' __file.lineFeed = "Unix";',
|
|
60
|
+
' __file.open("w");',
|
|
61
|
+
' __file.write(__text);',
|
|
62
|
+
' __file.close();',
|
|
63
|
+
' return "ok";',
|
|
64
|
+
'})();',
|
|
65
|
+
'',
|
|
66
|
+
].join('\n');
|
|
67
|
+
return bundle + '\n' + frame;
|
|
68
|
+
}
|
|
69
|
+
buildAppleScript(scriptPath, timeoutMs) {
|
|
70
|
+
const seconds = Math.max(5, Math.ceil(timeoutMs / 1000) + 5);
|
|
71
|
+
return [
|
|
72
|
+
'with timeout of ' + seconds + ' seconds',
|
|
73
|
+
'\ttell application id "' + this.appBundleId + '"',
|
|
74
|
+
'\t\tdo script (POSIX file "' + escapeForJsString(scriptPath) + '") language javascript',
|
|
75
|
+
'\tend tell',
|
|
76
|
+
'end timeout',
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
async send(command, options = {}) {
|
|
80
|
+
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
|
|
81
|
+
let directory;
|
|
82
|
+
try {
|
|
83
|
+
directory = mkdtempSync(join(tmpdir(), 'idc-spm-'));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return transportError('io_error', 'Could not create a temporary directory.', String(error));
|
|
87
|
+
}
|
|
88
|
+
const scriptPath = join(directory, 'command.jsx');
|
|
89
|
+
const resultPath = join(directory, 'result.json');
|
|
90
|
+
const appleScriptPath = join(directory, 'command.applescript');
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(scriptPath, this.buildScript(command, resultPath), 'utf8');
|
|
93
|
+
writeFileSync(appleScriptPath, this.buildAppleScript(scriptPath, timeoutMs), 'utf8');
|
|
94
|
+
const outcome = await runProcess('osascript', [appleScriptPath], timeoutMs);
|
|
95
|
+
if (outcome.spawnError !== null) {
|
|
96
|
+
return transportError('app_not_available', 'Could not run osascript – this transport needs macOS: ' + outcome.spawnError.message, null);
|
|
97
|
+
}
|
|
98
|
+
if (outcome.timedOut) {
|
|
99
|
+
return transportError('timeout', 'InDesign did not answer within ' + timeoutMs +
|
|
100
|
+
' ms. The script may still be running inside InDesign – check the document before repeating the call.', { command: command.cmd, timeout_ms: timeoutMs });
|
|
101
|
+
}
|
|
102
|
+
let raw = null;
|
|
103
|
+
try {
|
|
104
|
+
raw = readFileSync(resultPath, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
raw = null;
|
|
108
|
+
}
|
|
109
|
+
if (raw === null || raw.length === 0) {
|
|
110
|
+
const stderr = outcome.stderr.trim();
|
|
111
|
+
const kind = APP_NOT_AVAILABLE_HINTS.some((hint) => stderr.includes(hint))
|
|
112
|
+
? 'app_not_available'
|
|
113
|
+
: 'script_error';
|
|
114
|
+
return transportError(kind, stderr.length > 0
|
|
115
|
+
? 'InDesign did not return a result: ' + stderr
|
|
116
|
+
: 'InDesign did not return a result and reported nothing.', { exit_code: outcome.code, stdout: outcome.stdout.trim() });
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(raw);
|
|
120
|
+
if (!Array.isArray(parsed.warnings))
|
|
121
|
+
parsed.warnings = [];
|
|
122
|
+
return parsed;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return transportError('script_error', 'The result of the script is not valid JSON: ' + String(error), { raw: raw.slice(0, 2000) });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (!this.keepTempFiles) {
|
|
130
|
+
try {
|
|
131
|
+
rmSync(directory, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* Aufraeumen darf keinen Aufruf scheitern lassen. */
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async close() {
|
|
140
|
+
/* Kein dauerhafter Zustand: nichts zu schliessen. */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function runProcess(command, args, timeoutMs) {
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
146
|
+
let stdout = '';
|
|
147
|
+
let stderr = '';
|
|
148
|
+
let timedOut = false;
|
|
149
|
+
let settled = false;
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
timedOut = true;
|
|
152
|
+
child.kill('SIGKILL');
|
|
153
|
+
}, timeoutMs);
|
|
154
|
+
child.stdout.on('data', (chunk) => {
|
|
155
|
+
stdout += chunk.toString('utf8');
|
|
156
|
+
});
|
|
157
|
+
child.stderr.on('data', (chunk) => {
|
|
158
|
+
stderr += chunk.toString('utf8');
|
|
159
|
+
});
|
|
160
|
+
const finish = (code, spawnError) => {
|
|
161
|
+
if (settled)
|
|
162
|
+
return;
|
|
163
|
+
settled = true;
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
resolve({ code, stdout, stderr, timedOut, spawnError });
|
|
166
|
+
};
|
|
167
|
+
child.on('error', (error) => finish(null, error));
|
|
168
|
+
child.on('close', (code) => finish(code, null));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=osascript.js.map
|