@segnavia/idml 0.29.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/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IdmlFontUse, IdmlReportData } from "./report.js";
2
2
  import { IdmlImportOptions, IdmlImportReport, IdmlImportResult, importIdml } from "./import.js";
3
3
  import { IDML_LIMITS, MAX_IDML_ENTRIES, MAX_IDML_PACKAGE_BYTES, MAX_IDML_PART_BYTES, MAX_IDML_TOTAL_INFLATED_BYTES, MAX_IDML_XML_DEPTH } from "./limits.js";
4
- import { IdmlStory } from "./types.js";
5
- import { IdmlDocumentPreferences, IdmlMargin, IdmlMasterSpread, IdmlModel, IdmlPage, IdmlSpread } from "./model.js";
4
+ import { IdmlDocumentPreferences, IdmlMargin, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory } from "./types.js";
5
+ import { IdmlModel } from "./model.js";
6
6
  export { IDML_LIMITS, type IdmlDocumentPreferences, type IdmlFontUse, type IdmlImportOptions, type IdmlImportReport, type IdmlImportResult, type IdmlMargin, type IdmlMasterSpread, type IdmlModel, type IdmlPage, type IdmlReportData, type IdmlSpread, type IdmlStory, MAX_IDML_ENTRIES, MAX_IDML_PACKAGE_BYTES, MAX_IDML_PART_BYTES, MAX_IDML_TOTAL_INFLATED_BYTES, MAX_IDML_XML_DEPTH, importIdml };
package/dist/map-book.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { refusal } from "./refusal.js";
2
- import { ReportCollector } from "./report.js";
2
+ import { logicalMargin } from "./margins.js";
3
3
  import { storyHtml } from "./stories.js";
4
+ import { chapterParents, mapMasters } from "./masters.js";
5
+ import { ReportCollector } from "./report.js";
4
6
  import { StyleMapper } from "./styles.js";
7
+ import { matchVariables } from "./variables.js";
5
8
  import { Book, MM_PER_PT } from "@segnavia/format";
6
9
  import { findActiveContent } from "@segnavia/html";
7
10
  //#region src/map-book.ts
@@ -25,26 +28,38 @@ function bodyColumnGap(model, stories, report) {
25
28
  }
26
29
  return selected;
27
30
  }
31
+ function refuseActiveBookHtml(chapters, parents) {
32
+ const html = [...chapters.map((chapter) => chapter.html), ...parents.flatMap((parent) => [parent.head ?? "", parent.foot ?? ""])].join("");
33
+ const active = findActiveContent(html);
34
+ if (active.length > 0) throw refusal(`the imported Book contains active content (${active.join(", ")})`, "remove that active content");
35
+ }
28
36
  function modelToBook(model, report = new ReportCollector(model.report)) {
29
37
  const preferences = model.preferences;
30
38
  const rightBinding = preferences.binding === "right-to-left";
31
39
  const bookDirection = rightBinding ? "rtl" : "ltr";
32
40
  const styles = new StyleMapper(model.styles ?? [], model.swatches ?? [], report, bookDirection);
41
+ const matchedVariables = matchVariables(model.textVariables ?? [], report);
33
42
  const bookLanguage = styles.rootLanguage();
34
43
  const stories = orderedStories(model);
44
+ const masters = mapMasters(model, matchedVariables, styles, report);
35
45
  const chapters = stories.map((story) => ({
36
46
  story,
37
- mapped: storyHtml(story, styles, report, bookDirection, bookLanguage)
38
- })).map(({ story, mapped: value }) => ({
39
- title: story.id,
40
- html: value.html
41
- }));
47
+ mapped: storyHtml({
48
+ ...story,
49
+ textVariables: matchedVariables
50
+ }, styles, report, bookDirection, bookLanguage)
51
+ })).map(({ story, mapped: value }) => {
52
+ const parents = chapterParents(story.id, model, masters, report);
53
+ return {
54
+ title: story.id,
55
+ html: value.html,
56
+ ...parents ? { parents } : {}
57
+ };
58
+ });
42
59
  if (chapters.length === 0) throw refusal("the IDML has no stories", "add at least one Story");
43
- const html = chapters.map((chapter) => chapter.html).join("");
44
- const active = findActiveContent(html);
45
- if (active.length > 0) throw refusal(`the imported Book contains active content (${active.join(", ")})`, "remove that active content");
60
+ refuseActiveBookHtml(chapters, masters.parents);
46
61
  const columnGap = bodyColumnGap(model, stories, report);
47
- const css = styles.css();
62
+ const css = `${styles.css()}${masters.css}`;
48
63
  return Book.parse({
49
64
  title: model.id,
50
65
  mode: "paginated",
@@ -54,20 +69,16 @@ function modelToBook(model, report = new ReportCollector(model.report)) {
54
69
  height: preferences.pageHeight,
55
70
  unit: "pt"
56
71
  },
57
- margin: {
58
- top: preferences.margin.top * MM_PER_PT,
59
- bottom: preferences.margin.bottom * MM_PER_PT,
60
- inner: (rightBinding ? preferences.margin.right : preferences.margin.left) * MM_PER_PT,
61
- outer: (rightBinding ? preferences.margin.left : preferences.margin.right) * MM_PER_PT
62
- },
72
+ margin: masters.bookMargin ?? logicalMargin(preferences.margin, rightBinding ? "left" : "right"),
63
73
  ...columnGap === void 0 ? {} : { columnGap: columnGap * MM_PER_PT }
64
74
  },
65
75
  dir: bookDirection,
66
76
  ...bookLanguage ? { lang: bookLanguage } : {},
67
77
  ...css ? { css } : {},
68
78
  rectoOpenings: preferences.facingPages,
79
+ parentPages: masters.parents,
69
80
  chapters
70
81
  });
71
82
  }
72
83
  //#endregion
73
- export { modelToBook };
84
+ export { modelToBook, refuseActiveBookHtml };
@@ -0,0 +1,21 @@
1
+ import { numberAttribute } from "./xml.js";
2
+ import { MM_PER_PT } from "@segnavia/format";
3
+ //#region src/margins.ts
4
+ function readMargin(element) {
5
+ return {
6
+ top: numberAttribute(element, "Top", 0),
7
+ bottom: numberAttribute(element, "Bottom", 0),
8
+ left: numberAttribute(element, "Left", 0),
9
+ right: numberAttribute(element, "Right", 0)
10
+ };
11
+ }
12
+ function logicalMargin(margin, side) {
13
+ return {
14
+ top: margin.top * MM_PER_PT,
15
+ bottom: margin.bottom * MM_PER_PT,
16
+ inner: (side === "left" ? margin.right : margin.left) * MM_PER_PT,
17
+ outer: (side === "left" ? margin.left : margin.right) * MM_PER_PT
18
+ };
19
+ }
20
+ //#endregion
21
+ export { logicalMargin, readMargin };
@@ -0,0 +1,200 @@
1
+ import { logicalMargin } from "./margins.js";
2
+ import { storyHtml } from "./stories.js";
3
+ import { BOOK_PARENT_NAMES, MM_PER_PT } from "@segnavia/format";
4
+ import serialize from "dom-serializer";
5
+ import { Element } from "domhandler";
6
+ import { parseDocument } from "htmlparser2";
7
+ const slug = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "master";
8
+ const mm = (points) => `${Number((points * MM_PER_PT).toFixed(6))}mm`;
9
+ function roleFor(page, rtl, ordinal) {
10
+ if (!page.side || page.side === "single") return (ordinal ?? 0) % 2 === 0 ? "recto" : "verso";
11
+ return page.side === (rtl ? "left" : "right") ? "recto" : "verso";
12
+ }
13
+ function masterPage(master, role, rtl) {
14
+ if (master.pages.length === 1) return master.pages[0];
15
+ const side = role === "recto" ? rtl ? "left" : "right" : rtl ? "right" : "left";
16
+ return master.pages.find((page) => page.side === side);
17
+ }
18
+ function parentName(master, role) {
19
+ return `idml-${slug(master.name)}-${master.pages.length === 1 ? "recto" : role}`;
20
+ }
21
+ function frameRegion(page, frame, height) {
22
+ if (frame.bounds.top + frame.bounds.height <= page.margin.top) return "head";
23
+ if (frame.bounds.top >= height - page.margin.bottom) return "foot";
24
+ }
25
+ function regionHtml(master, page, region, context) {
26
+ const boxes = [];
27
+ for (const frame of page.frames) {
28
+ if (frameRegion(page, frame, context.model.preferences.pageHeight) !== region) continue;
29
+ const story = context.stories.get(frame.story);
30
+ if (!story) {
31
+ context.report.add(context.report.unsupportedElements, "master story missing", `${master.name}#${frame.id}`);
32
+ continue;
33
+ }
34
+ const className = `idml-master-frame-${context.css.length}`;
35
+ const mapped = storyHtml({
36
+ ...story,
37
+ textVariables: context.variables,
38
+ master: true
39
+ }, context.styles, context.report, context.rtl ? "rtl" : "ltr", void 0);
40
+ const nodes = parseDocument(mapped.html, { xmlMode: false }).children;
41
+ boxes.push(new Element("div", { class: className }, nodes));
42
+ const right = frame.bounds.left + frame.bounds.width;
43
+ const inlineStart = context.rtl ? context.model.preferences.pageWidth - page.margin.right - right : frame.bounds.left - page.margin.left;
44
+ const blockStart = region === "head" ? frame.bounds.top : frame.bounds.top - (context.model.preferences.pageHeight - page.margin.bottom);
45
+ context.css.push(`:where(.${className}){position:absolute;inset-inline-start:${mm(inlineStart)};inline-size:${mm(frame.bounds.width)};inset-block-start:${mm(blockStart)};block-size:${mm(frame.bounds.height)}}`);
46
+ context.mappedStories.add(frame.story);
47
+ }
48
+ if (boxes.length === 0) return void 0;
49
+ return serialize(new Element("div", { class: "idml-master-region" }, boxes), {
50
+ xmlMode: false,
51
+ encodeEntities: "utf8"
52
+ });
53
+ }
54
+ function pageSide(page, role, rtl) {
55
+ if (page.side === "left" || page.side === "right") return page.side;
56
+ return role === "recto" ? rtl ? "left" : "right" : rtl ? "right" : "left";
57
+ }
58
+ function buildParent(master, role, byId, context) {
59
+ const page = masterPage(master, role, context.rtl);
60
+ if (!page) return void 0;
61
+ const base = master.basedOn ? byId.get(master.basedOn) : void 0;
62
+ const head = regionHtml(master, page, "head", context);
63
+ const foot = regionHtml(master, page, "foot", context);
64
+ for (const frame of page.frames) if (!frameRegion(page, frame, context.model.preferences.pageHeight)) context.report.add(context.report.unsupportedElements, "master TextFrame", `${master.name}#${frame.id}`);
65
+ return {
66
+ name: parentName(master, role),
67
+ ...base ? { basedOn: parentName(base, role) } : {},
68
+ margin: logicalMargin(page.margin, pageSide(page, role, context.rtl)),
69
+ ...head ? { head } : {},
70
+ ...foot ? { foot } : {}
71
+ };
72
+ }
73
+ function reportExtraPages(master, selected, report) {
74
+ for (const page of master.pages) {
75
+ if (selected.has(page.id)) continue;
76
+ report.add(report.unsupportedElements, "master page", `${master.name}#${page.id}`);
77
+ for (const frame of page.frames) report.add(report.unsupportedElements, "master TextFrame", `${master.name}#${frame.id}`);
78
+ }
79
+ }
80
+ function buildParents(masters, byId, context) {
81
+ const result = [];
82
+ for (const master of masters) {
83
+ const roles = master.pages.length === 1 ? ["recto"] : ["recto", "verso"];
84
+ const selected = /* @__PURE__ */ new Set();
85
+ for (const role of roles) {
86
+ const page = masterPage(master, role, context.rtl);
87
+ if (page) selected.add(page.id);
88
+ const parent = buildParent(master, role, byId, context);
89
+ if (parent) result.push(parent);
90
+ }
91
+ reportExtraPages(master, selected, context.report);
92
+ for (const item of master.unmappedItems) context.report.add(context.report.unsupportedElements, `master ${item.type}`, `${master.name}#${item.id}`);
93
+ }
94
+ return result;
95
+ }
96
+ function emittedMasters(model, applied) {
97
+ const byId = new Map(model.masterSpreads.map((master) => [master.id, master]));
98
+ const emitted = new Set(applied);
99
+ for (const id of emitted) {
100
+ const basedOn = byId.get(id)?.basedOn;
101
+ if (basedOn) emitted.add(basedOn);
102
+ }
103
+ return model.masterSpreads.filter((master) => emitted.has(master.id));
104
+ }
105
+ function appliedBookMargin(model, applied, report) {
106
+ const masters = model.masterSpreads.filter((master) => applied.has(master.id));
107
+ if (masters.length !== applied.size) return void 0;
108
+ const pages = masters.flatMap((master) => master.pages.map((page) => ({
109
+ master,
110
+ page
111
+ })));
112
+ const first = pages[0]?.page.margin;
113
+ if (!first) return void 0;
114
+ const signature = (margin) => `${margin.top},${margin.bottom},${margin.left},${margin.right}`;
115
+ if (pages.some(({ page }) => signature(page.margin) !== signature(first))) {
116
+ for (const master of masters) {
117
+ const margins = [...new Set(master.pages.map((page) => signature(page.margin)))];
118
+ report.add(report.unsupportedElements, "master margins disagree", `${master.name}:${margins.join("|")}`);
119
+ }
120
+ return;
121
+ }
122
+ return logicalMargin(first, model.preferences.binding === "right-to-left" ? "left" : "right");
123
+ }
124
+ function mapMasters(model, variables, styles, report) {
125
+ const rtl = model.preferences.binding === "right-to-left";
126
+ const applied = new Set(model.spreads.flatMap((spread) => spread.pages.map((page) => page.appliedMaster).filter((id) => Boolean(id && id !== "n"))));
127
+ const masters = emittedMasters(model, applied);
128
+ const byId = new Map(model.masterSpreads.map((master) => [master.id, master]));
129
+ for (const spread of model.spreads) for (const page of spread.pages) {
130
+ const id = page.appliedMaster;
131
+ if (id && id !== "n" && !byId.has(id)) report.addOnce(report.unsupportedElements, `applied master unknown:${id}`, page.id);
132
+ }
133
+ const css = [":where(.idml-master-region){position:relative;block-size:100%}"];
134
+ const mappedStories = /* @__PURE__ */ new Set();
135
+ const parents = buildParents(masters, byId, {
136
+ model,
137
+ variables,
138
+ styles,
139
+ report,
140
+ rtl,
141
+ stories: new Map(model.stories.map((story) => [story.id, story])),
142
+ mappedStories,
143
+ css
144
+ });
145
+ if (model.spreads.some((spread) => spread.pages.some((page) => !page.appliedMaster || page.appliedMaster === "n"))) parents.push({
146
+ name: "idml-none",
147
+ basedOn: BOOK_PARENT_NAMES.blank
148
+ });
149
+ for (const story of model.masterStoryIds) if (!mappedStories.has(story)) report.add(report.unsupportedElements, "master story", story);
150
+ const parentFor = (masterId, role, location) => {
151
+ if (!masterId || masterId === "n") return "idml-none";
152
+ const master = byId.get(masterId);
153
+ if (master && applied.has(masterId)) return parentName(master, role);
154
+ report.addOnce(report.unsupportedElements, `applied master unknown:${masterId}`, location);
155
+ return role === "recto" ? BOOK_PARENT_NAMES.recto : BOOK_PARENT_NAMES.verso;
156
+ };
157
+ return {
158
+ parents,
159
+ css: css.join(""),
160
+ bookMargin: appliedBookMargin(model, applied, report),
161
+ parentFor,
162
+ mappedStories
163
+ };
164
+ }
165
+ function chapterParents(story, model, mapping, report) {
166
+ const rtl = model.preferences.binding === "right-to-left";
167
+ const pages = [];
168
+ let ordinal = 0;
169
+ for (const spread of model.spreads) for (const page of spread.pages) {
170
+ if (page.stories.includes(story)) pages.push({
171
+ page,
172
+ ordinal
173
+ });
174
+ ordinal += 1;
175
+ }
176
+ const first = pages[0];
177
+ if (!first) return void 0;
178
+ const firstRole = roleFor(first.page, rtl, first.ordinal);
179
+ const result = { opener: mapping.parentFor(first.page.appliedMaster, firstRole, first.page.id) };
180
+ for (const role of ["recto", "verso"]) {
181
+ const candidates = pages.slice(1).filter((entry) => roleFor(entry.page, rtl, entry.ordinal) === role);
182
+ if (candidates.length === 0) continue;
183
+ const counts = /* @__PURE__ */ new Map();
184
+ for (const entry of candidates) {
185
+ const id = entry.page.appliedMaster ?? "n";
186
+ counts.set(id, (counts.get(id) ?? 0) + 1);
187
+ }
188
+ const chosen = [...counts].sort((a, b) => b[1] - a[1])[0]?.[0];
189
+ if (chosen) result[role] = mapping.parentFor(chosen, role, candidates[0]?.page.id ?? story);
190
+ if (chosen && counts.size > 1) report.mixedMasterChapters.push({
191
+ chapter: story,
192
+ side: role,
193
+ masters: [...counts.keys()],
194
+ chosen
195
+ });
196
+ }
197
+ return result;
198
+ }
199
+ //#endregion
200
+ export { chapterParents, mapMasters };
package/dist/model.d.ts CHANGED
@@ -1,32 +1,6 @@
1
1
  import { IdmlReportData } from "./report.js";
2
- import { IdmlFramePreference, IdmlStory, IdmlStyle, IdmlSwatch } from "./types.js";
2
+ import { IdmlDocumentPreferences, IdmlFramePreference, IdmlMargin, IdmlMasterPage, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory, IdmlStyle, IdmlSwatch, IdmlTextVariable } from "./types.js";
3
3
  //#region src/model.d.ts
4
- export interface IdmlMargin {
5
- top: number;
6
- bottom: number;
7
- left: number;
8
- right: number;
9
- }
10
- export interface IdmlDocumentPreferences {
11
- pageWidth: number;
12
- pageHeight: number;
13
- facingPages: boolean;
14
- binding: 'left-to-right' | 'right-to-left';
15
- margin: IdmlMargin;
16
- }
17
- export interface IdmlPage {
18
- id: string;
19
- appliedMaster?: string;
20
- stories: string[];
21
- }
22
- export interface IdmlSpread {
23
- id: string;
24
- pages: IdmlPage[];
25
- }
26
- export interface IdmlMasterSpread {
27
- id: string;
28
- pages: IdmlPage[];
29
- }
30
4
  export interface IdmlModel {
31
5
  id: string;
32
6
  preferences: IdmlDocumentPreferences;
@@ -38,7 +12,8 @@ export interface IdmlModel {
38
12
  styles?: IdmlStyle[];
39
13
  swatches?: IdmlSwatch[];
40
14
  bodyFramePreferences?: IdmlFramePreference[];
15
+ textVariables?: IdmlTextVariable[];
41
16
  report?: IdmlReportData;
42
17
  }
43
18
  //#endregion
44
- export type { IdmlStory };
19
+ export type { IdmlDocumentPreferences, IdmlMargin, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory };
package/dist/model.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { refusal } from "./refusal.js";
2
- import { ReportCollector } from "./report.js";
3
2
  import { localName, numberAttribute } from "./xml.js";
3
+ import { readMargin } from "./margins.js";
4
+ import { ReportCollector } from "./report.js";
4
5
  import { parseStyles, parseSwatches } from "./styles.js";
5
6
  import { FramePreferenceIndex, framePreference } from "./frame-preferences.js";
6
7
  import { IDML_LIMITS } from "./limits.js";
8
+ import { parseParts } from "./parse-parts.js";
7
9
  import { parseStories } from "./story-model.js";
8
10
  import { collectUnsupportedElements } from "./unsupported-elements.js";
9
11
  import { isTag } from "domhandler";
10
- import { Parser, parseDocument } from "htmlparser2";
11
12
  //#region src/model.ts
12
13
  function elements(root, name) {
13
14
  const found = [];
@@ -35,57 +36,14 @@ function boolAttribute(element, name, fallback) {
35
36
  if (raw === "false") return false;
36
37
  throw refusal(`IDML ${name} must be true or false`, `set ${name} to true or false`);
37
38
  }
38
- function checkXmlDepth(source, part, limit) {
39
- let depth = 0;
40
- new Parser({
41
- onopentag() {
42
- depth += 1;
43
- if (depth > limit) throw refusal(`IDML XML part ${part} exceeds the ${limit} level XML depth limit`, `reduce its nesting to ${limit} levels or fewer`);
44
- },
45
- onclosetag() {
46
- depth -= 1;
47
- }
48
- }, {
49
- xmlMode: true,
50
- recognizeSelfClosing: true
51
- }).end(source);
52
- }
53
- function parseParts(entries, limits) {
54
- const decoder = new TextDecoder("utf-8", { fatal: true });
55
- return entries.filter((entry) => entry.name.toLowerCase().endsWith(".xml")).map((entry) => {
56
- let source;
57
- try {
58
- source = decoder.decode(entry.bytes);
59
- } catch {
60
- throw refusal(`IDML XML part ${entry.name} is not valid UTF-8`, "export that XML part as UTF-8");
61
- }
62
- let document;
63
- try {
64
- checkXmlDepth(source, entry.name, limits.xmlDepth);
65
- document = parseDocument(source, {
66
- xmlMode: true,
67
- recognizeSelfClosing: true
68
- });
69
- } catch (error) {
70
- if (error instanceof Error && error.message.includes("XML depth limit")) throw error;
71
- throw refusal(`IDML XML part ${entry.name} could not be parsed: ${String(error)}`, "export well-formed XML");
72
- }
73
- const all = elements(document);
74
- if (all.length === 0) throw refusal(`IDML XML part ${entry.name} has no root element`, "export a non-empty well-formed XML part");
75
- return {
76
- name: entry.name,
77
- document,
78
- all
79
- };
80
- });
81
- }
82
39
  function page(element) {
83
40
  const id = attribute(element, "Self");
84
41
  if (!id) throw refusal("an IDML Page has no Self identifier", "give every Page a Self identifier");
85
42
  return {
86
43
  id,
87
44
  appliedMaster: attribute(element, "AppliedMaster"),
88
- stories: []
45
+ stories: [],
46
+ side: "single"
89
47
  };
90
48
  }
91
49
  const IDENTITY = [
@@ -127,12 +85,19 @@ function pageArea(element) {
127
85
  apply(matrix, [left, bottom])
128
86
  ];
129
87
  }
130
- function frameCenter(element, matrix) {
131
- const anchors = elements(element).map((candidate) => attribute(candidate, "Anchor")).filter((value) => value !== void 0).map((value) => value.trim().split(/\s+/).map(Number)).filter((value) => value.length === 2 && value.every(Number.isFinite)).map((point) => apply(matrix, point));
132
- if (anchors.length === 0) return void 0;
133
- const xs = anchors.map(([x]) => x);
134
- const ys = anchors.map(([, y]) => y);
135
- return [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2];
88
+ function bounds(points) {
89
+ if (points.length === 0) return void 0;
90
+ const xs = points.map(([x]) => x);
91
+ const ys = points.map(([, y]) => y);
92
+ return {
93
+ left: Math.min(...xs),
94
+ top: Math.min(...ys),
95
+ right: Math.max(...xs),
96
+ bottom: Math.max(...ys)
97
+ };
98
+ }
99
+ function frameBounds(element, matrix) {
100
+ return bounds(elements(element).map((candidate) => attribute(candidate, "Anchor")).filter((value) => value !== void 0).map((value) => value.trim().split(/\s+/).map(Number)).filter((value) => value.length === 2 && value.every(Number.isFinite)).map((point) => apply(matrix, point)));
136
101
  }
137
102
  function contains(area, [x, y]) {
138
103
  let inside = false;
@@ -148,9 +113,14 @@ function framePlacement(element, matrix, location) {
148
113
  const story = attribute(element, "ParentStory");
149
114
  if (!story) return void 0;
150
115
  const preference = framePreference(element, location);
116
+ const box = frameBounds(element, matrix);
151
117
  return {
118
+ id: attribute(element, "Self") ?? story,
152
119
  story,
153
- center: frameCenter(element, matrix),
120
+ ...box ? {
121
+ bounds: box,
122
+ center: [(box.left + box.right) / 2, (box.top + box.bottom) / 2]
123
+ } : {},
154
124
  ...preference ? { preference } : {}
155
125
  };
156
126
  }
@@ -178,10 +148,83 @@ function spreadFrames(spread, location) {
178
148
  }
179
149
  return result;
180
150
  }
181
- function placeStories(element, pageElements, pages, report, framePreferences, location) {
151
+ function assignSides(pageElements, pages) {
152
+ if (pages.length < 2) return;
153
+ const order = pageElements.map((element) => {
154
+ const area = pageArea(element);
155
+ return area ? bounds(area) : void 0;
156
+ }).map((area, index) => ({
157
+ index,
158
+ x: area ? (area.left + area.right) / 2 : index
159
+ })).sort((a, b) => a.x - b.x);
160
+ const left = order[0];
161
+ const right = order.at(-1);
162
+ const leftPage = left ? pages[left.index] : void 0;
163
+ const rightPage = right ? pages[right.index] : void 0;
164
+ if (leftPage) leftPage.side = "left";
165
+ if (rightPage && right?.index !== left?.index) rightPage.side = "right";
166
+ }
167
+ function masterFrames(frames, pageElements, pages) {
168
+ const result = pages.map(() => []);
169
+ const areas = pageElements.map((candidate) => {
170
+ const area = pageArea(candidate);
171
+ return area ? bounds(area) : void 0;
172
+ });
173
+ for (const frame of frames) {
174
+ const absolute = frame.bounds;
175
+ if (!absolute || !frame.center) continue;
176
+ const index = pageElements.findIndex((candidate) => {
177
+ const area = pageArea(candidate);
178
+ return area ? contains(area, frame.center) : false;
179
+ });
180
+ const area = areas[index];
181
+ if (index < 0 || !area) continue;
182
+ result[index]?.push({
183
+ id: frame.id,
184
+ story: frame.story,
185
+ bounds: {
186
+ left: absolute.left - area.left,
187
+ top: absolute.top - area.top,
188
+ width: absolute.right - absolute.left,
189
+ height: absolute.bottom - absolute.top
190
+ }
191
+ });
192
+ }
193
+ return result;
194
+ }
195
+ function masterSpread(element, pageElements, pages, placements) {
196
+ const frames = masterFrames(placements, pageElements, pages);
197
+ const prefix = attribute(element, "NamePrefix") ?? "";
198
+ const base = attribute(element, "BaseName") ?? attribute(element, "Name") ?? attribute(element, "Self") ?? "master";
199
+ const name = prefix ? `${prefix}-${base}` : base;
200
+ const frameIds = new Set(frames.flat().map((frame) => frame.id));
201
+ const unmappedItems = element.children.flatMap((child) => {
202
+ if (!isTag(child)) return [];
203
+ const type = localName(child.name);
204
+ if (type === "Page" || type === "Properties") return [];
205
+ const id = attribute(child, "Self") ?? type;
206
+ if (type === "TextFrame" && frameIds.has(id)) return [];
207
+ return [{
208
+ type,
209
+ id
210
+ }];
211
+ });
212
+ return {
213
+ id: attribute(element, "Self") ?? base,
214
+ name,
215
+ ...attribute(element, "BasedOn") ? { basedOn: attribute(element, "BasedOn") } : {},
216
+ pages: pages.map((value, index) => ({
217
+ ...value,
218
+ margin: readMargin(elements(pageElements[index], "MarginPreference")[0]),
219
+ frames: frames[index] ?? []
220
+ })),
221
+ unmappedItems
222
+ };
223
+ }
224
+ function placeStories(frames, pageElements, pages, report, framePreferences) {
182
225
  const areas = pageElements.map(pageArea);
183
226
  const frameStories = /* @__PURE__ */ new Set();
184
- for (const frame of spreadFrames(element, location)) {
227
+ for (const frame of frames) {
185
228
  frameStories.add(frame.story);
186
229
  framePreferences.add(frame.story, frame.preference);
187
230
  const pageIndex = frame.center ? areas.findIndex((area) => area !== void 0 && contains(area, frame.center)) : -1;
@@ -206,8 +249,10 @@ function spreads(parts, kind, orderedNames, report, framePreferences) {
206
249
  if (!id) continue;
207
250
  const pageElements = elements(element, "Page");
208
251
  const pages = pageElements.map(page);
209
- for (const story of placeStories(element, pageElements, pages, report, framePreferences, name)) frameStories.add(story);
210
- result.push({
252
+ assignSides(pageElements, pages);
253
+ const frames = spreadFrames(element, name);
254
+ for (const story of placeStories(frames, pageElements, pages, report, framePreferences)) frameStories.add(story);
255
+ result.push(kind === "MasterSpread" ? masterSpread(element, pageElements, pages, frames) : {
211
256
  id,
212
257
  pages
213
258
  });
@@ -218,6 +263,21 @@ function spreads(parts, kind, orderedNames, report, framePreferences) {
218
263
  frameStories
219
264
  };
220
265
  }
266
+ function textVariables(designmap) {
267
+ return designmap.all.filter((element) => localName(element.name) === "TextVariable").flatMap((element) => {
268
+ const id = attribute(element, "Self");
269
+ if (!id) return [];
270
+ const preference = element.children.find((child) => isTag(child) && localName(child.name).endsWith("Preference"));
271
+ return [{
272
+ id,
273
+ name: attribute(element, "Name") ?? id,
274
+ type: attribute(element, "VariableType") ?? "unknown",
275
+ appliedCharacterStyle: preference ? attribute(preference, "AppliedCharacterStyle") : void 0,
276
+ searchStrategy: preference ? attribute(preference, "SearchStrategy") : void 0,
277
+ location: `designmap.xml#${id}`
278
+ }];
279
+ });
280
+ }
221
281
  function spreadSources(document, kind) {
222
282
  return document.all.filter((element) => localName(element.name) === kind).map((element) => attribute(element, "src")).filter((source) => source !== void 0);
223
283
  }
@@ -238,7 +298,6 @@ function idmlModel(entries, limits = IDML_LIMITS) {
238
298
  const spreadResult = spreads(parts, "Spread", spreadSources(designmap, "Spread"), report, framePreferences);
239
299
  const masterResult = spreads(parts, "MasterSpread", spreadSources(designmap, "MasterSpread"), report, new FramePreferenceIndex());
240
300
  const masterStoryIds = [...masterResult.frameStories].filter((story) => !spreadResult.frameStories.has(story));
241
- for (const story of masterStoryIds) report.add(report.unsupportedElements, "master story", story);
242
301
  return {
243
302
  id: attribute(document, "Self") ?? "IDML document",
244
303
  preferences: {
@@ -246,17 +305,13 @@ function idmlModel(entries, limits = IDML_LIMITS) {
246
305
  pageHeight: numberAttribute(preference, "PageHeight"),
247
306
  facingPages: boolAttribute(preference, "FacingPages", true),
248
307
  binding: binding === "RightToLeft" ? "right-to-left" : "left-to-right",
249
- margin: {
250
- top: numberAttribute(margin, "Top", 0),
251
- bottom: numberAttribute(margin, "Bottom", 0),
252
- left: numberAttribute(margin, "Left", 0),
253
- right: numberAttribute(margin, "Right", 0)
254
- }
308
+ margin: readMargin(margin)
255
309
  },
256
310
  spreads: spreadResult.values,
257
311
  masterSpreads: masterResult.values,
258
312
  stories: parseStories(parts, framePreferences, report),
259
313
  bodyFramePreferences: framePreferences.values(),
314
+ textVariables: textVariables(designmap),
260
315
  masterStoryIds,
261
316
  partsRead: parts.map((part) => part.name),
262
317
  styles: parts.flatMap((part) => part.name.endsWith("/Styles.xml") || part.name === "Resources/Styles.xml" ? parseStyles(part.all, part.name) : []),
@@ -0,0 +1,61 @@
1
+ import { refusal } from "./refusal.js";
2
+ import { isTag } from "domhandler";
3
+ import { Parser, parseDocument } from "htmlparser2";
4
+ //#region src/parse-parts.ts
5
+ function allElements(root) {
6
+ const found = [];
7
+ const pending = [root];
8
+ while (pending.length > 0) {
9
+ const node = pending.pop();
10
+ if (!node) continue;
11
+ if (isTag(node)) found.push(node);
12
+ if ("children" in node) for (const child of [...node.children].reverse()) pending.push(child);
13
+ }
14
+ return found;
15
+ }
16
+ function checkDepth(source, part, limit) {
17
+ let depth = 0;
18
+ new Parser({
19
+ onopentag() {
20
+ depth += 1;
21
+ if (depth > limit) throw refusal(`IDML XML part ${part} exceeds the ${limit} level XML depth limit`, `reduce its nesting to ${limit} levels or fewer`);
22
+ },
23
+ onclosetag() {
24
+ depth -= 1;
25
+ }
26
+ }, {
27
+ xmlMode: true,
28
+ recognizeSelfClosing: true
29
+ }).end(source);
30
+ }
31
+ function parseParts(entries, limits) {
32
+ const decoder = new TextDecoder("utf-8", { fatal: true });
33
+ return entries.filter((entry) => entry.name.toLowerCase().endsWith(".xml")).map((entry) => {
34
+ let source;
35
+ try {
36
+ source = decoder.decode(entry.bytes);
37
+ } catch {
38
+ throw refusal(`IDML XML part ${entry.name} is not valid UTF-8`, "export that XML part as UTF-8");
39
+ }
40
+ let document;
41
+ try {
42
+ checkDepth(source, entry.name, limits.xmlDepth);
43
+ document = parseDocument(source, {
44
+ xmlMode: true,
45
+ recognizeSelfClosing: true
46
+ });
47
+ } catch (error) {
48
+ if (error instanceof Error && error.message.includes("XML depth limit")) throw error;
49
+ throw refusal(`IDML XML part ${entry.name} could not be parsed: ${String(error)}`, "export well-formed XML");
50
+ }
51
+ const all = allElements(document);
52
+ if (all.length === 0) throw refusal(`IDML XML part ${entry.name} has no root element`, "export a non-empty well-formed XML part");
53
+ return {
54
+ name: entry.name,
55
+ document,
56
+ all
57
+ };
58
+ });
59
+ }
60
+ //#endregion
61
+ export { parseParts };
package/dist/report.d.ts CHANGED
@@ -4,6 +4,12 @@ export interface IdmlFontUse {
4
4
  fontStyle?: string;
5
5
  locations: string[];
6
6
  }
7
+ export interface IdmlMixedMasterChapter {
8
+ chapter: string;
9
+ side: 'recto' | 'verso';
10
+ masters: string[];
11
+ chosen: string;
12
+ }
7
13
  export interface IdmlReportData {
8
14
  unsupportedElements: Record<string, number>;
9
15
  unsupportedStyleProperties: Record<string, number>;
@@ -13,5 +19,6 @@ export interface IdmlReportData {
13
19
  spotColors: Record<string, number>;
14
20
  locations: Record<string, string[]>;
15
21
  fonts: IdmlFontUse[];
22
+ mixedMasterChapters: IdmlMixedMasterChapter[];
16
23
  }
17
24
  //#endregion
package/dist/report.js CHANGED
@@ -10,6 +10,7 @@ const BUCKET_NAMES = {
10
10
  var ReportCollector = class {
11
11
  locations = /* @__PURE__ */ new Map();
12
12
  fonts = /* @__PURE__ */ new Map();
13
+ mixedMasterChapters;
13
14
  buckets = Object.fromEntries(Object.keys(BUCKET_NAMES).map((name) => [name, /* @__PURE__ */ new Map()]));
14
15
  get unsupportedElements() {
15
16
  return this.buckets.unsupportedElements;
@@ -30,6 +31,7 @@ var ReportCollector = class {
30
31
  return this.buckets.spotColors;
31
32
  }
32
33
  constructor(seed) {
34
+ this.mixedMasterChapters = [...seed?.mixedMasterChapters ?? []];
33
35
  for (const name of Object.keys(BUCKET_NAMES)) for (const [key, count] of Object.entries(seed?.[name] ?? {})) this.buckets[name].set(key, count);
34
36
  for (const [key, locations] of Object.entries(seed?.locations ?? {})) this.locations.set(key, [...locations]);
35
37
  for (const font of seed?.fonts ?? []) this.font(font.appliedFont, font.fontStyle, font.locations[0] ?? "Resources/Styles.xml");
@@ -62,7 +64,8 @@ var ReportCollector = class {
62
64
  return {
63
65
  ...Object.fromEntries(Object.keys(BUCKET_NAMES).map((name) => [name, record(this.buckets[name])])),
64
66
  locations: Object.fromEntries([...this.locations].sort(([a], [b]) => a.localeCompare(b))),
65
- fonts: [...this.fonts.values()]
67
+ fonts: [...this.fonts.values()],
68
+ mixedMasterChapters: [...this.mixedMasterChapters]
66
69
  };
67
70
  }
68
71
  bucketName(bucket) {
package/dist/stories.js CHANGED
@@ -1,4 +1,4 @@
1
- import { resolveDirection } from "@segnavia/format";
1
+ import { BOOK_FURNITURE, MOTIF_ATTRIBUTE, motifFieldAttribute, resolveDirection } from "@segnavia/format";
2
2
  import { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN } from "@segnavia/html";
3
3
  import serialize from "dom-serializer";
4
4
  import { Element, Text } from "domhandler";
@@ -10,8 +10,7 @@ const OMITTED = /* @__PURE__ */ new Set([
10
10
  "Polygon",
11
11
  "Table",
12
12
  "Footnote",
13
- "Note",
14
- "TextVariableInstance"
13
+ "Note"
15
14
  ]);
16
15
  const WRAPPERS = /* @__PURE__ */ new Set([
17
16
  "HyperlinkTextSource",
@@ -41,7 +40,18 @@ function tokens(root, report) {
41
40
  kind: "paragraph",
42
41
  location: node.location
43
42
  });
44
- else if (node.name === "#processing-instruction") report.add(report.unsupportedSpecialCharacters, node.text ?? "processing instruction", node.location);
43
+ else if (node.name === "#processing-instruction") result.push({
44
+ kind: "processing-instruction",
45
+ text: node.text ?? "",
46
+ character,
47
+ location: node.location
48
+ });
49
+ else if (node.name === "TextVariableInstance") result.push({
50
+ kind: "variable",
51
+ variable: node.attributes.AssociatedTextVariable,
52
+ character,
53
+ location: node.location
54
+ });
45
55
  else if (OMITTED.has(node.name)) report.add(report.unsupportedElements, node.name, node.location);
46
56
  else if (node.name === "Properties") return true;
47
57
  else if (node.name === "Change" && node.attributes.ChangeType !== void 0 && !node.attributes.ChangeType.includes("Inserted")) report.add(report.unsupportedElements, "Change", node.location);
@@ -73,6 +83,71 @@ function contentNodes(token, styles, direction) {
73
83
  const classValue = classes(styleClass, localClass);
74
84
  return classValue ? [new Element("span", { class: classValue }, nodes)] : nodes;
75
85
  }
86
+ function matchedVariable(character, variables) {
87
+ const style = character ? rangeAttributes(character).AppliedCharacterStyle : void 0;
88
+ return variables.find((variable) => variable.appliedCharacterStyle === style);
89
+ }
90
+ const folioFurniture = BOOK_FURNITURE.find((item) => item.key.endsWith(":folio"));
91
+ const markFurniture = BOOK_FURNITURE.find((item) => item.key.endsWith(":mark"));
92
+ const markWhich = markFurniture?.fields.find((field) => field.key === "which");
93
+ function furnitureClass(token, styles, direction) {
94
+ const characterAttributes = rangeAttributes(token.character);
95
+ const characterStyle = characterAttributes.AppliedCharacterStyle;
96
+ return classes(styles.styleClass(characterStyle, "character"), styles.overrideClass(characterAttributes, styles.flattened(characterStyle, "character"), direction, token.character?.location ?? token.location));
97
+ }
98
+ function markNode(variable, className) {
99
+ if (!markFurniture) return void 0;
100
+ const last = markWhich && "choices" in markWhich ? markWhich.choices.find((choice) => choice === "last") : void 0;
101
+ const fallback = markWhich && "default" in markWhich ? markWhich.default : void 0;
102
+ return new Element("span", {
103
+ [MOTIF_ATTRIBUTE]: markFurniture.key,
104
+ [motifFieldAttribute("name")]: variable.name,
105
+ [motifFieldAttribute("which")]: variable.searchStrategy === "LastOnPage" ? last ?? fallback ?? "" : fallback ?? "",
106
+ ...className ? { class: className } : {}
107
+ }, []);
108
+ }
109
+ function furnitureNode(token, variables, master, styles, direction, report) {
110
+ const className = furnitureClass(token, styles, direction);
111
+ if (token.kind === "processing-instruction") {
112
+ if (master && /\bACE\s+18\b/.test(token.text ?? "") && folioFurniture) return new Element("span", {
113
+ [MOTIF_ATTRIBUTE]: folioFurniture.key,
114
+ ...className ? { class: className } : {}
115
+ }, []);
116
+ report.add(report.unsupportedSpecialCharacters, token.text ?? "processing instruction", token.location);
117
+ return;
118
+ }
119
+ if (token.kind !== "variable") return void 0;
120
+ const variable = variables.find((candidate) => candidate.id === token.variable);
121
+ if (master && variable) return markNode(variable, className);
122
+ report.add(report.unsupportedElements, variable ? `TextVariable:${variable.name}` : `TextVariableInstance:${token.variable ?? "unknown"}`, token.location);
123
+ }
124
+ function paragraphChildren(paragraphTokens, styles, report, direction, variables, master) {
125
+ const children = [];
126
+ let marked;
127
+ for (const token of paragraphTokens) {
128
+ if (token.kind === "text") {
129
+ const variable = master ? void 0 : matchedVariable(token.character, variables);
130
+ if (variable) {
131
+ if (marked?.variable.id !== variable.id) {
132
+ marked = {
133
+ variable,
134
+ node: new Element("span", { "data-mark": variable.name }, [])
135
+ };
136
+ children.push(marked.node);
137
+ }
138
+ marked.node.children.push(...contentNodes(token, styles, direction));
139
+ } else {
140
+ marked = void 0;
141
+ children.push(...contentNodes(token, styles, direction));
142
+ }
143
+ continue;
144
+ }
145
+ marked = void 0;
146
+ const furniture = furnitureNode(token, variables, master, styles, direction, report);
147
+ if (furniture) children.push(furniture);
148
+ }
149
+ return children;
150
+ }
76
151
  function textNodes(value) {
77
152
  const pieces = value.split("\u2028");
78
153
  const result = [];
@@ -82,7 +157,7 @@ function textNodes(value) {
82
157
  }
83
158
  return result;
84
159
  }
85
- function paragraphNode(paragraph, paragraphTokens, styles, report, bookDirection, bookLanguage, storyDirection, frame) {
160
+ function paragraphNode(paragraph, paragraphTokens, styles, report, bookDirection, bookLanguage, storyDirection, frame, variables, master) {
86
161
  const attributes = rangeAttributes(paragraph);
87
162
  const styleId = attributes.AppliedParagraphStyle;
88
163
  const flattened = styles.flattened(styleId, "paragraph");
@@ -97,17 +172,13 @@ function paragraphNode(paragraph, paragraphTokens, styles, report, bookDirection
97
172
  if (className) htmlAttributes.class = className;
98
173
  if (resolvedDirection !== bookDirection) htmlAttributes.dir = resolvedDirection;
99
174
  if (language && language !== bookLanguage) htmlAttributes.lang = language;
100
- const span = attributes.SpanColumnType ?? flattened.SpanColumnType;
175
+ const span = attributes.SpanColumnType ?? flattened.SpanColumnType ?? "SingleColumn";
101
176
  if (frame && frame.columns >= 2 && span === "SingleColumn") {
102
177
  if (Number.isInteger(frame.columns) && frame.columns >= BOOK_COLUMN_MIN && frame.columns <= BOOK_COLUMN_MAX) htmlAttributes["data-columns"] = String(frame.columns);
103
178
  else report.addOnce(report.unsupportedElements, `TextColumnCount:${frame.columns}`, frame.location ?? paragraph.location);
104
179
  }
105
180
  if (span === "SplitColumns") report.add(report.unsupportedStyleProperties, "SpanColumnType:SplitColumns", paragraph.location);
106
- const children = [];
107
- for (const token of paragraphTokens) {
108
- if (token.kind !== "text") continue;
109
- children.push(...contentNodes(token, styles, resolvedDirection));
110
- }
181
+ const children = paragraphChildren(paragraphTokens, styles, report, resolvedDirection, variables, master);
111
182
  if (children.length === 0) children.push(new Element("br", {}, []));
112
183
  return new Element("p", htmlAttributes, children);
113
184
  }
@@ -117,11 +188,17 @@ function storyHtml(story, styles, report, bookDirection, bookLanguage) {
117
188
  encodeEntities: "utf8"
118
189
  }) };
119
190
  const paragraphs = [];
120
- for (const paragraph of story.root.children.filter((node) => node.name === "ParagraphStyleRange")) {
191
+ const paragraphRanges = [];
192
+ const collectParagraphs = (node) => {
193
+ if (node.name === "ParagraphStyleRange") paragraphRanges.push(node);
194
+ else if (node.name === "Story" || node.name === "XMLElement") for (const child of node.children) collectParagraphs(child);
195
+ };
196
+ collectParagraphs(story.root);
197
+ for (const paragraph of paragraphRanges) {
121
198
  const all = tokens(paragraph, report);
122
199
  let current = [];
123
200
  const flush = () => {
124
- paragraphs.push(paragraphNode(paragraph, current, styles, report, bookDirection, bookLanguage, story.direction, story.frame));
201
+ paragraphs.push(paragraphNode(paragraph, current, styles, report, bookDirection, bookLanguage, story.direction, story.frame, story.textVariables ?? [], story.master ?? false));
125
202
  current = [];
126
203
  };
127
204
  for (const token of all) if (token.kind === "paragraph") flush();
@@ -1,5 +1,5 @@
1
- import { activeIdmlValue } from "./style-property-report.js";
2
1
  import { idmlDirection, localName } from "./xml.js";
2
+ import { activeIdmlValue } from "./style-property-report.js";
3
3
  import { isTag } from "domhandler";
4
4
  //#region src/story-model.ts
5
5
  function textNode(node, part, path) {
package/dist/styles.js CHANGED
@@ -1,6 +1,6 @@
1
+ import { idmlDirection, localName } from "./xml.js";
1
2
  import { stylePropertyMapping } from "./style-mappings.js";
2
3
  import { reportableStyleProperties } from "./style-property-report.js";
3
- import { idmlDirection, localName } from "./xml.js";
4
4
  import { isTag } from "domhandler";
5
5
  //#region src/styles.ts
6
6
  const PARAGRAPH_ROOT = "$ID/[No paragraph style]";
package/dist/types.d.ts CHANGED
@@ -32,4 +32,59 @@ export interface IdmlStory {
32
32
  root?: IdmlTextNode;
33
33
  frame?: IdmlFramePreference;
34
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
+ }
35
90
  //#endregion
@@ -24,6 +24,8 @@ const MAPPED_ELEMENTS = /* @__PURE__ */ new Set([
24
24
  "ParagraphStyle",
25
25
  "CharacterStyle",
26
26
  "Color",
27
+ "TextVariable",
28
+ "MatchCharacterStylePreference",
27
29
  "Properties"
28
30
  ]);
29
31
  function insideMappedResource(element) {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segnavia/idml",
3
- "version": "0.29.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.29.0",
30
- "@segnavia/html": "0.29.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"