@segnavia/idml 0.0.0-stage → 0.29.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.
- package/LICENSE +21 -0
- package/dist/frame-preferences.js +45 -0
- package/dist/import.d.ts +13 -0
- package/dist/import.js +27 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/limits.d.ts +15 -0
- package/dist/limits.js +15 -0
- package/dist/map-book.js +73 -0
- package/dist/model.d.ts +44 -0
- package/dist/model.js +268 -0
- package/dist/refusal.js +6 -0
- package/dist/report.d.ts +17 -0
- package/dist/report.js +75 -0
- package/dist/stories.js +141 -0
- package/dist/story-model.js +53 -0
- package/dist/style-mappings.js +52 -0
- package/dist/style-property-report.js +236 -0
- package/dist/styles.js +285 -0
- package/dist/types.d.ts +35 -0
- package/dist/unsupported-elements.js +49 -0
- package/dist/xml.js +18 -0
- package/dist/zip.js +142 -0
- package/package.json +33 -4
- package/README.md +0 -3
package/dist/styles.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { stylePropertyMapping } from "./style-mappings.js";
|
|
2
|
+
import { reportableStyleProperties } from "./style-property-report.js";
|
|
3
|
+
import { idmlDirection, localName } from "./xml.js";
|
|
4
|
+
import { isTag } from "domhandler";
|
|
5
|
+
//#region src/styles.ts
|
|
6
|
+
const PARAGRAPH_ROOT = "$ID/[No paragraph style]";
|
|
7
|
+
const CHARACTER_ROOT = "$ID/[No character style]";
|
|
8
|
+
function text(element) {
|
|
9
|
+
return element.children.map((child) => "data" in child ? child.data : isTag(child) ? text(child) : "").join("").trim();
|
|
10
|
+
}
|
|
11
|
+
function properties(element) {
|
|
12
|
+
const result = { ...element.attribs };
|
|
13
|
+
const holder = element.children.find((child) => isTag(child) && localName(child.name) === "Properties");
|
|
14
|
+
for (const child of holder?.children ?? []) {
|
|
15
|
+
if (!isTag(child)) continue;
|
|
16
|
+
const value = text(child);
|
|
17
|
+
if (value || child.children.some(isTag)) result[localName(child.name)] = value || "[entry]";
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
function parseStyles(roots, part) {
|
|
22
|
+
const result = [];
|
|
23
|
+
for (const kind of ["ParagraphStyle", "CharacterStyle"]) for (const element of roots.filter((candidate) => localName(candidate.name) === kind)) {
|
|
24
|
+
const values = properties(element);
|
|
25
|
+
const id = values.Self;
|
|
26
|
+
if (!id) continue;
|
|
27
|
+
result.push({
|
|
28
|
+
id,
|
|
29
|
+
name: values.Name ?? id.split("/").at(-1) ?? id,
|
|
30
|
+
kind: kind === "ParagraphStyle" ? "paragraph" : "character",
|
|
31
|
+
...values.BasedOn ? { basedOn: values.BasedOn } : {},
|
|
32
|
+
properties: Object.fromEntries(Object.entries(values).filter(([name]) => ![
|
|
33
|
+
"Self",
|
|
34
|
+
"Name",
|
|
35
|
+
"BasedOn"
|
|
36
|
+
].includes(name))),
|
|
37
|
+
location: `${part}#${id}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function parseSwatches(roots) {
|
|
43
|
+
const result = [];
|
|
44
|
+
for (const element of roots.filter((candidate) => localName(candidate.name) === "Color")) {
|
|
45
|
+
const id = element.attribs.Self;
|
|
46
|
+
if (!id) continue;
|
|
47
|
+
const name = element.attribs.Name ?? id;
|
|
48
|
+
const space = element.attribs.Space;
|
|
49
|
+
const values = (element.attribs.ColorValue ?? "").trim().split(/\s+/).map(Number).filter(Number.isFinite);
|
|
50
|
+
const model = element.attribs.Model === "Spot" ? "spot" : space === "RGB" ? "rgb" : space === "CMYK" ? "cmyk" : "named";
|
|
51
|
+
result.push({
|
|
52
|
+
id,
|
|
53
|
+
name,
|
|
54
|
+
model,
|
|
55
|
+
...values.length > 0 ? { values } : {}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function rootName(kind) {
|
|
61
|
+
return kind === "paragraph" ? PARAGRAPH_ROOT : CHARACTER_ROOT;
|
|
62
|
+
}
|
|
63
|
+
function slug(value) {
|
|
64
|
+
return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "style";
|
|
65
|
+
}
|
|
66
|
+
const LANGUAGES = {
|
|
67
|
+
"$ID/Hebrew": "he",
|
|
68
|
+
"$ID/English: USA": "en-US",
|
|
69
|
+
"$ID/English: UK": "en-GB",
|
|
70
|
+
"$ID/French": "fr",
|
|
71
|
+
"$ID/German": "de",
|
|
72
|
+
"$ID/Spanish": "es",
|
|
73
|
+
"$ID/Italian": "it",
|
|
74
|
+
"$ID/Arabic": "ar"
|
|
75
|
+
};
|
|
76
|
+
function hasCssMapping(names, css) {
|
|
77
|
+
return [...names].some((name) => {
|
|
78
|
+
const mapping = stylePropertyMapping(name);
|
|
79
|
+
return mapping?.kind === "css" && mapping.css === css;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function textDeclarations(values, mapped, output) {
|
|
83
|
+
if (mapped.has("Capitalization") && values.Capitalization === "SmallCaps") output.push("font-variant-caps:small-caps");
|
|
84
|
+
if (mapped.has("Capitalization") && values.Capitalization === "AllCaps") output.push("text-transform:uppercase");
|
|
85
|
+
if (mapped.has("Underline") && (values.Underline === "true" || values.Underline === "Underline")) output.push("text-decoration-line:underline");
|
|
86
|
+
}
|
|
87
|
+
var StyleMapper = class {
|
|
88
|
+
swatches;
|
|
89
|
+
report;
|
|
90
|
+
bookDirection;
|
|
91
|
+
byId;
|
|
92
|
+
classes = /* @__PURE__ */ new Map();
|
|
93
|
+
used = /* @__PURE__ */ new Set();
|
|
94
|
+
overrides = /* @__PURE__ */ new Map();
|
|
95
|
+
reportedStyleProperties = /* @__PURE__ */ new Set();
|
|
96
|
+
constructor(styles, swatches, report, bookDirection) {
|
|
97
|
+
this.swatches = swatches;
|
|
98
|
+
this.report = report;
|
|
99
|
+
this.bookDirection = bookDirection;
|
|
100
|
+
this.byId = new Map(styles.map((style) => [style.id, style]));
|
|
101
|
+
const taken = /* @__PURE__ */ new Set();
|
|
102
|
+
for (const style of styles) {
|
|
103
|
+
const base = `${style.kind === "paragraph" ? "idml-p-" : "idml-c-"}${slug(style.name)}`;
|
|
104
|
+
let candidate = base;
|
|
105
|
+
let suffix = 2;
|
|
106
|
+
while (taken.has(candidate)) candidate = `${base}-${suffix++}`;
|
|
107
|
+
taken.add(candidate);
|
|
108
|
+
this.classes.set(style.id, candidate);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
flattened(id, kind) {
|
|
112
|
+
return Object.assign({}, ...this.styleChain(id, kind).map((style) => style.properties));
|
|
113
|
+
}
|
|
114
|
+
styleChain(id, kind) {
|
|
115
|
+
const root = this.rootStyle(kind);
|
|
116
|
+
const chain = [];
|
|
117
|
+
const seen = /* @__PURE__ */ new Set();
|
|
118
|
+
let current = this.findStyle(id, kind);
|
|
119
|
+
while (current && !seen.has(current.id)) {
|
|
120
|
+
seen.add(current.id);
|
|
121
|
+
chain.unshift(current);
|
|
122
|
+
current = this.findStyle(current.basedOn, kind);
|
|
123
|
+
}
|
|
124
|
+
if (root && !chain.includes(root)) chain.unshift(root);
|
|
125
|
+
return chain;
|
|
126
|
+
}
|
|
127
|
+
styleClass(id, _kind) {
|
|
128
|
+
if (!id || id.includes("[None]") || id.includes("[No character style]")) return void 0;
|
|
129
|
+
const style = this.findStyle(id, _kind);
|
|
130
|
+
const value = style ? this.classes.get(style.id) : void 0;
|
|
131
|
+
if (value && style) this.used.add(style.id);
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
overrideClass(values, base, directionValue, location, styleDirection) {
|
|
135
|
+
const changed = Object.fromEntries(Object.entries(values).filter(([name, value]) => stylePropertyMapping(name)?.kind !== "non-formatting" && value !== base[name]));
|
|
136
|
+
const resolved = {
|
|
137
|
+
...base,
|
|
138
|
+
...values
|
|
139
|
+
};
|
|
140
|
+
for (const name of reportableStyleProperties(changed, resolved)) this.report.add(this.report.unsupportedOverrideProperties, name, location);
|
|
141
|
+
if (changed.AppliedFont !== void 0 || changed.FontStyle !== void 0) this.report.font(resolved.AppliedFont, resolved.FontStyle, location);
|
|
142
|
+
const cssProperties = Object.fromEntries(Object.entries(changed).filter(([name]) => stylePropertyMapping(name)?.kind === "css"));
|
|
143
|
+
if (styleDirection && styleDirection !== directionValue) {
|
|
144
|
+
for (const name of ["LeftIndent", "RightIndent"]) cssProperties[name] = base[name] ?? "0";
|
|
145
|
+
for (const name of ["Justification"]) if (base[name] !== void 0 && cssProperties[name] === void 0) cssProperties[name] = base[name];
|
|
146
|
+
}
|
|
147
|
+
if (Object.keys(cssProperties).length === 0) return void 0;
|
|
148
|
+
const key = JSON.stringify(Object.entries(cssProperties).sort(([a], [b]) => a.localeCompare(b))) + directionValue;
|
|
149
|
+
const found = this.overrides.get(key);
|
|
150
|
+
if (found) return found.name;
|
|
151
|
+
const name = `idml-o-${this.overrides.size + 1}`;
|
|
152
|
+
this.overrides.set(key, {
|
|
153
|
+
name,
|
|
154
|
+
properties: cssProperties,
|
|
155
|
+
direction: directionValue
|
|
156
|
+
});
|
|
157
|
+
return name;
|
|
158
|
+
}
|
|
159
|
+
paragraphDirection(properties) {
|
|
160
|
+
return idmlDirection(properties.ParagraphDirection);
|
|
161
|
+
}
|
|
162
|
+
paragraphStyleDirection(properties) {
|
|
163
|
+
return this.paragraphDirection(properties) ?? this.bookDirection;
|
|
164
|
+
}
|
|
165
|
+
language(value, location) {
|
|
166
|
+
if (!value) return void 0;
|
|
167
|
+
const known = LANGUAGES[value];
|
|
168
|
+
if (!known) this.report.add(this.report.unsupportedLanguages, value, location);
|
|
169
|
+
return known;
|
|
170
|
+
}
|
|
171
|
+
rootLanguage() {
|
|
172
|
+
const root = this.rootStyle("paragraph");
|
|
173
|
+
return this.language(root?.properties.AppliedLanguage, root?.location ?? "Resources/Styles.xml");
|
|
174
|
+
}
|
|
175
|
+
css() {
|
|
176
|
+
const rules = [];
|
|
177
|
+
for (const id of this.used) {
|
|
178
|
+
const style = this.byId.get(id);
|
|
179
|
+
const name = this.classes.get(id);
|
|
180
|
+
if (!style || !name) continue;
|
|
181
|
+
const values = this.flattened(id, style.kind);
|
|
182
|
+
this.reportUnmappedStyleProperties(style);
|
|
183
|
+
this.report.font(values.AppliedFont, values.FontStyle, style.location);
|
|
184
|
+
const dir = style.kind === "paragraph" ? this.paragraphStyleDirection(values) : this.bookDirection;
|
|
185
|
+
const declarations = this.declarations(values, dir, style.location);
|
|
186
|
+
rules.push(`.${name}{${declarations}}`);
|
|
187
|
+
}
|
|
188
|
+
for (const value of this.overrides.values()) {
|
|
189
|
+
const declarations = this.declarations(value.properties, value.direction, value.name, this.report.unsupportedOverrideProperties);
|
|
190
|
+
if (declarations) rules.push(`.${value.name}{${declarations}}`);
|
|
191
|
+
}
|
|
192
|
+
return rules.join("");
|
|
193
|
+
}
|
|
194
|
+
declarations(values, dir, location, unsupported = this.report.unsupportedStyleProperties) {
|
|
195
|
+
const output = [];
|
|
196
|
+
const mapped = new Set(Object.keys(values).filter((name) => stylePropertyMapping(name)?.kind === "css"));
|
|
197
|
+
const pt = (name, property) => {
|
|
198
|
+
if (!mapped.has(name)) return;
|
|
199
|
+
const value = Number(values[name]);
|
|
200
|
+
if (Number.isFinite(value)) output.push(`${property}:${value}pt`);
|
|
201
|
+
};
|
|
202
|
+
pt("PointSize", "font-size");
|
|
203
|
+
if (mapped.has("Leading") && values.Leading === "Auto") output.push(`line-height:${(Number(values.AutoLeading) || 120) / 100}`);
|
|
204
|
+
else pt("Leading", "line-height");
|
|
205
|
+
const tracking = Number(values.Tracking);
|
|
206
|
+
if (mapped.has("Tracking") && Number.isFinite(tracking)) output.push(`letter-spacing:${tracking / 1e3}em`);
|
|
207
|
+
pt("SpaceBefore", "margin-block-start");
|
|
208
|
+
pt("SpaceAfter", "margin-block-end");
|
|
209
|
+
pt("FirstLineIndent", "text-indent");
|
|
210
|
+
const startIndent = dir === "rtl" ? "RightIndent" : "LeftIndent";
|
|
211
|
+
const endIndent = dir === "rtl" ? "LeftIndent" : "RightIndent";
|
|
212
|
+
pt(startIndent, "padding-inline-start");
|
|
213
|
+
pt(endIndent, "padding-inline-end");
|
|
214
|
+
textDeclarations(values, mapped, output);
|
|
215
|
+
if (mapped.has("Justification")) this.justification(values.Justification, dir, output, location, unsupported);
|
|
216
|
+
if (hasCssMapping(mapped, "keeps")) this.keeps(values, output);
|
|
217
|
+
if (mapped.has("StartParagraph") && values.StartParagraph && values.StartParagraph !== "Anywhere") this.report.add(unsupported, `StartParagraph:${values.StartParagraph}`, location);
|
|
218
|
+
const color = mapped.has("FillColor") ? this.color(values.FillColor, location) : void 0;
|
|
219
|
+
if (color) output.push(`color:${color}`);
|
|
220
|
+
return output.join(";");
|
|
221
|
+
}
|
|
222
|
+
findStyle(id, kind) {
|
|
223
|
+
if (!id) return void 0;
|
|
224
|
+
const exact = this.byId.get(id);
|
|
225
|
+
if (exact?.kind === kind) return exact;
|
|
226
|
+
return [...this.byId.values()].find((style) => style.kind === kind && (style.name === id || style.id.endsWith(`/${id}`)));
|
|
227
|
+
}
|
|
228
|
+
rootStyle(kind) {
|
|
229
|
+
return [...this.byId.values()].find((style) => style.kind === kind && (style.name === rootName(kind) || style.id.endsWith(`/${rootName(kind)}`)));
|
|
230
|
+
}
|
|
231
|
+
reportUnmappedStyleProperties(style) {
|
|
232
|
+
const root = this.rootStyle(style.kind);
|
|
233
|
+
const resolved = {};
|
|
234
|
+
for (const source of this.styleChain(style.id, style.kind)) {
|
|
235
|
+
Object.assign(resolved, source.properties);
|
|
236
|
+
for (const name of reportableStyleProperties(source.properties, resolved, source === root)) {
|
|
237
|
+
const key = `${source.id}\u0000${name}`;
|
|
238
|
+
if (this.reportedStyleProperties.has(key)) continue;
|
|
239
|
+
this.reportedStyleProperties.add(key);
|
|
240
|
+
this.report.add(this.report.unsupportedStyleProperties, name, source.location);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
justification(value, dir, output, location, bucket) {
|
|
245
|
+
if (!value) return;
|
|
246
|
+
const side = (physical) => physical === (dir === "rtl" ? "right" : "left") ? "start" : "end";
|
|
247
|
+
if (value === "LeftAlign" || value === "RightAlign") output.push(`text-align:${side(value === "LeftAlign" ? "left" : "right")}`);
|
|
248
|
+
else if (value === "CenterAlign") output.push("text-align:center");
|
|
249
|
+
else if (value === "FullyJustified") output.push("text-align:justify", "text-align-last:justify");
|
|
250
|
+
else if (value.endsWith("Justified")) {
|
|
251
|
+
output.push("text-align:justify");
|
|
252
|
+
if (value.startsWith("Left")) output.push(`text-align-last:${side("left")}`);
|
|
253
|
+
else if (value.startsWith("Right")) output.push(`text-align-last:${side("right")}`);
|
|
254
|
+
else if (value.startsWith("Center")) output.push("text-align-last:center");
|
|
255
|
+
} else if (value === "ToBindingSide" || value === "AwayFromBindingSide") this.report.add(bucket, `Justification:${value}`, location);
|
|
256
|
+
}
|
|
257
|
+
keeps(values, output) {
|
|
258
|
+
if ((Number(values.KeepWithNext) || 0) > 0) output.push("break-after:avoid");
|
|
259
|
+
if (values.KeepLinesTogether === "true" && values.KeepAllLinesTogether === "true") output.push("break-inside:avoid");
|
|
260
|
+
else if (values.KeepLinesTogether === "true") {
|
|
261
|
+
const first = Number(values.KeepFirstLines);
|
|
262
|
+
const last = Number(values.KeepLastLines);
|
|
263
|
+
if (Number.isInteger(first) && first > 0) output.push(`orphans:${first}`);
|
|
264
|
+
if (Number.isInteger(last) && last > 0) output.push(`widows:${last}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
color(id, location) {
|
|
268
|
+
if (!id || id.endsWith("/None")) return void 0;
|
|
269
|
+
if (id.endsWith("/Black")) return "#000";
|
|
270
|
+
if (id.endsWith("/Paper")) return "#fff";
|
|
271
|
+
const swatch = this.swatches.find((value) => value.id === id || value.name === id);
|
|
272
|
+
if (!swatch) return void 0;
|
|
273
|
+
if (swatch.model === "spot") {
|
|
274
|
+
this.report.add(this.report.spotColors, swatch.name, location);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (swatch.model === "rgb" && swatch.values?.length === 3) return `rgb(${swatch.values.join(" ")})`;
|
|
278
|
+
if (swatch.model === "cmyk" && swatch.values?.length === 4) {
|
|
279
|
+
const [c, m, y, k] = swatch.values.map((value) => value / 100);
|
|
280
|
+
return `rgb(${Math.round(255 * (1 - c) * (1 - k))} ${Math.round(255 * (1 - m) * (1 - k))} ${Math.round(255 * (1 - y) * (1 - k))})`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
//#endregion
|
|
285
|
+
export { StyleMapper, parseStyles, parseSwatches };
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
export interface IdmlTextNode {
|
|
3
|
+
name: string;
|
|
4
|
+
attributes: Record<string, string>;
|
|
5
|
+
text?: string;
|
|
6
|
+
children: IdmlTextNode[];
|
|
7
|
+
location: string;
|
|
8
|
+
}
|
|
9
|
+
export interface IdmlStyle {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
kind: 'paragraph' | 'character';
|
|
13
|
+
basedOn?: string;
|
|
14
|
+
properties: Record<string, string>;
|
|
15
|
+
location: string;
|
|
16
|
+
}
|
|
17
|
+
export interface IdmlSwatch {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
model: 'rgb' | 'cmyk' | 'spot' | 'named';
|
|
21
|
+
values?: number[];
|
|
22
|
+
}
|
|
23
|
+
export interface IdmlFramePreference {
|
|
24
|
+
columns: number;
|
|
25
|
+
gutter: number;
|
|
26
|
+
location?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface IdmlStory {
|
|
29
|
+
id: string;
|
|
30
|
+
part: string;
|
|
31
|
+
direction?: 'ltr' | 'rtl';
|
|
32
|
+
root?: IdmlTextNode;
|
|
33
|
+
frame?: IdmlFramePreference;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { localName } from "./xml.js";
|
|
2
|
+
//#region src/unsupported-elements.ts
|
|
3
|
+
const MAPPED_ELEMENTS = /* @__PURE__ */ new Set([
|
|
4
|
+
"Document",
|
|
5
|
+
"DocumentPreference",
|
|
6
|
+
"MarginPreference",
|
|
7
|
+
"MasterSpread",
|
|
8
|
+
"Spread",
|
|
9
|
+
"Page",
|
|
10
|
+
"Story",
|
|
11
|
+
"TextFrame",
|
|
12
|
+
"Group",
|
|
13
|
+
"TextFramePreference",
|
|
14
|
+
"PathGeometry",
|
|
15
|
+
"GeometryPathType",
|
|
16
|
+
"PathPointArray",
|
|
17
|
+
"PathPointType",
|
|
18
|
+
"Styles",
|
|
19
|
+
"Graphic",
|
|
20
|
+
"RootParagraphStyleGroup",
|
|
21
|
+
"ParagraphStyleGroup",
|
|
22
|
+
"RootCharacterStyleGroup",
|
|
23
|
+
"CharacterStyleGroup",
|
|
24
|
+
"ParagraphStyle",
|
|
25
|
+
"CharacterStyle",
|
|
26
|
+
"Color",
|
|
27
|
+
"Properties"
|
|
28
|
+
]);
|
|
29
|
+
function insideMappedResource(element) {
|
|
30
|
+
let parent = element.parent;
|
|
31
|
+
while (parent && "name" in parent) {
|
|
32
|
+
const name = localName(parent.name);
|
|
33
|
+
if (name === "ParagraphStyle" || name === "CharacterStyle" || name === "Color") return true;
|
|
34
|
+
parent = parent.parent;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
function collectUnsupportedElements(parts, report) {
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
if (part.name.startsWith("Stories/")) continue;
|
|
41
|
+
for (const element of part.all) {
|
|
42
|
+
const name = localName(element.name);
|
|
43
|
+
if (!MAPPED_ELEMENTS.has(name) && !insideMappedResource(element)) report.add(report.unsupportedElements, name, part.name);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return report.unsupportedElements;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { collectUnsupportedElements };
|
package/dist/xml.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { refusal } from "./refusal.js";
|
|
2
|
+
//#region src/xml.ts
|
|
3
|
+
function localName(name) {
|
|
4
|
+
return name.includes(":") ? name.split(":").at(-1) ?? name : name;
|
|
5
|
+
}
|
|
6
|
+
function numberAttribute(element, name, fallback) {
|
|
7
|
+
const raw = element?.attribs[name];
|
|
8
|
+
const value = raw === void 0 ? fallback : Number(raw);
|
|
9
|
+
if (value === void 0 || !Number.isFinite(value) || value < 0) throw refusal(`IDML ${name} must be a non-negative number`, `set ${name} to a non-negative numeric value`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function idmlDirection(value) {
|
|
13
|
+
const normalized = value?.toLowerCase();
|
|
14
|
+
if (normalized?.includes("righttoleft")) return "rtl";
|
|
15
|
+
if (normalized?.includes("lefttoright")) return "ltr";
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { idmlDirection, localName, numberAttribute };
|
package/dist/zip.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { refusal } from "./refusal.js";
|
|
2
|
+
import { IDML_LIMITS } from "./limits.js";
|
|
3
|
+
//#region src/zip.ts
|
|
4
|
+
const END_SIGNATURE = 101010256;
|
|
5
|
+
const CENTRAL_SIGNATURE = 33639248;
|
|
6
|
+
const LOCAL_SIGNATURE = 67324752;
|
|
7
|
+
const MAX_END_SEARCH = 65557;
|
|
8
|
+
function range(view, offset, length, label) {
|
|
9
|
+
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 0 || offset + length > view.byteLength) throw refusal(`the ZIP ${label} is outside the package`, "export a complete IDML package");
|
|
10
|
+
}
|
|
11
|
+
function findEnd(view) {
|
|
12
|
+
const first = Math.max(0, view.byteLength - MAX_END_SEARCH);
|
|
13
|
+
for (let offset = view.byteLength - 22; offset >= first; offset -= 1) if (view.getUint32(offset, true) === END_SIGNATURE) return offset;
|
|
14
|
+
throw refusal("the ZIP end-of-central-directory record is missing", "export a complete IDML package");
|
|
15
|
+
}
|
|
16
|
+
function validateCentralEntry(entry, startDisk, names) {
|
|
17
|
+
if ((entry.flags & 65) !== 0) throw refusal(`ZIP entry ${entry.name} is encrypted`, "export the IDML without ZIP encryption");
|
|
18
|
+
if (startDisk !== 0) throw refusal(`ZIP entry ${entry.name} spans disks`, "place every entry in one ordinary ZIP disk");
|
|
19
|
+
if (entry.compressedSize === 4294967295 || entry.inflatedSize === 4294967295 || entry.localOffset === 4294967295) throw refusal(`ZIP entry ${entry.name} uses ZIP64`, "export an ordinary ZIP package within the named IDML limits");
|
|
20
|
+
if (entry.method !== 0 && entry.method !== 8) throw refusal(`ZIP entry ${entry.name} uses compression method ${entry.method}`, "store it or use the deflate compression method");
|
|
21
|
+
if (names.has(entry.name)) throw refusal(`the ZIP contains duplicate entry ${entry.name}`, "give every IDML part one unique package path");
|
|
22
|
+
names.add(entry.name);
|
|
23
|
+
}
|
|
24
|
+
function centralEntries(view, limits) {
|
|
25
|
+
const end = findEnd(view);
|
|
26
|
+
range(view, end, 22, "end-of-central-directory record");
|
|
27
|
+
const disk = view.getUint16(end + 4, true);
|
|
28
|
+
const centralDisk = view.getUint16(end + 6, true);
|
|
29
|
+
const diskEntries = view.getUint16(end + 8, true);
|
|
30
|
+
const entries = view.getUint16(end + 10, true);
|
|
31
|
+
const centralSize = view.getUint32(end + 12, true);
|
|
32
|
+
const centralOffset = view.getUint32(end + 16, true);
|
|
33
|
+
range(view, end, 22 + view.getUint16(end + 20, true), "end-of-central-directory record");
|
|
34
|
+
if (disk !== 0 || centralDisk !== 0 || diskEntries !== entries) throw refusal("ZIP entries spanning disks are not supported", "place every entry in one ordinary ZIP disk");
|
|
35
|
+
if (entries === 65535 || centralSize === 4294967295 || centralOffset === 4294967295) throw refusal("ZIP64 is not supported", "export an ordinary ZIP package within the named IDML limits");
|
|
36
|
+
if (entries > limits.entries) throw refusal(`the ZIP entry count ${entries} exceeds the ${limits.entries} entry limit`, `reduce the package to ${limits.entries} entries or fewer`);
|
|
37
|
+
range(view, centralOffset, centralSize, "central directory");
|
|
38
|
+
const decoder = new TextDecoder();
|
|
39
|
+
const result = [];
|
|
40
|
+
const names = /* @__PURE__ */ new Set();
|
|
41
|
+
let offset = centralOffset;
|
|
42
|
+
for (let index = 0; index < entries; index += 1) {
|
|
43
|
+
range(view, offset, 46, "central-directory entry");
|
|
44
|
+
if (view.getUint32(offset, true) !== CENTRAL_SIGNATURE) throw refusal(`ZIP central-directory entry ${index + 1} is invalid`, "export a complete IDML package");
|
|
45
|
+
const flags = view.getUint16(offset + 8, true);
|
|
46
|
+
const method = view.getUint16(offset + 10, true);
|
|
47
|
+
const compressedSize = view.getUint32(offset + 20, true);
|
|
48
|
+
const inflatedSize = view.getUint32(offset + 24, true);
|
|
49
|
+
const nameLength = view.getUint16(offset + 28, true);
|
|
50
|
+
const extraLength = view.getUint16(offset + 30, true);
|
|
51
|
+
const entryCommentLength = view.getUint16(offset + 32, true);
|
|
52
|
+
const startDisk = view.getUint16(offset + 34, true);
|
|
53
|
+
const localOffset = view.getUint32(offset + 42, true);
|
|
54
|
+
const recordLength = 46 + nameLength + extraLength + entryCommentLength;
|
|
55
|
+
range(view, offset, recordLength, "central-directory entry");
|
|
56
|
+
const entry = {
|
|
57
|
+
name: decoder.decode(new Uint8Array(view.buffer, view.byteOffset + offset + 46, nameLength)),
|
|
58
|
+
flags,
|
|
59
|
+
method,
|
|
60
|
+
compressedSize,
|
|
61
|
+
inflatedSize,
|
|
62
|
+
localOffset
|
|
63
|
+
};
|
|
64
|
+
validateCentralEntry(entry, startDisk, names);
|
|
65
|
+
result.push(entry);
|
|
66
|
+
offset += recordLength;
|
|
67
|
+
}
|
|
68
|
+
if (offset !== centralOffset + centralSize) throw refusal("the ZIP central-directory size does not match its entries", "export a complete IDML package");
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
async function inflatedStream(bytes, entry) {
|
|
72
|
+
const source = new Blob([bytes.slice().buffer]).stream();
|
|
73
|
+
if (entry.method === 0) return source;
|
|
74
|
+
if (typeof DecompressionStream === "undefined") throw refusal("DecompressionStream is unavailable at run time", "use a runtime with DecompressionStream deflate-raw support or store every ZIP entry");
|
|
75
|
+
try {
|
|
76
|
+
const decompressor = new DecompressionStream("deflate-raw");
|
|
77
|
+
return source.pipeThrough(decompressor);
|
|
78
|
+
} catch {
|
|
79
|
+
throw refusal("DecompressionStream does not support deflate-raw at run time", "use a runtime with DecompressionStream deflate-raw support or store every ZIP entry");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function inflate(bytes, entry, limits, total) {
|
|
83
|
+
if (entry.inflatedSize > limits.partBytes) throw refusal(`ZIP entry ${entry.name} declares ${entry.inflatedSize} inflated bytes, exceeding the ${limits.partBytes} byte per-part limit`, `reduce that part to ${limits.partBytes} bytes or fewer`);
|
|
84
|
+
const reader = (await inflatedStream(bytes, entry)).getReader();
|
|
85
|
+
const chunks = [];
|
|
86
|
+
let size = 0;
|
|
87
|
+
while (true) {
|
|
88
|
+
let next;
|
|
89
|
+
try {
|
|
90
|
+
next = await reader.read();
|
|
91
|
+
} catch {
|
|
92
|
+
throw refusal(`ZIP entry ${entry.name} could not be inflated`, "export a complete IDML package");
|
|
93
|
+
}
|
|
94
|
+
if (next.done) break;
|
|
95
|
+
size += next.value.byteLength;
|
|
96
|
+
total.bytes += next.value.byteLength;
|
|
97
|
+
if (size > limits.partBytes) {
|
|
98
|
+
await reader.cancel();
|
|
99
|
+
throw refusal(`ZIP entry ${entry.name} exceeds the ${limits.partBytes} byte inflated per-part limit`, `reduce that part to ${limits.partBytes} bytes or fewer`);
|
|
100
|
+
}
|
|
101
|
+
if (total.bytes > limits.totalInflatedBytes) {
|
|
102
|
+
await reader.cancel();
|
|
103
|
+
throw refusal(`the ZIP exceeds the ${limits.totalInflatedBytes} byte total inflated limit`, `reduce all inflated parts to ${limits.totalInflatedBytes} bytes or fewer`);
|
|
104
|
+
}
|
|
105
|
+
chunks.push(next.value);
|
|
106
|
+
}
|
|
107
|
+
if (size !== entry.inflatedSize) throw refusal(`ZIP entry ${entry.name} declared ${entry.inflatedSize} inflated bytes but produced ${size}`, "export a complete IDML package");
|
|
108
|
+
const result = new Uint8Array(size);
|
|
109
|
+
let offset = 0;
|
|
110
|
+
for (const chunk of chunks) {
|
|
111
|
+
result.set(chunk, offset);
|
|
112
|
+
offset += chunk.byteLength;
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
function compressedBytes(bytes, view, entry) {
|
|
117
|
+
range(view, entry.localOffset, 30, `local header for ${entry.name}`);
|
|
118
|
+
if (view.getUint32(entry.localOffset, true) !== LOCAL_SIGNATURE) throw refusal(`the ZIP local header for ${entry.name} is invalid`, "export a complete IDML package");
|
|
119
|
+
const localFlags = view.getUint16(entry.localOffset + 6, true);
|
|
120
|
+
const localMethod = view.getUint16(entry.localOffset + 8, true);
|
|
121
|
+
if ((localFlags & 65) !== 0) throw refusal(`ZIP entry ${entry.name} is encrypted`, "export the IDML without ZIP encryption");
|
|
122
|
+
if (localMethod !== entry.method) throw refusal(`ZIP entry ${entry.name} has conflicting compression methods`, "export a complete IDML package");
|
|
123
|
+
const nameLength = view.getUint16(entry.localOffset + 26, true);
|
|
124
|
+
const extraLength = view.getUint16(entry.localOffset + 28, true);
|
|
125
|
+
const start = entry.localOffset + 30 + nameLength + extraLength;
|
|
126
|
+
range(view, start, entry.compressedSize, `data for ${entry.name}`);
|
|
127
|
+
return bytes.subarray(start, start + entry.compressedSize);
|
|
128
|
+
}
|
|
129
|
+
async function readZip(bytes, limits = IDML_LIMITS) {
|
|
130
|
+
if (bytes.byteLength > limits.packageBytes) throw refusal(`the IDML package has ${bytes.byteLength} bytes, exceeding the ${limits.packageBytes} byte package limit`, `reduce the package to ${limits.packageBytes} bytes or fewer`);
|
|
131
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
132
|
+
const entries = centralEntries(view, limits);
|
|
133
|
+
const result = [];
|
|
134
|
+
const total = { bytes: 0 };
|
|
135
|
+
for (const entry of entries) result.push({
|
|
136
|
+
name: entry.name,
|
|
137
|
+
bytes: await inflate(compressedBytes(bytes, view, entry), entry, limits, total)
|
|
138
|
+
});
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
export { readZip };
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@segnavia/idml",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.29.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/modstudio/segnavia.git",
|
|
9
|
+
"directory": "packages/idml"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"@segnavia/source": "./src/index.ts",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@segnavia/format": "0.29.0",
|
|
30
|
+
"@segnavia/html": "0.29.0",
|
|
31
|
+
"dom-serializer": "^2.0.0",
|
|
32
|
+
"domhandler": "^5.0.3",
|
|
33
|
+
"htmlparser2": "^10.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/README.md
DELETED