@segnavia/idml 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frame-preferences.js +45 -0
- package/dist/import.d.ts +2 -2
- package/dist/import.js +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/map-book.js +34 -8
- package/dist/model.d.ts +8 -6
- package/dist/model.js +37 -60
- package/dist/report.d.ts +17 -0
- package/dist/report.js +75 -0
- package/dist/stories.js +141 -0
- package/dist/story-model.js +53 -0
- package/dist/style-mappings.js +52 -0
- package/dist/style-property-report.js +236 -0
- package/dist/styles.js +285 -0
- package/dist/types.d.ts +35 -0
- package/dist/unsupported-elements.js +49 -0
- package/dist/xml.js +18 -0
- package/package.json +3 -3
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { localName, numberAttribute } from "./xml.js";
|
|
2
|
+
import { isTag } from "domhandler";
|
|
3
|
+
//#region src/frame-preferences.ts
|
|
4
|
+
function preferenceElement(frame) {
|
|
5
|
+
const pending = [...frame.children];
|
|
6
|
+
while (pending.length > 0) {
|
|
7
|
+
const node = pending.pop();
|
|
8
|
+
if (!node || !isTag(node)) continue;
|
|
9
|
+
if (localName(node.name) === "TextFramePreference") return node;
|
|
10
|
+
pending.push(...node.children);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function framePreference(frame, location) {
|
|
14
|
+
const element = preferenceElement(frame);
|
|
15
|
+
if (!element) return void 0;
|
|
16
|
+
return {
|
|
17
|
+
columns: numberAttribute(element, "TextColumnCount", 1),
|
|
18
|
+
gutter: numberAttribute(element, "TextColumnGutter", 0),
|
|
19
|
+
location
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
var FramePreferenceIndex = class {
|
|
23
|
+
byStory = /* @__PURE__ */ new Map();
|
|
24
|
+
add(story, preference) {
|
|
25
|
+
if (!preference || preference.columns < 2) return;
|
|
26
|
+
const values = this.byStory.get(story) ?? [];
|
|
27
|
+
values.push(preference);
|
|
28
|
+
this.byStory.set(story, values);
|
|
29
|
+
}
|
|
30
|
+
preferred(story) {
|
|
31
|
+
const values = this.byStory.get(story) ?? [];
|
|
32
|
+
const counts = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const value of values) {
|
|
34
|
+
const key = `${value.columns}\u0000${value.gutter}`;
|
|
35
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
36
|
+
}
|
|
37
|
+
const selected = [...counts].sort((left, right) => right[1] - left[1])[0]?.[0];
|
|
38
|
+
return values.find((value) => `${value.columns}\u0000${value.gutter}` === selected);
|
|
39
|
+
}
|
|
40
|
+
values() {
|
|
41
|
+
return [...this.byStory.values()].flat();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
//#endregion
|
|
45
|
+
export { FramePreferenceIndex, framePreference };
|
package/dist/import.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { IdmlReportData } from "./report.js";
|
|
1
2
|
import { Book } from "@segnavia/format";
|
|
2
3
|
//#region src/import.d.ts
|
|
3
4
|
export type IdmlImportOptions = Record<never, never>;
|
|
4
|
-
export interface IdmlImportReport {
|
|
5
|
+
export interface IdmlImportReport extends IdmlReportData {
|
|
5
6
|
partsRead: string[];
|
|
6
|
-
unsupportedElements: Record<string, number>;
|
|
7
7
|
}
|
|
8
8
|
export interface IdmlImportResult {
|
|
9
9
|
book: Book;
|
package/dist/import.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { refusal } from "./refusal.js";
|
|
2
|
+
import { ReportCollector } from "./report.js";
|
|
2
3
|
import { modelToBook } from "./map-book.js";
|
|
3
4
|
import { idmlModel } from "./model.js";
|
|
4
5
|
import { readZip } from "./zip.js";
|
|
@@ -13,11 +14,12 @@ async function importIdml(bytes, _options = {}) {
|
|
|
13
14
|
if (value !== IDML_MIMETYPE) throw refusal(`the IDML mimetype entry is ${JSON.stringify(value)}, not ${IDML_MIMETYPE}`, "use the IDML package mimetype");
|
|
14
15
|
if (!byName.has("designmap.xml")) throw refusal("the IDML designmap.xml entry is missing", "add the required designmap.xml entry");
|
|
15
16
|
const model = idmlModel(entries);
|
|
17
|
+
const collector = new ReportCollector(model.report);
|
|
16
18
|
return {
|
|
17
|
-
book: modelToBook(model),
|
|
19
|
+
book: modelToBook(model, collector),
|
|
18
20
|
report: {
|
|
19
21
|
partsRead: model.partsRead,
|
|
20
|
-
|
|
22
|
+
...collector.data()
|
|
21
23
|
}
|
|
22
24
|
};
|
|
23
25
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { IdmlFontUse, IdmlReportData } from "./report.js";
|
|
1
2
|
import { IdmlImportOptions, IdmlImportReport, IdmlImportResult, importIdml } from "./import.js";
|
|
2
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";
|
|
3
|
-
import {
|
|
4
|
-
|
|
4
|
+
import { IdmlStory } from "./types.js";
|
|
5
|
+
import { IdmlDocumentPreferences, IdmlMargin, IdmlMasterSpread, IdmlModel, IdmlPage, IdmlSpread } from "./model.js";
|
|
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,27 +1,50 @@
|
|
|
1
1
|
import { refusal } from "./refusal.js";
|
|
2
|
+
import { ReportCollector } from "./report.js";
|
|
3
|
+
import { storyHtml } from "./stories.js";
|
|
4
|
+
import { StyleMapper } from "./styles.js";
|
|
2
5
|
import { Book, MM_PER_PT } from "@segnavia/format";
|
|
3
6
|
import { findActiveContent } from "@segnavia/html";
|
|
4
|
-
import serialize from "dom-serializer";
|
|
5
|
-
import { Element } from "domhandler";
|
|
6
7
|
//#region src/map-book.ts
|
|
7
|
-
const emptyParagraph = () => serialize(new Element("p", {}, []), { xmlMode: false });
|
|
8
8
|
function orderedStories(model) {
|
|
9
9
|
const order = /* @__PURE__ */ new Map();
|
|
10
10
|
for (const spread of model.spreads) for (const page of spread.pages) for (const story of page.stories) if (!order.has(story)) order.set(story, order.size);
|
|
11
11
|
const masterStories = new Set(model.masterStoryIds);
|
|
12
12
|
return model.stories.filter((story) => !masterStories.has(story.id)).sort((left, right) => (order.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(right.id) ?? Number.MAX_SAFE_INTEGER));
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function bodyColumnGap(model, stories, report) {
|
|
15
|
+
const storyFrames = stories.flatMap((story) => story.frame?.columns && story.frame.columns >= 2 ? [story.frame] : []);
|
|
16
|
+
const frames = (model.bodyFramePreferences ?? storyFrames).filter((frame) => frame.columns >= 2);
|
|
17
|
+
const representativeFrames = storyFrames.length > 0 ? storyFrames : frames;
|
|
18
|
+
const counts = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const frame of representativeFrames) counts.set(frame.gutter, (counts.get(frame.gutter) ?? 0) + 1);
|
|
20
|
+
const selected = [...counts].sort((left, right) => right[1] - left[1])[0]?.[0];
|
|
21
|
+
if (selected === void 0) return void 0;
|
|
22
|
+
for (const gutter of new Set(frames.map((frame) => frame.gutter).filter((gutter) => gutter !== selected))) {
|
|
23
|
+
const frame = frames.find((candidate) => candidate.gutter === gutter);
|
|
24
|
+
report.add(report.unsupportedElements, "differing body frame gutters", frame?.location ?? model.id);
|
|
25
|
+
}
|
|
26
|
+
return selected;
|
|
27
|
+
}
|
|
28
|
+
function modelToBook(model, report = new ReportCollector(model.report)) {
|
|
15
29
|
const preferences = model.preferences;
|
|
16
30
|
const rightBinding = preferences.binding === "right-to-left";
|
|
17
|
-
const
|
|
31
|
+
const bookDirection = rightBinding ? "rtl" : "ltr";
|
|
32
|
+
const styles = new StyleMapper(model.styles ?? [], model.swatches ?? [], report, bookDirection);
|
|
33
|
+
const bookLanguage = styles.rootLanguage();
|
|
34
|
+
const stories = orderedStories(model);
|
|
35
|
+
const chapters = stories.map((story) => ({
|
|
36
|
+
story,
|
|
37
|
+
mapped: storyHtml(story, styles, report, bookDirection, bookLanguage)
|
|
38
|
+
})).map(({ story, mapped: value }) => ({
|
|
18
39
|
title: story.id,
|
|
19
|
-
html:
|
|
40
|
+
html: value.html
|
|
20
41
|
}));
|
|
21
42
|
if (chapters.length === 0) throw refusal("the IDML has no stories", "add at least one Story");
|
|
22
43
|
const html = chapters.map((chapter) => chapter.html).join("");
|
|
23
44
|
const active = findActiveContent(html);
|
|
24
45
|
if (active.length > 0) throw refusal(`the imported Book contains active content (${active.join(", ")})`, "remove that active content");
|
|
46
|
+
const columnGap = bodyColumnGap(model, stories, report);
|
|
47
|
+
const css = styles.css();
|
|
25
48
|
return Book.parse({
|
|
26
49
|
title: model.id,
|
|
27
50
|
mode: "paginated",
|
|
@@ -36,9 +59,12 @@ function modelToBook(model) {
|
|
|
36
59
|
bottom: preferences.margin.bottom * MM_PER_PT,
|
|
37
60
|
inner: (rightBinding ? preferences.margin.right : preferences.margin.left) * MM_PER_PT,
|
|
38
61
|
outer: (rightBinding ? preferences.margin.left : preferences.margin.right) * MM_PER_PT
|
|
39
|
-
}
|
|
62
|
+
},
|
|
63
|
+
...columnGap === void 0 ? {} : { columnGap: columnGap * MM_PER_PT }
|
|
40
64
|
},
|
|
41
|
-
dir:
|
|
65
|
+
dir: bookDirection,
|
|
66
|
+
...bookLanguage ? { lang: bookLanguage } : {},
|
|
67
|
+
...css ? { css } : {},
|
|
42
68
|
rectoOpenings: preferences.facingPages,
|
|
43
69
|
chapters
|
|
44
70
|
});
|
package/dist/model.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IdmlReportData } from "./report.js";
|
|
2
|
+
import { IdmlFramePreference, IdmlStory, IdmlStyle, IdmlSwatch } from "./types.js";
|
|
1
3
|
//#region src/model.d.ts
|
|
2
4
|
export interface IdmlMargin {
|
|
3
5
|
top: number;
|
|
@@ -25,10 +27,6 @@ export interface IdmlMasterSpread {
|
|
|
25
27
|
id: string;
|
|
26
28
|
pages: IdmlPage[];
|
|
27
29
|
}
|
|
28
|
-
export interface IdmlStory {
|
|
29
|
-
id: string;
|
|
30
|
-
part: string;
|
|
31
|
-
}
|
|
32
30
|
export interface IdmlModel {
|
|
33
31
|
id: string;
|
|
34
32
|
preferences: IdmlDocumentPreferences;
|
|
@@ -37,6 +35,10 @@ export interface IdmlModel {
|
|
|
37
35
|
stories: IdmlStory[];
|
|
38
36
|
masterStoryIds: string[];
|
|
39
37
|
partsRead: string[];
|
|
40
|
-
|
|
38
|
+
styles?: IdmlStyle[];
|
|
39
|
+
swatches?: IdmlSwatch[];
|
|
40
|
+
bodyFramePreferences?: IdmlFramePreference[];
|
|
41
|
+
report?: IdmlReportData;
|
|
41
42
|
}
|
|
42
|
-
//#endregion
|
|
43
|
+
//#endregion
|
|
44
|
+
export type { IdmlStory };
|
package/dist/model.js
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { refusal } from "./refusal.js";
|
|
2
|
+
import { ReportCollector } from "./report.js";
|
|
3
|
+
import { localName, numberAttribute } from "./xml.js";
|
|
4
|
+
import { parseStyles, parseSwatches } from "./styles.js";
|
|
5
|
+
import { FramePreferenceIndex, framePreference } from "./frame-preferences.js";
|
|
2
6
|
import { IDML_LIMITS } from "./limits.js";
|
|
7
|
+
import { parseStories } from "./story-model.js";
|
|
8
|
+
import { collectUnsupportedElements } from "./unsupported-elements.js";
|
|
3
9
|
import { isTag } from "domhandler";
|
|
4
10
|
import { Parser, parseDocument } from "htmlparser2";
|
|
5
11
|
//#region src/model.ts
|
|
6
|
-
const MAPPED_ELEMENTS = /* @__PURE__ */ new Set([
|
|
7
|
-
"Document",
|
|
8
|
-
"DocumentPreference",
|
|
9
|
-
"MarginPreference",
|
|
10
|
-
"MasterSpread",
|
|
11
|
-
"Spread",
|
|
12
|
-
"Page",
|
|
13
|
-
"Story",
|
|
14
|
-
"TextFrame"
|
|
15
|
-
]);
|
|
16
|
-
function localName(name) {
|
|
17
|
-
return name.includes(":") ? name.split(":").at(-1) ?? name : name;
|
|
18
|
-
}
|
|
19
12
|
function elements(root, name) {
|
|
20
13
|
const found = [];
|
|
21
14
|
const pending = [root];
|
|
@@ -35,12 +28,6 @@ function attribute(element, ...names) {
|
|
|
35
28
|
if (value !== void 0) return value;
|
|
36
29
|
}
|
|
37
30
|
}
|
|
38
|
-
function numberAttribute(element, name, fallback) {
|
|
39
|
-
const raw = element && attribute(element, name);
|
|
40
|
-
const value = raw === void 0 ? fallback : Number(raw);
|
|
41
|
-
if (value === void 0 || !Number.isFinite(value) || value < 0) throw refusal(`IDML ${name} must be a non-negative number`, `set ${name} to a non-negative numeric value`);
|
|
42
|
-
return value;
|
|
43
|
-
}
|
|
44
31
|
function boolAttribute(element, name, fallback) {
|
|
45
32
|
const raw = attribute(element, name);
|
|
46
33
|
if (raw === void 0) return fallback;
|
|
@@ -157,7 +144,17 @@ function contains(area, [x, y]) {
|
|
|
157
144
|
}
|
|
158
145
|
return inside;
|
|
159
146
|
}
|
|
160
|
-
function
|
|
147
|
+
function framePlacement(element, matrix, location) {
|
|
148
|
+
const story = attribute(element, "ParentStory");
|
|
149
|
+
if (!story) return void 0;
|
|
150
|
+
const preference = framePreference(element, location);
|
|
151
|
+
return {
|
|
152
|
+
story,
|
|
153
|
+
center: frameCenter(element, matrix),
|
|
154
|
+
...preference ? { preference } : {}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function spreadFrames(spread, location) {
|
|
161
158
|
const result = [];
|
|
162
159
|
const pending = [...spread.children].reverse().map((node) => ({
|
|
163
160
|
node,
|
|
@@ -170,11 +167,8 @@ function spreadFrames(spread) {
|
|
|
170
167
|
if (name === "Page" || name === "Spread" || name === "MasterSpread") continue;
|
|
171
168
|
const matrix = name === "Group" || name === "TextFrame" ? compose(current.matrix, transform(current.node)) : current.matrix;
|
|
172
169
|
if (name === "TextFrame") {
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
story,
|
|
176
|
-
center: frameCenter(current.node, matrix)
|
|
177
|
-
});
|
|
170
|
+
const placement = framePlacement(current.node, matrix, `${location}#TextFrame[${result.length + 1}]`);
|
|
171
|
+
if (placement) result.push(placement);
|
|
178
172
|
continue;
|
|
179
173
|
}
|
|
180
174
|
for (let index = current.node.children.length - 1; index >= 0; index -= 1) pending.push({
|
|
@@ -184,25 +178,26 @@ function spreadFrames(spread) {
|
|
|
184
178
|
}
|
|
185
179
|
return result;
|
|
186
180
|
}
|
|
187
|
-
function placeStories(element, pageElements, pages,
|
|
181
|
+
function placeStories(element, pageElements, pages, report, framePreferences, location) {
|
|
188
182
|
const areas = pageElements.map(pageArea);
|
|
189
183
|
const frameStories = /* @__PURE__ */ new Set();
|
|
190
|
-
for (const frame of spreadFrames(element)) {
|
|
184
|
+
for (const frame of spreadFrames(element, location)) {
|
|
191
185
|
frameStories.add(frame.story);
|
|
186
|
+
framePreferences.add(frame.story, frame.preference);
|
|
192
187
|
const pageIndex = frame.center ? areas.findIndex((area) => area !== void 0 && contains(area, frame.center)) : -1;
|
|
193
188
|
const target = pages[pageIndex];
|
|
194
189
|
if (target && !target.stories.includes(frame.story)) target.stories.push(frame.story);
|
|
195
|
-
else if (pageIndex < 0)
|
|
190
|
+
else if (pageIndex < 0) report.add(report.unsupportedElements, "TextFrame off page", frame.story);
|
|
196
191
|
}
|
|
197
192
|
return frameStories;
|
|
198
193
|
}
|
|
199
|
-
function spreads(parts, kind, orderedNames,
|
|
194
|
+
function spreads(parts, kind, orderedNames, report, framePreferences) {
|
|
200
195
|
const result = [];
|
|
201
196
|
const frameStories = /* @__PURE__ */ new Set();
|
|
202
197
|
const byName = new Map(parts.map((part) => [part.name, part]));
|
|
203
198
|
const named = new Set(orderedNames);
|
|
204
199
|
const unlisted = parts.filter((part) => !named.has(part.name) && part.all.some((element) => localName(element.name) === kind && attribute(element, "Self"))).length;
|
|
205
|
-
if (unlisted > 0)
|
|
200
|
+
if (unlisted > 0) report.add(report.unsupportedElements, "unlisted spread part", kind, unlisted);
|
|
206
201
|
for (const name of orderedNames) {
|
|
207
202
|
const part = byName.get(name);
|
|
208
203
|
if (!part) throw refusal(`designmap.xml ${kind} src ${name} has no matching ZIP entry`, `add the named ${kind} part to the package`);
|
|
@@ -211,7 +206,7 @@ function spreads(parts, kind, orderedNames, unsupportedCounts) {
|
|
|
211
206
|
if (!id) continue;
|
|
212
207
|
const pageElements = elements(element, "Page");
|
|
213
208
|
const pages = pageElements.map(page);
|
|
214
|
-
for (const story of placeStories(element, pageElements, pages,
|
|
209
|
+
for (const story of placeStories(element, pageElements, pages, report, framePreferences, name)) frameStories.add(story);
|
|
215
210
|
result.push({
|
|
216
211
|
id,
|
|
217
212
|
pages
|
|
@@ -226,29 +221,6 @@ function spreads(parts, kind, orderedNames, unsupportedCounts) {
|
|
|
226
221
|
function spreadSources(document, kind) {
|
|
227
222
|
return document.all.filter((element) => localName(element.name) === kind).map((element) => attribute(element, "src")).filter((source) => source !== void 0);
|
|
228
223
|
}
|
|
229
|
-
function stories(parts) {
|
|
230
|
-
const result = [];
|
|
231
|
-
const seen = /* @__PURE__ */ new Set();
|
|
232
|
-
for (const part of parts) for (const element of part.all.filter((candidate) => localName(candidate.name) === "Story")) {
|
|
233
|
-
const id = attribute(element, "Self");
|
|
234
|
-
if (!id || seen.has(id)) continue;
|
|
235
|
-
seen.add(id);
|
|
236
|
-
result.push({
|
|
237
|
-
id,
|
|
238
|
-
part: part.name
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
function unsupported(parts) {
|
|
244
|
-
const counts = /* @__PURE__ */ new Map();
|
|
245
|
-
for (const part of parts) for (const element of part.all) {
|
|
246
|
-
const name = localName(element.name);
|
|
247
|
-
if (MAPPED_ELEMENTS.has(name)) continue;
|
|
248
|
-
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
249
|
-
}
|
|
250
|
-
return counts;
|
|
251
|
-
}
|
|
252
224
|
function idmlModel(entries, limits = IDML_LIMITS) {
|
|
253
225
|
const parts = parseParts(entries, limits);
|
|
254
226
|
const designmap = parts.find((part) => part.name === "designmap.xml");
|
|
@@ -260,11 +232,13 @@ function idmlModel(entries, limits = IDML_LIMITS) {
|
|
|
260
232
|
const margin = parts.flatMap((part) => part.all).find((element) => localName(element.name) === "MarginPreference");
|
|
261
233
|
const binding = attribute(preference, "PageBinding") ?? attribute(document, "PageBinding") ?? "LeftToRight";
|
|
262
234
|
if (binding !== "LeftToRight" && binding !== "RightToLeft") throw refusal(`IDML PageBinding ${binding} is not supported`, "use LeftToRight or RightToLeft page binding");
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
const
|
|
235
|
+
const report = new ReportCollector();
|
|
236
|
+
collectUnsupportedElements(parts, report);
|
|
237
|
+
const framePreferences = new FramePreferenceIndex();
|
|
238
|
+
const spreadResult = spreads(parts, "Spread", spreadSources(designmap, "Spread"), report, framePreferences);
|
|
239
|
+
const masterResult = spreads(parts, "MasterSpread", spreadSources(designmap, "MasterSpread"), report, new FramePreferenceIndex());
|
|
266
240
|
const masterStoryIds = [...masterResult.frameStories].filter((story) => !spreadResult.frameStories.has(story));
|
|
267
|
-
|
|
241
|
+
for (const story of masterStoryIds) report.add(report.unsupportedElements, "master story", story);
|
|
268
242
|
return {
|
|
269
243
|
id: attribute(document, "Self") ?? "IDML document",
|
|
270
244
|
preferences: {
|
|
@@ -281,10 +255,13 @@ function idmlModel(entries, limits = IDML_LIMITS) {
|
|
|
281
255
|
},
|
|
282
256
|
spreads: spreadResult.values,
|
|
283
257
|
masterSpreads: masterResult.values,
|
|
284
|
-
stories:
|
|
258
|
+
stories: parseStories(parts, framePreferences, report),
|
|
259
|
+
bodyFramePreferences: framePreferences.values(),
|
|
285
260
|
masterStoryIds,
|
|
286
261
|
partsRead: parts.map((part) => part.name),
|
|
287
|
-
|
|
262
|
+
styles: parts.flatMap((part) => part.name.endsWith("/Styles.xml") || part.name === "Resources/Styles.xml" ? parseStyles(part.all, part.name) : []),
|
|
263
|
+
swatches: parts.flatMap((part) => part.name.endsWith("/Graphic.xml") || part.name === "Resources/Graphic.xml" ? parseSwatches(part.all) : []),
|
|
264
|
+
report: report.data()
|
|
288
265
|
};
|
|
289
266
|
}
|
|
290
267
|
//#endregion
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/report.d.ts
|
|
2
|
+
export interface IdmlFontUse {
|
|
3
|
+
appliedFont: string;
|
|
4
|
+
fontStyle?: string;
|
|
5
|
+
locations: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface IdmlReportData {
|
|
8
|
+
unsupportedElements: Record<string, number>;
|
|
9
|
+
unsupportedStyleProperties: Record<string, number>;
|
|
10
|
+
unsupportedOverrideProperties: Record<string, number>;
|
|
11
|
+
unsupportedSpecialCharacters: Record<string, number>;
|
|
12
|
+
unsupportedLanguages: Record<string, number>;
|
|
13
|
+
spotColors: Record<string, number>;
|
|
14
|
+
locations: Record<string, string[]>;
|
|
15
|
+
fonts: IdmlFontUse[];
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/report.ts
|
|
2
|
+
const BUCKET_NAMES = {
|
|
3
|
+
unsupportedElements: "element",
|
|
4
|
+
unsupportedStyleProperties: "style",
|
|
5
|
+
unsupportedOverrideProperties: "override",
|
|
6
|
+
unsupportedSpecialCharacters: "special",
|
|
7
|
+
unsupportedLanguages: "language",
|
|
8
|
+
spotColors: "spot"
|
|
9
|
+
};
|
|
10
|
+
var ReportCollector = class {
|
|
11
|
+
locations = /* @__PURE__ */ new Map();
|
|
12
|
+
fonts = /* @__PURE__ */ new Map();
|
|
13
|
+
buckets = Object.fromEntries(Object.keys(BUCKET_NAMES).map((name) => [name, /* @__PURE__ */ new Map()]));
|
|
14
|
+
get unsupportedElements() {
|
|
15
|
+
return this.buckets.unsupportedElements;
|
|
16
|
+
}
|
|
17
|
+
get unsupportedStyleProperties() {
|
|
18
|
+
return this.buckets.unsupportedStyleProperties;
|
|
19
|
+
}
|
|
20
|
+
get unsupportedOverrideProperties() {
|
|
21
|
+
return this.buckets.unsupportedOverrideProperties;
|
|
22
|
+
}
|
|
23
|
+
get unsupportedSpecialCharacters() {
|
|
24
|
+
return this.buckets.unsupportedSpecialCharacters;
|
|
25
|
+
}
|
|
26
|
+
get unsupportedLanguages() {
|
|
27
|
+
return this.buckets.unsupportedLanguages;
|
|
28
|
+
}
|
|
29
|
+
get spotColors() {
|
|
30
|
+
return this.buckets.spotColors;
|
|
31
|
+
}
|
|
32
|
+
constructor(seed) {
|
|
33
|
+
for (const name of Object.keys(BUCKET_NAMES)) for (const [key, count] of Object.entries(seed?.[name] ?? {})) this.buckets[name].set(key, count);
|
|
34
|
+
for (const [key, locations] of Object.entries(seed?.locations ?? {})) this.locations.set(key, [...locations]);
|
|
35
|
+
for (const font of seed?.fonts ?? []) this.font(font.appliedFont, font.fontStyle, font.locations[0] ?? "Resources/Styles.xml");
|
|
36
|
+
}
|
|
37
|
+
add(bucket, name, location, count = 1) {
|
|
38
|
+
bucket.set(name, (bucket.get(name) ?? 0) + count);
|
|
39
|
+
const key = `${this.bucketName(bucket)}:${name}`;
|
|
40
|
+
const values = this.locations.get(key) ?? [];
|
|
41
|
+
if (values.length < 5 && !values.includes(location)) values.push(location);
|
|
42
|
+
this.locations.set(key, values);
|
|
43
|
+
}
|
|
44
|
+
addOnce(bucket, name, location) {
|
|
45
|
+
const key = `${this.bucketName(bucket)}:${name}`;
|
|
46
|
+
if (this.locations.get(key)?.includes(location)) return;
|
|
47
|
+
this.add(bucket, name, location);
|
|
48
|
+
}
|
|
49
|
+
font(appliedFont, fontStyle, location) {
|
|
50
|
+
if (!appliedFont) return;
|
|
51
|
+
const key = `${appliedFont}\u0000${fontStyle ?? ""}`;
|
|
52
|
+
const found = this.fonts.get(key) ?? {
|
|
53
|
+
appliedFont,
|
|
54
|
+
...fontStyle ? { fontStyle } : {},
|
|
55
|
+
locations: []
|
|
56
|
+
};
|
|
57
|
+
if (found.locations.length < 5 && !found.locations.includes(location)) found.locations.push(location);
|
|
58
|
+
this.fonts.set(key, found);
|
|
59
|
+
}
|
|
60
|
+
data() {
|
|
61
|
+
const record = (values) => Object.fromEntries([...values].sort(([a], [b]) => a.localeCompare(b)));
|
|
62
|
+
return {
|
|
63
|
+
...Object.fromEntries(Object.keys(BUCKET_NAMES).map((name) => [name, record(this.buckets[name])])),
|
|
64
|
+
locations: Object.fromEntries([...this.locations].sort(([a], [b]) => a.localeCompare(b))),
|
|
65
|
+
fonts: [...this.fonts.values()]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
bucketName(bucket) {
|
|
69
|
+
const found = Object.keys(BUCKET_NAMES).find((name) => this.buckets[name] === bucket);
|
|
70
|
+
if (!found) throw new Error("Report bucket is not registered.");
|
|
71
|
+
return BUCKET_NAMES[found];
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { ReportCollector };
|
package/dist/stories.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { resolveDirection } from "@segnavia/format";
|
|
2
|
+
import { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN } from "@segnavia/html";
|
|
3
|
+
import serialize from "dom-serializer";
|
|
4
|
+
import { Element, Text } from "domhandler";
|
|
5
|
+
//#region src/stories.ts
|
|
6
|
+
const OMITTED = /* @__PURE__ */ new Set([
|
|
7
|
+
"TextFrame",
|
|
8
|
+
"Rectangle",
|
|
9
|
+
"Group",
|
|
10
|
+
"Polygon",
|
|
11
|
+
"Table",
|
|
12
|
+
"Footnote",
|
|
13
|
+
"Note",
|
|
14
|
+
"TextVariableInstance"
|
|
15
|
+
]);
|
|
16
|
+
const WRAPPERS = /* @__PURE__ */ new Set([
|
|
17
|
+
"HyperlinkTextSource",
|
|
18
|
+
"XMLElement",
|
|
19
|
+
"Change"
|
|
20
|
+
]);
|
|
21
|
+
function classes(...values) {
|
|
22
|
+
return values.filter(Boolean).join(" ") || void 0;
|
|
23
|
+
}
|
|
24
|
+
function rangeAttributes(node) {
|
|
25
|
+
if (!node) return {};
|
|
26
|
+
const result = { ...node.attributes };
|
|
27
|
+
const holder = node.children.find((child) => child.name === "Properties");
|
|
28
|
+
for (const child of holder?.children ?? []) if (child.text) result[child.name] = child.text.trim();
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function tokens(root, report) {
|
|
32
|
+
const result = [];
|
|
33
|
+
const terminal = (node, character) => {
|
|
34
|
+
if (node.name === "Content") result.push({
|
|
35
|
+
kind: "text",
|
|
36
|
+
text: node.text ?? "",
|
|
37
|
+
character,
|
|
38
|
+
location: node.location
|
|
39
|
+
});
|
|
40
|
+
else if (node.name === "Br") result.push({
|
|
41
|
+
kind: "paragraph",
|
|
42
|
+
location: node.location
|
|
43
|
+
});
|
|
44
|
+
else if (node.name === "#processing-instruction") report.add(report.unsupportedSpecialCharacters, node.text ?? "processing instruction", node.location);
|
|
45
|
+
else if (OMITTED.has(node.name)) report.add(report.unsupportedElements, node.name, node.location);
|
|
46
|
+
else if (node.name === "Properties") return true;
|
|
47
|
+
else if (node.name === "Change" && node.attributes.ChangeType !== void 0 && !node.attributes.ChangeType.includes("Inserted")) report.add(report.unsupportedElements, "Change", node.location);
|
|
48
|
+
else return false;
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
const walk = (node, character) => {
|
|
52
|
+
if (terminal(node, character)) return;
|
|
53
|
+
const nextCharacter = node.name === "CharacterStyleRange" ? node : character;
|
|
54
|
+
if (WRAPPERS.has(node.name)) report.add(report.unsupportedElements, node.name, node.location);
|
|
55
|
+
else if (![
|
|
56
|
+
"Story",
|
|
57
|
+
"ParagraphStyleRange",
|
|
58
|
+
"CharacterStyleRange"
|
|
59
|
+
].includes(node.name)) report.add(report.unsupportedElements, node.name, node.location);
|
|
60
|
+
for (const child of node.children) walk(child, nextCharacter);
|
|
61
|
+
};
|
|
62
|
+
walk(root);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function contentNodes(token, styles, direction) {
|
|
66
|
+
const character = token.character;
|
|
67
|
+
const characterAttributes = rangeAttributes(character);
|
|
68
|
+
const characterStyle = characterAttributes.AppliedCharacterStyle;
|
|
69
|
+
const characterBase = styles.flattened(characterStyle, "character");
|
|
70
|
+
const styleClass = styles.styleClass(characterStyle, "character");
|
|
71
|
+
const localClass = styles.overrideClass(characterAttributes, characterBase, direction, character?.location ?? token.location);
|
|
72
|
+
const nodes = textNodes(token.text ?? "");
|
|
73
|
+
const classValue = classes(styleClass, localClass);
|
|
74
|
+
return classValue ? [new Element("span", { class: classValue }, nodes)] : nodes;
|
|
75
|
+
}
|
|
76
|
+
function textNodes(value) {
|
|
77
|
+
const pieces = value.split("\u2028");
|
|
78
|
+
const result = [];
|
|
79
|
+
for (const [index, piece] of pieces.entries()) {
|
|
80
|
+
if (piece) result.push(new Text(piece));
|
|
81
|
+
if (index < pieces.length - 1) result.push(new Element("br", {}, []));
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
function paragraphNode(paragraph, paragraphTokens, styles, report, bookDirection, bookLanguage, storyDirection, frame) {
|
|
86
|
+
const attributes = rangeAttributes(paragraph);
|
|
87
|
+
const styleId = attributes.AppliedParagraphStyle;
|
|
88
|
+
const flattened = styles.flattened(styleId, "paragraph");
|
|
89
|
+
const rawText = paragraphTokens.filter((token) => token.kind === "text").map((token) => token.text ?? "").join("");
|
|
90
|
+
const resolvedDirection = styles.paragraphDirection(attributes) ?? styles.paragraphDirection(flattened) ?? storyDirection ?? resolveDirection(void 0, void 0, rawText);
|
|
91
|
+
const languageName = attributes.AppliedLanguage ?? flattened.AppliedLanguage;
|
|
92
|
+
const language = styles.language(languageName, paragraph.location);
|
|
93
|
+
const paragraphClass = styles.styleClass(styleId, "paragraph");
|
|
94
|
+
const overrideClass = styles.overrideClass(attributes, flattened, resolvedDirection, paragraph.location, styles.paragraphStyleDirection(flattened));
|
|
95
|
+
const htmlAttributes = {};
|
|
96
|
+
const className = classes(paragraphClass, overrideClass);
|
|
97
|
+
if (className) htmlAttributes.class = className;
|
|
98
|
+
if (resolvedDirection !== bookDirection) htmlAttributes.dir = resolvedDirection;
|
|
99
|
+
if (language && language !== bookLanguage) htmlAttributes.lang = language;
|
|
100
|
+
const span = attributes.SpanColumnType ?? flattened.SpanColumnType;
|
|
101
|
+
if (frame && frame.columns >= 2 && span === "SingleColumn") {
|
|
102
|
+
if (Number.isInteger(frame.columns) && frame.columns >= BOOK_COLUMN_MIN && frame.columns <= BOOK_COLUMN_MAX) htmlAttributes["data-columns"] = String(frame.columns);
|
|
103
|
+
else report.addOnce(report.unsupportedElements, `TextColumnCount:${frame.columns}`, frame.location ?? paragraph.location);
|
|
104
|
+
}
|
|
105
|
+
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
|
+
}
|
|
111
|
+
if (children.length === 0) children.push(new Element("br", {}, []));
|
|
112
|
+
return new Element("p", htmlAttributes, children);
|
|
113
|
+
}
|
|
114
|
+
function storyHtml(story, styles, report, bookDirection, bookLanguage) {
|
|
115
|
+
if (!story.root) return { html: serialize(new Element("p", {}, [new Element("br", {}, [])]), {
|
|
116
|
+
xmlMode: false,
|
|
117
|
+
encodeEntities: "utf8"
|
|
118
|
+
}) };
|
|
119
|
+
const paragraphs = [];
|
|
120
|
+
for (const paragraph of story.root.children.filter((node) => node.name === "ParagraphStyleRange")) {
|
|
121
|
+
const all = tokens(paragraph, report);
|
|
122
|
+
let current = [];
|
|
123
|
+
const flush = () => {
|
|
124
|
+
paragraphs.push(paragraphNode(paragraph, current, styles, report, bookDirection, bookLanguage, story.direction, story.frame));
|
|
125
|
+
current = [];
|
|
126
|
+
};
|
|
127
|
+
for (const token of all) if (token.kind === "paragraph") flush();
|
|
128
|
+
else current.push(token);
|
|
129
|
+
if (current.length > 0 || all.length === 0 || all.at(-1)?.kind !== "paragraph") flush();
|
|
130
|
+
}
|
|
131
|
+
if (paragraphs.length === 0) paragraphs.push(new Element("p", {}, [new Element("br", {}, [])]));
|
|
132
|
+
return {
|
|
133
|
+
html: serialize(paragraphs, {
|
|
134
|
+
xmlMode: false,
|
|
135
|
+
encodeEntities: "utf8"
|
|
136
|
+
}),
|
|
137
|
+
...story.frame ? { gutter: story.frame.gutter } : {}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { storyHtml };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { activeIdmlValue } from "./style-property-report.js";
|
|
2
|
+
import { idmlDirection, localName } from "./xml.js";
|
|
3
|
+
import { isTag } from "domhandler";
|
|
4
|
+
//#region src/story-model.ts
|
|
5
|
+
function textNode(node, part, path) {
|
|
6
|
+
const children = [];
|
|
7
|
+
let childIndex = 0;
|
|
8
|
+
for (const child of node.children) {
|
|
9
|
+
childIndex += 1;
|
|
10
|
+
if (isTag(child)) children.push(textNode(child, part, `${path}/${localName(child.name)}[${childIndex}]`));
|
|
11
|
+
else if (child.type === "directive") children.push({
|
|
12
|
+
name: "#processing-instruction",
|
|
13
|
+
attributes: {},
|
|
14
|
+
text: child.data,
|
|
15
|
+
children: [],
|
|
16
|
+
location: `${part}${path}#pi${childIndex}`
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const directText = node.children.filter((child) => child.type === "text").map((child) => child.data).join("");
|
|
20
|
+
return {
|
|
21
|
+
name: localName(node.name),
|
|
22
|
+
attributes: { ...node.attribs },
|
|
23
|
+
...directText ? { text: directText } : {},
|
|
24
|
+
children,
|
|
25
|
+
location: `${part}${path}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function storyDirection(element, part, id, report) {
|
|
29
|
+
const preference = element.children.find((child) => isTag(child) && localName(child.name) === "StoryPreference");
|
|
30
|
+
for (const [name, value] of Object.entries(preference?.attribs ?? {})) if (name !== "StoryDirection" && activeIdmlValue(value)) report.add(report.unsupportedElements, `StoryPreference:${name}`, `${part}#${id}`);
|
|
31
|
+
return idmlDirection(preference?.attribs.StoryDirection ?? element.attribs.StoryDirection);
|
|
32
|
+
}
|
|
33
|
+
function parseStories(parts, framePreferences, report) {
|
|
34
|
+
const result = [];
|
|
35
|
+
const seen = /* @__PURE__ */ new Set();
|
|
36
|
+
for (const part of parts) for (const element of part.all.filter((candidate) => localName(candidate.name) === "Story")) {
|
|
37
|
+
const id = element.attribs.Self;
|
|
38
|
+
if (!id || seen.has(id)) continue;
|
|
39
|
+
seen.add(id);
|
|
40
|
+
const direction = storyDirection(element, part.name, id, report);
|
|
41
|
+
const frame = framePreferences.preferred(id);
|
|
42
|
+
result.push({
|
|
43
|
+
id,
|
|
44
|
+
part: part.name,
|
|
45
|
+
root: textNode(element, part.name, `/Story[@Self=${id}]`),
|
|
46
|
+
...direction ? { direction } : {},
|
|
47
|
+
...frame ? { frame } : {}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { parseStories };
|