@segnavia/idml 0.28.0 → 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.
@@ -0,0 +1,52 @@
1
+ //#region src/style-mappings.ts
2
+ const css = (name) => ({
3
+ kind: "css",
4
+ css: name
5
+ });
6
+ const nonFormatting = { kind: "non-formatting" };
7
+ const STYLE_PROPERTY_MAPPINGS = {
8
+ PointSize: css("font-size"),
9
+ Leading: css("line-height"),
10
+ AutoLeading: css("line-height"),
11
+ Tracking: css("letter-spacing"),
12
+ SpaceBefore: css("margin-block-start"),
13
+ SpaceAfter: css("margin-block-end"),
14
+ FirstLineIndent: css("text-indent"),
15
+ Capitalization: css("capitalization"),
16
+ Underline: css("text-decoration-line"),
17
+ FillColor: css("color"),
18
+ LeftIndent: css("padding-inline"),
19
+ RightIndent: css("padding-inline"),
20
+ Justification: css("text-align"),
21
+ KeepWithNext: css("keeps"),
22
+ KeepLinesTogether: css("keeps"),
23
+ KeepAllLinesTogether: css("keeps"),
24
+ KeepFirstLines: css("keeps"),
25
+ KeepLastLines: css("keeps"),
26
+ StartParagraph: css("starts"),
27
+ ParagraphDirection: { kind: "direction" },
28
+ AppliedLanguage: { kind: "language" },
29
+ AppliedFont: { kind: "font" },
30
+ FontStyle: { kind: "font" },
31
+ SpanColumnType: { kind: "columns" },
32
+ Self: nonFormatting,
33
+ Name: nonFormatting,
34
+ BasedOn: nonFormatting,
35
+ AppliedParagraphStyle: nonFormatting,
36
+ AppliedCharacterStyle: nonFormatting,
37
+ EmitCss: nonFormatting,
38
+ StyleUniqueId: nonFormatting,
39
+ KeyboardShortcut: nonFormatting,
40
+ ExtendedKeyboardShortcut: nonFormatting,
41
+ Imported: nonFormatting,
42
+ PreviewColor: nonFormatting,
43
+ IncludeClass: nonFormatting,
44
+ EpubAriaRole: nonFormatting,
45
+ SplitDocument: nonFormatting,
46
+ NextStyle: nonFormatting
47
+ };
48
+ function stylePropertyMapping(name) {
49
+ return STYLE_PROPERTY_MAPPINGS[name];
50
+ }
51
+ //#endregion
52
+ export { STYLE_PROPERTY_MAPPINGS, stylePropertyMapping };
@@ -0,0 +1,236 @@
1
+ import { stylePropertyMapping } from "./style-mappings.js";
2
+ //#region src/style-property-report.ts
3
+ const INDESIGN_PROPERTY_DEFAULTS = {
4
+ HorizontalScale: [100],
5
+ VerticalScale: [100],
6
+ Skew: [0],
7
+ BaselineShift: [0],
8
+ DesiredWordSpacing: [100],
9
+ MinimumWordSpacing: [80],
10
+ MaximumWordSpacing: [133],
11
+ DesiredLetterSpacing: [0],
12
+ MinimumLetterSpacing: [0],
13
+ MaximumLetterSpacing: [0],
14
+ DesiredGlyphScaling: [100],
15
+ MinimumGlyphScaling: [100],
16
+ MaximumGlyphScaling: [100],
17
+ FillTint: [-1, 100],
18
+ KerningMethod: ["$ID/Metrics"],
19
+ Ligatures: [true],
20
+ OTFContextualAlternate: [true],
21
+ OTFMark: [true],
22
+ OTFFigureStyle: ["Default"],
23
+ OTFLocale: [true],
24
+ PositionalForm: ["None"],
25
+ CharacterAlignment: ["AlignEmCenter"],
26
+ DigitsType: ["DefaultDigits"],
27
+ CharacterDirection: ["DefaultDirection"],
28
+ DiacriticPosition: ["OpentypePositionFromBaseline"],
29
+ XOffsetDiacritic: [0],
30
+ YOffsetDiacritic: [0],
31
+ Kashidas: ["DefaultKashidas"],
32
+ ParagraphKashidaWidth: [2],
33
+ ParagraphJustification: ["DefaultJustification"],
34
+ GlyphForm: ["None"],
35
+ MiterLimit: [4],
36
+ EndJoin: ["MiterEndJoin"],
37
+ NoBreak: [false],
38
+ SingleWordJustification: ["LeftAlign"],
39
+ "OTF*": [false]
40
+ };
41
+ const JUSTIFICATION_SETTINGS = /* @__PURE__ */ new Set([
42
+ "DesiredWordSpacing",
43
+ "MinimumWordSpacing",
44
+ "MaximumWordSpacing",
45
+ "DesiredLetterSpacing",
46
+ "MinimumLetterSpacing",
47
+ "MaximumLetterSpacing",
48
+ "DesiredGlyphScaling",
49
+ "MinimumGlyphScaling",
50
+ "MaximumGlyphScaling",
51
+ "SingleWordJustification"
52
+ ]);
53
+ const CJK_PREFIXES = [
54
+ "Kinsoku",
55
+ "Shatai",
56
+ "CjkGrid",
57
+ "Grid",
58
+ "AutoTcy"
59
+ ];
60
+ const CJK_PROPERTIES = /* @__PURE__ */ new Set([
61
+ "Mojikumi",
62
+ "Jidori",
63
+ "Rensuuji",
64
+ "BunriKinshi",
65
+ "Tsume",
66
+ "LeadingAki",
67
+ "TrailingAki",
68
+ "Gyoudori",
69
+ "ParagraphGyoudori",
70
+ "KeyboardDirection",
71
+ "TreatIdeographicSpaceAsSpace",
72
+ "CharacterRotation",
73
+ "RotateSingleByteCharacters"
74
+ ]);
75
+ function truthy(value) {
76
+ return value?.trim().toLowerCase() === "true";
77
+ }
78
+ function notNone(value) {
79
+ const normalized = value?.trim().toLowerCase();
80
+ return Boolean(normalized && ![
81
+ "none",
82
+ "$id/none",
83
+ "nolist"
84
+ ].includes(normalized));
85
+ }
86
+ const SWITCHED_GROUPS = [
87
+ {
88
+ matches: (name) => name.startsWith("RuleAbove"),
89
+ enabled: (values) => truthy(values.RuleAbove),
90
+ reportAs: "RuleAbove"
91
+ },
92
+ {
93
+ matches: (name) => name.startsWith("RuleBelow"),
94
+ enabled: (values) => truthy(values.RuleBelow),
95
+ reportAs: "RuleBelow"
96
+ },
97
+ {
98
+ matches: (name) => name.startsWith("ParagraphBorder"),
99
+ enabled: (values) => truthy(values.ParagraphBorderOn),
100
+ reportAs: "ParagraphBorderOn"
101
+ },
102
+ {
103
+ matches: (name) => name.startsWith("ParagraphShading"),
104
+ enabled: (values) => truthy(values.ParagraphShadingOn),
105
+ reportAs: "ParagraphShadingOn"
106
+ },
107
+ {
108
+ matches: (name) => name.startsWith("Underline"),
109
+ enabled: (values) => truthy(values.Underline) || values.Underline === "Underline",
110
+ reportAs: "Underline"
111
+ },
112
+ {
113
+ matches: (name) => name.startsWith("StrikeThrough") || name.startsWith("StrikeThru"),
114
+ enabled: (values) => truthy(values.StrikeThru ?? values.StrikeThrough),
115
+ reportAs: "StrikeThru"
116
+ },
117
+ {
118
+ matches: (name) => name.startsWith("Bullets") || name.startsWith("Numbering"),
119
+ enabled: (values) => notNone(values.BulletsAndNumberingListType),
120
+ reportAs: "BulletsAndNumberingListType"
121
+ },
122
+ {
123
+ matches: (name) => name.startsWith("DropCap") || name === "DropcapDetail",
124
+ enabled: (values) => Number(values.DropCapLines) > 0,
125
+ reportAs: "DropCapLines"
126
+ },
127
+ {
128
+ matches: (name) => name.startsWith("Kenten"),
129
+ enabled: (values) => notNone(values.KentenKind),
130
+ reportAs: "KentenKind"
131
+ },
132
+ {
133
+ matches: (name) => name.startsWith("Ruby"),
134
+ enabled: (values) => truthy(values.RubyFlag),
135
+ reportAs: "RubyFlag"
136
+ },
137
+ {
138
+ matches: (name) => name.startsWith("Warichu"),
139
+ enabled: (values) => truthy(values.Warichu),
140
+ reportAs: "Warichu"
141
+ },
142
+ {
143
+ matches: (name) => name.startsWith("Tatechuyoko"),
144
+ enabled: (values) => truthy(values.Tatechuyoko),
145
+ reportAs: "Tatechuyoko"
146
+ },
147
+ {
148
+ matches: (name) => name.startsWith("Gradient"),
149
+ enabled: (values) => [values.FillColor, values.StrokeColor].some((value) => value?.toLowerCase().includes("gradient")),
150
+ reportAs: "Gradient"
151
+ },
152
+ {
153
+ matches: (name) => name.startsWith("Stroke"),
154
+ enabled: (values) => Number(values.StrokeWeight) > 0 && notNone(values.StrokeColor),
155
+ reportAs: "Stroke"
156
+ }
157
+ ];
158
+ function equalsDefault(name, value) {
159
+ const defaults = INDESIGN_PROPERTY_DEFAULTS[name] ?? (name.startsWith("OTF") ? INDESIGN_PROPERTY_DEFAULTS["OTF*"] : void 0);
160
+ if (!defaults) return false;
161
+ const normalized = value.trim().toLowerCase();
162
+ return defaults.some((candidate) => String(candidate).toLowerCase() === normalized);
163
+ }
164
+ function hasEntries(value) {
165
+ const normalized = value?.trim().toLowerCase();
166
+ return Boolean(normalized && ![
167
+ "false",
168
+ "0",
169
+ "none",
170
+ "$id/none",
171
+ "[]"
172
+ ].includes(normalized));
173
+ }
174
+ function isCjkLanguage(value) {
175
+ return /(?:chinese|japanese|korean|^zh(?:-|$)|^ja(?:-|$)|^ko(?:-|$))/i.test(value ?? "");
176
+ }
177
+ function activeIdmlValue(value) {
178
+ const normalized = value.trim().toLowerCase();
179
+ if (!normalized || [
180
+ "false",
181
+ "none",
182
+ "nothing",
183
+ "$id/none",
184
+ "$id/nothing",
185
+ "n",
186
+ "no",
187
+ "off"
188
+ ].includes(normalized)) return false;
189
+ const numbers = normalized.split(/[\s,]+/).map(Number);
190
+ return numbers.length === 0 || !numbers.every((number) => Number.isFinite(number) && number === 0);
191
+ }
192
+ function isHyphenationProperty(name) {
193
+ return name.startsWith("Hyphenat") || name === "HyphenWeight" || name === "ProviderHyphenationStyle";
194
+ }
195
+ function groupedProperties(names, resolved) {
196
+ const output = [];
197
+ if (names.some(isHyphenationProperty) && truthy(resolved.Hyphenation)) output.push("Hyphenation");
198
+ const changedJustification = [...JUSTIFICATION_SETTINGS].some((name) => {
199
+ const value = resolved[name];
200
+ return value !== void 0 && !equalsDefault(name, value);
201
+ });
202
+ if (names.some((name) => JUSTIFICATION_SETTINGS.has(name)) && changedJustification) output.push("JustificationSettings");
203
+ return output;
204
+ }
205
+ function reportableProperty(name, value, resolved, root) {
206
+ if (stylePropertyMapping(name)) return void 0;
207
+ if (isHyphenationProperty(name) || JUSTIFICATION_SETTINGS.has(name)) return void 0;
208
+ if ([
209
+ "EmptyGrepStyles",
210
+ "EmptyNestedStyles",
211
+ "EmptyLineStyles"
212
+ ].includes(name)) return void 0;
213
+ if ([
214
+ "AllGREPStyles",
215
+ "AllNestedStyles",
216
+ "AllLineStyles"
217
+ ].includes(name) || name === "TabList") return hasEntries(value) ? name : void 0;
218
+ const switched = SWITCHED_GROUPS.find((group) => group.matches(name));
219
+ if (switched) return switched.enabled(resolved) ? switched.reportAs : void 0;
220
+ if ((CJK_PREFIXES.some((prefix) => name.startsWith(prefix)) || CJK_PROPERTIES.has(name)) && !isCjkLanguage(resolved.AppliedLanguage)) return void 0;
221
+ if (equalsDefault(name, value)) return void 0;
222
+ return !root || activeIdmlValue(value) ? name : void 0;
223
+ }
224
+ function reportableStyleProperties(source, resolved, root = false) {
225
+ const names = Object.keys(source);
226
+ const output = new Set(groupedProperties(names, resolved));
227
+ for (const name of names) {
228
+ const value = source[name];
229
+ if (value === void 0) continue;
230
+ const reported = reportableProperty(name, value, resolved, root);
231
+ if (reported) output.add(reported);
232
+ }
233
+ return [...output];
234
+ }
235
+ //#endregion
236
+ export { INDESIGN_PROPERTY_DEFAULTS, activeIdmlValue, reportableStyleProperties };
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 };
@@ -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 };