@segnavia/idml 0.28.0 → 0.30.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/dist/styles.js ADDED
@@ -0,0 +1,285 @@
1
+ import { idmlDirection, localName } from "./xml.js";
2
+ import { stylePropertyMapping } from "./style-mappings.js";
3
+ import { reportableStyleProperties } from "./style-property-report.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 };
@@ -0,0 +1,90 @@
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
+ export interface IdmlTextVariable {
36
+ id: string;
37
+ name: string;
38
+ type: string;
39
+ appliedCharacterStyle?: string;
40
+ searchStrategy?: string;
41
+ location: string;
42
+ }
43
+ export interface IdmlMargin {
44
+ top: number;
45
+ bottom: number;
46
+ left: number;
47
+ right: number;
48
+ }
49
+ export interface IdmlDocumentPreferences {
50
+ pageWidth: number;
51
+ pageHeight: number;
52
+ facingPages: boolean;
53
+ binding: 'left-to-right' | 'right-to-left';
54
+ margin: IdmlMargin;
55
+ }
56
+ export interface IdmlPage {
57
+ id: string;
58
+ appliedMaster?: string;
59
+ stories: string[];
60
+ side?: 'left' | 'right' | 'single';
61
+ }
62
+ export interface IdmlSpread {
63
+ id: string;
64
+ pages: IdmlPage[];
65
+ }
66
+ export interface IdmlMasterFrame {
67
+ id: string;
68
+ story: string;
69
+ bounds: {
70
+ left: number;
71
+ top: number;
72
+ width: number;
73
+ height: number;
74
+ };
75
+ }
76
+ export interface IdmlMasterPage extends IdmlPage {
77
+ margin: IdmlMargin;
78
+ frames: IdmlMasterFrame[];
79
+ }
80
+ export interface IdmlMasterSpread {
81
+ id: string;
82
+ name: string;
83
+ basedOn?: string;
84
+ pages: IdmlMasterPage[];
85
+ unmappedItems: {
86
+ type: string;
87
+ id: string;
88
+ }[];
89
+ }
90
+ //#endregion
@@ -0,0 +1,51 @@
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
+ "TextVariable",
28
+ "MatchCharacterStylePreference",
29
+ "Properties"
30
+ ]);
31
+ function insideMappedResource(element) {
32
+ let parent = element.parent;
33
+ while (parent && "name" in parent) {
34
+ const name = localName(parent.name);
35
+ if (name === "ParagraphStyle" || name === "CharacterStyle" || name === "Color") return true;
36
+ parent = parent.parent;
37
+ }
38
+ return false;
39
+ }
40
+ function collectUnsupportedElements(parts, report) {
41
+ for (const part of parts) {
42
+ if (part.name.startsWith("Stories/")) continue;
43
+ for (const element of part.all) {
44
+ const name = localName(element.name);
45
+ if (!MAPPED_ELEMENTS.has(name) && !insideMappedResource(element)) report.add(report.unsupportedElements, name, part.name);
46
+ }
47
+ }
48
+ return report.unsupportedElements;
49
+ }
50
+ //#endregion
51
+ export { collectUnsupportedElements };
@@ -0,0 +1,21 @@
1
+ //#region src/variables.ts
2
+ const MATCH_CHARACTER_STYLE = "MatchCharacterStyleType";
3
+ function matchVariables(variables, report) {
4
+ const result = [];
5
+ const matchedStyles = /* @__PURE__ */ new Set();
6
+ for (const variable of variables) {
7
+ if (variable.type !== MATCH_CHARACTER_STYLE || !variable.appliedCharacterStyle) {
8
+ report.add(report.unsupportedElements, `TextVariable:${variable.name}`, variable.location);
9
+ continue;
10
+ }
11
+ if (matchedStyles.has(variable.appliedCharacterStyle)) {
12
+ report.add(report.unsupportedElements, `TextVariable duplicate style:${variable.name}`, variable.location);
13
+ continue;
14
+ }
15
+ matchedStyles.add(variable.appliedCharacterStyle);
16
+ result.push(variable);
17
+ }
18
+ return result;
19
+ }
20
+ //#endregion
21
+ export { matchVariables };
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segnavia/idml",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,8 +26,8 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@segnavia/format": "0.28.0",
30
- "@segnavia/html": "0.28.0",
29
+ "@segnavia/format": "0.30.0",
30
+ "@segnavia/html": "0.30.0",
31
31
  "dom-serializer": "^2.0.0",
32
32
  "domhandler": "^5.0.3",
33
33
  "htmlparser2": "^10.0.0"