@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 +2 -2
- package/dist/map-book.js +28 -17
- package/dist/margins.js +21 -0
- package/dist/masters.js +200 -0
- package/dist/model.d.ts +3 -28
- package/dist/model.js +120 -65
- package/dist/parse-parts.js +61 -0
- package/dist/report.d.ts +7 -0
- package/dist/report.js +4 -1
- package/dist/stories.js +90 -13
- package/dist/story-model.js +1 -1
- package/dist/styles.js +1 -1
- package/dist/types.d.ts +55 -0
- package/dist/unsupported-elements.js +2 -0
- package/dist/variables.js +21 -0
- package/package.json +3 -3
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 {
|
|
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 {
|
|
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(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/margins.js
ADDED
|
@@ -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 };
|
package/dist/masters.js
ADDED
|
@@ -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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
210
|
-
|
|
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")
|
|
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
|
-
|
|
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();
|
package/dist/story-model.js
CHANGED
|
@@ -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
|
|
@@ -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.
|
|
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.
|
|
30
|
-
"@segnavia/html": "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"
|