@segnavia/idml 0.31.0 → 0.32.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/fonts.d.ts +10 -0
- package/dist/fonts.js +105 -0
- package/dist/import.d.ts +2 -2
- package/dist/import.js +2 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -1
- package/dist/map-book.js +15 -6
- package/dist/masters.js +22 -7
- package/dist/model.d.ts +2 -1
- package/dist/model.js +21 -135
- package/dist/openers.js +134 -0
- package/dist/placement-report.js +15 -0
- package/dist/placements.js +300 -0
- package/dist/positioned-box.js +14 -0
- package/dist/report.d.ts +7 -0
- package/dist/report.js +42 -5
- package/dist/stories.js +1 -1
- package/dist/styles.js +17 -20
- package/dist/svg-path.js +33 -0
- package/dist/swatch-color.js +25 -0
- package/dist/types.d.ts +59 -0
- package/dist/unsupported-elements.js +17 -1
- package/dist/xml.js +19 -1
- package/package.json +3 -3
package/dist/fonts.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "./types.js";
|
|
2
|
+
import "@segnavia/format";
|
|
3
|
+
import { Element } from "domhandler";
|
|
4
|
+
//#region src/fonts.d.ts
|
|
5
|
+
export declare const IDML_FONT_WEIGHTS: Readonly<Record<string, number>>;
|
|
6
|
+
export declare function idmlFontStyle(name: string | undefined): {
|
|
7
|
+
weight?: number;
|
|
8
|
+
style?: 'italic';
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
package/dist/fonts.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { refusal } from "./refusal.js";
|
|
2
|
+
import { fontUseKey } from "./report.js";
|
|
3
|
+
import { localName } from "./xml.js";
|
|
4
|
+
import { FontReferenceInput } from "@segnavia/format";
|
|
5
|
+
//#region src/fonts.ts
|
|
6
|
+
const IDML_FONT_WEIGHTS = {
|
|
7
|
+
thin: 100,
|
|
8
|
+
extralight: 200,
|
|
9
|
+
ultralight: 200,
|
|
10
|
+
light: 300,
|
|
11
|
+
regular: 400,
|
|
12
|
+
book: 400,
|
|
13
|
+
normal: 400,
|
|
14
|
+
roman: 400,
|
|
15
|
+
medium: 500,
|
|
16
|
+
semibold: 600,
|
|
17
|
+
demibold: 600,
|
|
18
|
+
bold: 700,
|
|
19
|
+
extrabold: 800,
|
|
20
|
+
heavy: 800,
|
|
21
|
+
black: 900
|
|
22
|
+
};
|
|
23
|
+
function idmlFontStyle(name) {
|
|
24
|
+
if (!name) return {};
|
|
25
|
+
const normalized = name.toLowerCase().replace(/[\s-]+/g, "");
|
|
26
|
+
const italic = normalized.endsWith("italic") ? "italic" : normalized.endsWith("oblique") ? "oblique" : void 0;
|
|
27
|
+
const weightName = italic ? normalized.slice(0, -italic.length) || "regular" : normalized;
|
|
28
|
+
const weight = IDML_FONT_WEIGHTS[weightName];
|
|
29
|
+
return {
|
|
30
|
+
...weight === void 0 ? {} : { weight },
|
|
31
|
+
...italic ? { style: "italic" } : {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function parseFonts(roots) {
|
|
35
|
+
return roots.filter((element) => localName(element.name) === "Font").flatMap((element) => {
|
|
36
|
+
const family = element.attribs.FontFamily;
|
|
37
|
+
if (!family) return [];
|
|
38
|
+
const style = element.attribs.FontStyleName;
|
|
39
|
+
const postscriptName = element.attribs.PostScriptName;
|
|
40
|
+
return [{
|
|
41
|
+
family,
|
|
42
|
+
...style ? { style } : {},
|
|
43
|
+
...postscriptName ? { postscriptName } : {}
|
|
44
|
+
}];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function fontFamilyValue(reference) {
|
|
48
|
+
return "app" in reference ? `var(--segnavia-font-${reference.app})` : `${JSON.stringify(reference.local)}, var(--segnavia-font-${reference.fallback})`;
|
|
49
|
+
}
|
|
50
|
+
var FontMapper = class {
|
|
51
|
+
fonts;
|
|
52
|
+
map;
|
|
53
|
+
report;
|
|
54
|
+
cache = /* @__PURE__ */ new Map();
|
|
55
|
+
references = /* @__PURE__ */ new Map();
|
|
56
|
+
constructor(fonts, map, report) {
|
|
57
|
+
this.fonts = fonts;
|
|
58
|
+
this.map = map;
|
|
59
|
+
this.report = report;
|
|
60
|
+
}
|
|
61
|
+
declarations(family, style, location) {
|
|
62
|
+
if (!family) return [];
|
|
63
|
+
const reference = this.resolve(family, style, location);
|
|
64
|
+
const face = idmlFontStyle(style);
|
|
65
|
+
return [
|
|
66
|
+
...reference ? [`font-family:${fontFamilyValue(reference)}`] : [],
|
|
67
|
+
...face.weight === void 0 ? [] : [`font-weight:${face.weight}`],
|
|
68
|
+
...face.style ? [`font-style:${face.style}`] : []
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
bookFonts() {
|
|
72
|
+
return [...this.references.values()];
|
|
73
|
+
}
|
|
74
|
+
resolve(family, style, location) {
|
|
75
|
+
const key = fontUseKey(family, style);
|
|
76
|
+
if (this.cache.has(key)) {
|
|
77
|
+
if (!this.cache.get(key)) this.report.unmappedFont(family, style, location);
|
|
78
|
+
return this.cache.get(key);
|
|
79
|
+
}
|
|
80
|
+
const known = this.fonts.find((font) => font.family === family && font.style === style);
|
|
81
|
+
const font = {
|
|
82
|
+
family,
|
|
83
|
+
...style ? { style } : {},
|
|
84
|
+
...known?.postscriptName ? { postscriptName: known.postscriptName } : {}
|
|
85
|
+
};
|
|
86
|
+
const answer = this.map?.(font);
|
|
87
|
+
if (answer === void 0) {
|
|
88
|
+
this.cache.set(key, void 0);
|
|
89
|
+
this.report.unmappedFont(family, style, location);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const parsed = FontReferenceInput.safeParse(answer);
|
|
93
|
+
if (!parsed.success) throw refusal(`the host font mapping for ${JSON.stringify(family)}${style ? ` style ${JSON.stringify(style)}` : ""} is invalid`, "return an app or local font reference with a valid app-font fallback");
|
|
94
|
+
const reference = parsed.data;
|
|
95
|
+
this.cache.set(key, reference);
|
|
96
|
+
this.add(reference);
|
|
97
|
+
if ("local" in reference) this.add({ app: reference.fallback });
|
|
98
|
+
return reference;
|
|
99
|
+
}
|
|
100
|
+
add(reference) {
|
|
101
|
+
this.references.set(JSON.stringify(reference), reference);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
export { FontMapper, IDML_FONT_WEIGHTS, idmlFontStyle, parseFonts };
|
package/dist/import.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IdmlReportData } from "./report.js";
|
|
2
|
+
import { IdmlImportOptions } from "./types.js";
|
|
2
3
|
import { Book } from "@segnavia/format";
|
|
3
4
|
//#region src/import.d.ts
|
|
4
|
-
export type IdmlImportOptions = Record<never, never>;
|
|
5
5
|
export interface IdmlImportReport extends IdmlReportData {
|
|
6
6
|
partsRead: string[];
|
|
7
7
|
}
|
|
@@ -9,5 +9,5 @@ export interface IdmlImportResult {
|
|
|
9
9
|
book: Book;
|
|
10
10
|
report: IdmlImportReport;
|
|
11
11
|
}
|
|
12
|
-
export declare function importIdml(bytes: Uint8Array,
|
|
12
|
+
export declare function importIdml(bytes: Uint8Array, options?: IdmlImportOptions): Promise<IdmlImportResult>;
|
|
13
13
|
//#endregion
|
package/dist/import.js
CHANGED
|
@@ -5,7 +5,7 @@ import { idmlModel } from "./model.js";
|
|
|
5
5
|
import { readZip } from "./zip.js";
|
|
6
6
|
//#region src/import.ts
|
|
7
7
|
const IDML_MIMETYPE = "application/vnd.adobe.indesign-idml-package";
|
|
8
|
-
async function importIdml(bytes,
|
|
8
|
+
async function importIdml(bytes, options = {}) {
|
|
9
9
|
const entries = await readZip(bytes);
|
|
10
10
|
const byName = new Map(entries.map((entry) => [entry.name, entry.bytes]));
|
|
11
11
|
const mimetype = byName.get("mimetype");
|
|
@@ -16,7 +16,7 @@ async function importIdml(bytes, _options = {}) {
|
|
|
16
16
|
const model = idmlModel(entries);
|
|
17
17
|
const collector = new ReportCollector(model.report);
|
|
18
18
|
return {
|
|
19
|
-
book: modelToBook(model, collector),
|
|
19
|
+
book: modelToBook(model, collector, options),
|
|
20
20
|
report: {
|
|
21
21
|
partsRead: model.partsRead,
|
|
22
22
|
...collector.data()
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { IdmlFontUse, IdmlReportData } from "./report.js";
|
|
2
|
-
import { IdmlImportOptions,
|
|
1
|
+
import { IdmlFontUse, IdmlLinkUse, IdmlReportData } from "./report.js";
|
|
2
|
+
import { IdmlAssetMap, IdmlDocumentPreferences, IdmlFont, IdmlFontMap, IdmlImportOptions, IdmlMargin, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory } from "./types.js";
|
|
3
|
+
import { IDML_FONT_WEIGHTS, idmlFontStyle } from "./fonts.js";
|
|
4
|
+
import { IdmlImportReport, IdmlImportResult, importIdml } from "./import.js";
|
|
3
5
|
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 { IdmlDocumentPreferences, IdmlMargin, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory } from "./types.js";
|
|
5
6
|
import { IdmlModel } 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 };
|
|
7
|
+
export { IDML_FONT_WEIGHTS, IDML_LIMITS, type IdmlAssetMap, type IdmlDocumentPreferences, type IdmlFont, type IdmlFontMap, type IdmlFontUse, type IdmlImportOptions, type IdmlImportReport, type IdmlImportResult, type IdmlLinkUse, 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, idmlFontStyle, importIdml };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IDML_FONT_WEIGHTS, idmlFontStyle } from "./fonts.js";
|
|
1
2
|
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";
|
|
2
3
|
import { importIdml } from "./import.js";
|
|
3
|
-
export { IDML_LIMITS, MAX_IDML_ENTRIES, MAX_IDML_PACKAGE_BYTES, MAX_IDML_PART_BYTES, MAX_IDML_TOTAL_INFLATED_BYTES, MAX_IDML_XML_DEPTH, importIdml };
|
|
4
|
+
export { IDML_FONT_WEIGHTS, IDML_LIMITS, MAX_IDML_ENTRIES, MAX_IDML_PACKAGE_BYTES, MAX_IDML_PART_BYTES, MAX_IDML_TOTAL_INFLATED_BYTES, MAX_IDML_XML_DEPTH, idmlFontStyle, importIdml };
|
package/dist/map-book.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { refusal } from "./refusal.js";
|
|
2
|
+
import { ReportCollector } from "./report.js";
|
|
3
|
+
import { FontMapper } from "./fonts.js";
|
|
2
4
|
import { logicalMargin } from "./margins.js";
|
|
3
5
|
import { storyHtml } from "./stories.js";
|
|
4
6
|
import { chapterParents, mapMasters } from "./masters.js";
|
|
5
|
-
import {
|
|
7
|
+
import { mapOpeners } from "./openers.js";
|
|
8
|
+
import { reportPlacements } from "./placement-report.js";
|
|
6
9
|
import { StyleMapper } from "./styles.js";
|
|
7
10
|
import { matchVariables } from "./variables.js";
|
|
8
11
|
import { Book, MM_PER_PT } from "@segnavia/format";
|
|
@@ -33,26 +36,30 @@ function refuseActiveBookHtml(chapters, parents) {
|
|
|
33
36
|
const active = findActiveContent(html);
|
|
34
37
|
if (active.length > 0) throw refusal(`the imported Book contains active content (${active.join(", ")})`, "remove that active content");
|
|
35
38
|
}
|
|
36
|
-
function modelToBook(model, report = new ReportCollector(model.report)) {
|
|
39
|
+
function modelToBook(model, report = new ReportCollector(model.report), options = {}) {
|
|
37
40
|
const preferences = model.preferences;
|
|
38
41
|
const rightBinding = preferences.binding === "right-to-left";
|
|
39
42
|
const bookDirection = rightBinding ? "rtl" : "ltr";
|
|
40
|
-
const
|
|
43
|
+
const fonts = new FontMapper(model.fonts ?? [], options.font, report);
|
|
44
|
+
const styles = new StyleMapper(model.styles ?? [], model.swatches ?? [], report, bookDirection, fonts);
|
|
41
45
|
const matchedVariables = matchVariables(model.textVariables ?? [], report);
|
|
42
46
|
const bookLanguage = styles.rootLanguage();
|
|
43
47
|
const stories = orderedStories(model);
|
|
44
48
|
const masters = mapMasters(model, matchedVariables, styles, report);
|
|
45
|
-
const
|
|
49
|
+
const mapped = stories.map((story) => ({
|
|
46
50
|
story,
|
|
47
51
|
mapped: storyHtml({
|
|
48
52
|
...story,
|
|
49
53
|
textVariables: matchedVariables
|
|
50
54
|
}, styles, report, bookDirection, bookLanguage)
|
|
51
|
-
}))
|
|
55
|
+
}));
|
|
56
|
+
const openers = mapOpeners(model, mapped, masters, report, options.asset);
|
|
57
|
+
reportPlacements(model, openers.consumedPages, report);
|
|
58
|
+
const chapters = mapped.filter(({ story }) => !openers.claimed.has(story.id)).map(({ story, mapped: value }) => {
|
|
52
59
|
const parents = chapterParents(story.id, model, masters, report);
|
|
53
60
|
return {
|
|
54
61
|
title: story.id,
|
|
55
|
-
html: value.html,
|
|
62
|
+
html: openers.html.get(story.id) ?? value.html,
|
|
56
63
|
...parents ? { parents } : {}
|
|
57
64
|
};
|
|
58
65
|
});
|
|
@@ -60,6 +67,7 @@ function modelToBook(model, report = new ReportCollector(model.report)) {
|
|
|
60
67
|
refuseActiveBookHtml(chapters, masters.parents);
|
|
61
68
|
const columnGap = bodyColumnGap(model, stories, report);
|
|
62
69
|
const css = `${styles.css()}${masters.css}`;
|
|
70
|
+
const bookFonts = fonts.bookFonts();
|
|
63
71
|
return Book.parse({
|
|
64
72
|
title: model.id,
|
|
65
73
|
mode: "paginated",
|
|
@@ -75,6 +83,7 @@ function modelToBook(model, report = new ReportCollector(model.report)) {
|
|
|
75
83
|
dir: bookDirection,
|
|
76
84
|
...bookLanguage ? { lang: bookLanguage } : {},
|
|
77
85
|
...css ? { css } : {},
|
|
86
|
+
...bookFonts.length > 0 ? { fonts: bookFonts } : {},
|
|
78
87
|
rectoOpenings: preferences.facingPages,
|
|
79
88
|
parentPages: masters.parents,
|
|
80
89
|
chapters
|
package/dist/masters.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { logicalMargin } from "./margins.js";
|
|
2
|
+
import { positionedBoxStyle } from "./positioned-box.js";
|
|
2
3
|
import { storyHtml } from "./stories.js";
|
|
3
|
-
import { BOOK_PARENT_NAMES
|
|
4
|
-
import serialize from "dom-serializer";
|
|
4
|
+
import { BOOK_PARENT_NAMES } from "@segnavia/format";
|
|
5
5
|
import { Element } from "domhandler";
|
|
6
|
+
import serialize from "dom-serializer";
|
|
6
7
|
import { parseDocument } from "htmlparser2";
|
|
7
8
|
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
9
|
function roleFor(page, rtl, ordinal) {
|
|
10
10
|
if (!page.side || page.side === "single") return (ordinal ?? 0) % 2 === 0 ? "recto" : "verso";
|
|
11
11
|
return page.side === (rtl ? "left" : "right") ? "recto" : "verso";
|
|
@@ -39,10 +39,20 @@ function regionHtml(master, page, region, context) {
|
|
|
39
39
|
}, context.styles, context.report, context.rtl ? "rtl" : "ltr", void 0);
|
|
40
40
|
const nodes = parseDocument(mapped.html, { xmlMode: false }).children;
|
|
41
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
42
|
const blockStart = region === "head" ? frame.bounds.top : frame.bounds.top - (context.model.preferences.pageHeight - page.margin.bottom);
|
|
45
|
-
|
|
43
|
+
const bounds = {
|
|
44
|
+
left: frame.bounds.left,
|
|
45
|
+
top: blockStart,
|
|
46
|
+
right: frame.bounds.left + frame.bounds.width,
|
|
47
|
+
bottom: blockStart + frame.bounds.height
|
|
48
|
+
};
|
|
49
|
+
context.css.push(`:where(.${className}){${positionedBoxStyle(bounds, {
|
|
50
|
+
pageWidth: context.model.preferences.pageWidth,
|
|
51
|
+
margin: page.margin,
|
|
52
|
+
rtl: context.rtl,
|
|
53
|
+
blockOrigin: 0,
|
|
54
|
+
unit: "mm"
|
|
55
|
+
})}}`);
|
|
46
56
|
context.mappedStories.add(frame.story);
|
|
47
57
|
}
|
|
48
58
|
if (boxes.length === 0) return void 0;
|
|
@@ -154,12 +164,17 @@ function mapMasters(model, variables, styles, report) {
|
|
|
154
164
|
report.addOnce(report.unsupportedElements, `applied master unknown:${masterId}`, location);
|
|
155
165
|
return role === "recto" ? BOOK_PARENT_NAMES.recto : BOOK_PARENT_NAMES.verso;
|
|
156
166
|
};
|
|
167
|
+
const marginFor = (page, ordinal) => {
|
|
168
|
+
const master = page.appliedMaster ? byId.get(page.appliedMaster) : void 0;
|
|
169
|
+
return (master ? masterPage(master, roleFor(page, rtl, ordinal), rtl) : void 0)?.margin ?? model.preferences.margin;
|
|
170
|
+
};
|
|
157
171
|
return {
|
|
158
172
|
parents,
|
|
159
173
|
css: css.join(""),
|
|
160
174
|
bookMargin: appliedBookMargin(model, applied, report),
|
|
161
175
|
parentFor,
|
|
162
|
-
mappedStories
|
|
176
|
+
mappedStories,
|
|
177
|
+
marginFor
|
|
163
178
|
};
|
|
164
179
|
}
|
|
165
180
|
function chapterParents(story, model, mapping, report) {
|
package/dist/model.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IdmlReportData } from "./report.js";
|
|
2
|
-
import { IdmlDocumentPreferences, IdmlFramePreference, IdmlMargin, IdmlMasterPage, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory, IdmlStyle, IdmlSwatch, IdmlTextVariable } from "./types.js";
|
|
2
|
+
import { IdmlDocumentPreferences, IdmlFont, IdmlFramePreference, IdmlMargin, IdmlMasterPage, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory, IdmlStyle, IdmlSwatch, IdmlTextVariable } from "./types.js";
|
|
3
3
|
//#region src/model.d.ts
|
|
4
4
|
export interface IdmlModel {
|
|
5
5
|
id: string;
|
|
@@ -10,6 +10,7 @@ export interface IdmlModel {
|
|
|
10
10
|
masterStoryIds: string[];
|
|
11
11
|
partsRead: string[];
|
|
12
12
|
styles?: IdmlStyle[];
|
|
13
|
+
fonts?: IdmlFont[];
|
|
13
14
|
swatches?: IdmlSwatch[];
|
|
14
15
|
bodyFramePreferences?: IdmlFramePreference[];
|
|
15
16
|
textVariables?: IdmlTextVariable[];
|
package/dist/model.js
CHANGED
|
@@ -1,34 +1,17 @@
|
|
|
1
1
|
import { refusal } from "./refusal.js";
|
|
2
|
-
import { localName, numberAttribute } from "./xml.js";
|
|
3
|
-
import { readMargin } from "./margins.js";
|
|
4
2
|
import { ReportCollector } from "./report.js";
|
|
3
|
+
import { attribute, elements, localName, numberAttribute } from "./xml.js";
|
|
4
|
+
import { parseFonts } from "./fonts.js";
|
|
5
|
+
import { readMargin } from "./margins.js";
|
|
5
6
|
import { parseStyles, parseSwatches } from "./styles.js";
|
|
6
|
-
import { FramePreferenceIndex
|
|
7
|
+
import { FramePreferenceIndex } from "./frame-preferences.js";
|
|
7
8
|
import { IDML_LIMITS } from "./limits.js";
|
|
8
9
|
import { parseParts } from "./parse-parts.js";
|
|
10
|
+
import { GRAPHIC_ELEMENTS, SHAPE_ELEMENTS, collectUnsupportedElements } from "./unsupported-elements.js";
|
|
11
|
+
import { attachPlacements, contains, pageArea, placementBounds, spreadPlacements } from "./placements.js";
|
|
9
12
|
import { parseStories } from "./story-model.js";
|
|
10
|
-
import { collectUnsupportedElements } from "./unsupported-elements.js";
|
|
11
13
|
import { isTag } from "domhandler";
|
|
12
14
|
//#region src/model.ts
|
|
13
|
-
function elements(root, name) {
|
|
14
|
-
const found = [];
|
|
15
|
-
const pending = [root];
|
|
16
|
-
while (pending.length > 0) {
|
|
17
|
-
const node = pending.pop();
|
|
18
|
-
if (!node) continue;
|
|
19
|
-
if (isTag(node)) {
|
|
20
|
-
if (!name || localName(node.name) === name) found.push(node);
|
|
21
|
-
}
|
|
22
|
-
if ("children" in node) for (let index = node.children.length - 1; index >= 0; index -= 1) pending.push(node.children[index]);
|
|
23
|
-
}
|
|
24
|
-
return found;
|
|
25
|
-
}
|
|
26
|
-
function attribute(element, ...names) {
|
|
27
|
-
for (const name of names) {
|
|
28
|
-
const value = element.attribs[name];
|
|
29
|
-
if (value !== void 0) return value;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
15
|
function boolAttribute(element, name, fallback) {
|
|
33
16
|
const raw = attribute(element, name);
|
|
34
17
|
if (raw === void 0) return fallback;
|
|
@@ -46,113 +29,11 @@ function page(element) {
|
|
|
46
29
|
side: "single"
|
|
47
30
|
};
|
|
48
31
|
}
|
|
49
|
-
const IDENTITY = [
|
|
50
|
-
1,
|
|
51
|
-
0,
|
|
52
|
-
0,
|
|
53
|
-
1,
|
|
54
|
-
0,
|
|
55
|
-
0
|
|
56
|
-
];
|
|
57
|
-
function transform(element) {
|
|
58
|
-
const values = (attribute(element, "ItemTransform") ?? "").trim().split(/\s+/).map(Number);
|
|
59
|
-
return values.length === 6 && values.every(Number.isFinite) ? values : IDENTITY;
|
|
60
|
-
}
|
|
61
|
-
function compose(parent, child) {
|
|
62
|
-
const [a, b, c, d, x, y] = parent;
|
|
63
|
-
const [e, f, g, h, u, v] = child;
|
|
64
|
-
return [
|
|
65
|
-
a * e + c * f,
|
|
66
|
-
b * e + d * f,
|
|
67
|
-
a * g + c * h,
|
|
68
|
-
b * g + d * h,
|
|
69
|
-
a * u + c * v + x,
|
|
70
|
-
b * u + d * v + y
|
|
71
|
-
];
|
|
72
|
-
}
|
|
73
|
-
function apply(value, point) {
|
|
74
|
-
return [value[0] * point[0] + value[2] * point[1] + value[4], value[1] * point[0] + value[3] * point[1] + value[5]];
|
|
75
|
-
}
|
|
76
|
-
function pageArea(element) {
|
|
77
|
-
const bounds = (attribute(element, "GeometricBounds") ?? "").trim().split(/\s+/).map(Number);
|
|
78
|
-
if (bounds.length !== 4 || !bounds.every(Number.isFinite)) return void 0;
|
|
79
|
-
const [top, left, bottom, right] = bounds;
|
|
80
|
-
const matrix = transform(element);
|
|
81
|
-
return [
|
|
82
|
-
apply(matrix, [left, top]),
|
|
83
|
-
apply(matrix, [right, top]),
|
|
84
|
-
apply(matrix, [right, bottom]),
|
|
85
|
-
apply(matrix, [left, bottom])
|
|
86
|
-
];
|
|
87
|
-
}
|
|
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)));
|
|
101
|
-
}
|
|
102
|
-
function contains(area, [x, y]) {
|
|
103
|
-
let inside = false;
|
|
104
|
-
for (let index = 0, previous = area.length - 1; index < area.length; previous = index, index += 1) {
|
|
105
|
-
const [xi, yi] = area[index];
|
|
106
|
-
const [xj, yj] = area[previous];
|
|
107
|
-
if (yi === yj && y === yi && x >= Math.min(xi, xj) && x <= Math.max(xi, xj)) return true;
|
|
108
|
-
if (yi > y !== yj > y && x <= (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
|
|
109
|
-
}
|
|
110
|
-
return inside;
|
|
111
|
-
}
|
|
112
|
-
function framePlacement(element, matrix, location) {
|
|
113
|
-
const story = attribute(element, "ParentStory");
|
|
114
|
-
if (!story) return void 0;
|
|
115
|
-
const preference = framePreference(element, location);
|
|
116
|
-
const box = frameBounds(element, matrix);
|
|
117
|
-
return {
|
|
118
|
-
id: attribute(element, "Self") ?? story,
|
|
119
|
-
story,
|
|
120
|
-
...box ? {
|
|
121
|
-
bounds: box,
|
|
122
|
-
center: [(box.left + box.right) / 2, (box.top + box.bottom) / 2]
|
|
123
|
-
} : {},
|
|
124
|
-
...preference ? { preference } : {}
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function spreadFrames(spread, location) {
|
|
128
|
-
const result = [];
|
|
129
|
-
const pending = [...spread.children].reverse().map((node) => ({
|
|
130
|
-
node,
|
|
131
|
-
matrix: IDENTITY
|
|
132
|
-
}));
|
|
133
|
-
while (pending.length > 0) {
|
|
134
|
-
const current = pending.pop();
|
|
135
|
-
if (!current || !isTag(current.node)) continue;
|
|
136
|
-
const name = localName(current.node.name);
|
|
137
|
-
if (name === "Page" || name === "Spread" || name === "MasterSpread") continue;
|
|
138
|
-
const matrix = name === "Group" || name === "TextFrame" ? compose(current.matrix, transform(current.node)) : current.matrix;
|
|
139
|
-
if (name === "TextFrame") {
|
|
140
|
-
const placement = framePlacement(current.node, matrix, `${location}#TextFrame[${result.length + 1}]`);
|
|
141
|
-
if (placement) result.push(placement);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
for (let index = current.node.children.length - 1; index >= 0; index -= 1) pending.push({
|
|
145
|
-
node: current.node.children[index],
|
|
146
|
-
matrix
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
return result;
|
|
150
|
-
}
|
|
151
32
|
function assignSides(pageElements, pages) {
|
|
152
33
|
if (pages.length < 2) return;
|
|
153
34
|
const order = pageElements.map((element) => {
|
|
154
35
|
const area = pageArea(element);
|
|
155
|
-
return area ?
|
|
36
|
+
return area ? placementBounds(area) : void 0;
|
|
156
37
|
}).map((area, index) => ({
|
|
157
38
|
index,
|
|
158
39
|
x: area ? (area.left + area.right) / 2 : index
|
|
@@ -168,14 +49,15 @@ function masterFrames(frames, pageElements, pages) {
|
|
|
168
49
|
const result = pages.map(() => []);
|
|
169
50
|
const areas = pageElements.map((candidate) => {
|
|
170
51
|
const area = pageArea(candidate);
|
|
171
|
-
return area ?
|
|
52
|
+
return area ? placementBounds(area) : void 0;
|
|
172
53
|
});
|
|
173
54
|
for (const frame of frames) {
|
|
174
55
|
const absolute = frame.bounds;
|
|
175
|
-
|
|
56
|
+
const center = frame.center;
|
|
57
|
+
if (!absolute || !center) continue;
|
|
176
58
|
const index = pageElements.findIndex((candidate) => {
|
|
177
59
|
const area = pageArea(candidate);
|
|
178
|
-
return area ? contains(area,
|
|
60
|
+
return area ? contains(area, center) : false;
|
|
179
61
|
});
|
|
180
62
|
const area = areas[index];
|
|
181
63
|
if (index < 0 || !area) continue;
|
|
@@ -193,7 +75,7 @@ function masterFrames(frames, pageElements, pages) {
|
|
|
193
75
|
return result;
|
|
194
76
|
}
|
|
195
77
|
function masterSpread(element, pageElements, pages, placements) {
|
|
196
|
-
const frames = masterFrames(placements, pageElements, pages);
|
|
78
|
+
const frames = masterFrames(placements.frames, pageElements, pages);
|
|
197
79
|
const prefix = attribute(element, "NamePrefix") ?? "";
|
|
198
80
|
const base = attribute(element, "BaseName") ?? attribute(element, "Name") ?? attribute(element, "Self") ?? "master";
|
|
199
81
|
const name = prefix ? `${prefix}-${base}` : base;
|
|
@@ -201,7 +83,8 @@ function masterSpread(element, pageElements, pages, placements) {
|
|
|
201
83
|
const unmappedItems = element.children.flatMap((child) => {
|
|
202
84
|
if (!isTag(child)) return [];
|
|
203
85
|
const type = localName(child.name);
|
|
204
|
-
if (type === "Page" || type === "Properties") return [];
|
|
86
|
+
if (type === "Page" || type === "Properties" || type === "Group" || type === "Link") return [];
|
|
87
|
+
if (SHAPE_ELEMENTS.includes(type) || GRAPHIC_ELEMENTS.includes(type)) return [];
|
|
205
88
|
const id = attribute(child, "Self") ?? type;
|
|
206
89
|
if (type === "TextFrame" && frameIds.has(id)) return [];
|
|
207
90
|
return [{
|
|
@@ -227,7 +110,8 @@ function placeStories(frames, pageElements, pages, report, framePreferences) {
|
|
|
227
110
|
for (const frame of frames) {
|
|
228
111
|
frameStories.add(frame.story);
|
|
229
112
|
framePreferences.add(frame.story, frame.preference);
|
|
230
|
-
const
|
|
113
|
+
const center = frame.center;
|
|
114
|
+
const pageIndex = center ? areas.findIndex((area) => area !== void 0 && contains(area, center)) : -1;
|
|
231
115
|
const target = pages[pageIndex];
|
|
232
116
|
if (target && !target.stories.includes(frame.story)) target.stories.push(frame.story);
|
|
233
117
|
else if (pageIndex < 0) report.add(report.unsupportedElements, "TextFrame off page", frame.story);
|
|
@@ -250,9 +134,10 @@ function spreads(parts, kind, orderedNames, report, framePreferences) {
|
|
|
250
134
|
const pageElements = elements(element, "Page");
|
|
251
135
|
const pages = pageElements.map(page);
|
|
252
136
|
assignSides(pageElements, pages);
|
|
253
|
-
const
|
|
254
|
-
for (const story of placeStories(frames, pageElements, pages, report, framePreferences)) frameStories.add(story);
|
|
255
|
-
|
|
137
|
+
const placements = spreadPlacements(element, name);
|
|
138
|
+
for (const story of placeStories(placements.frames, pageElements, pages, report, framePreferences)) frameStories.add(story);
|
|
139
|
+
attachPlacements(placements, pageElements, pages, report);
|
|
140
|
+
result.push(kind === "MasterSpread" ? masterSpread(element, pageElements, pages, placements) : {
|
|
256
141
|
id,
|
|
257
142
|
pages
|
|
258
143
|
});
|
|
@@ -315,6 +200,7 @@ function idmlModel(entries, limits = IDML_LIMITS) {
|
|
|
315
200
|
masterStoryIds,
|
|
316
201
|
partsRead: parts.map((part) => part.name),
|
|
317
202
|
styles: parts.flatMap((part) => part.name.endsWith("/Styles.xml") || part.name === "Resources/Styles.xml" ? parseStyles(part.all, part.name) : []),
|
|
203
|
+
fonts: parts.flatMap((part) => part.name.endsWith("/Fonts.xml") || part.name === "Resources/Fonts.xml" ? parseFonts(part.all) : []),
|
|
318
204
|
swatches: parts.flatMap((part) => part.name.endsWith("/Graphic.xml") || part.name === "Resources/Graphic.xml" ? parseSwatches(part.all) : []),
|
|
319
205
|
report: report.data()
|
|
320
206
|
};
|
package/dist/openers.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { formatRoundedNumber, positionedBoxStyle } from "./positioned-box.js";
|
|
2
|
+
import { artSvg } from "./svg-path.js";
|
|
3
|
+
import { findActiveContent, sanitize } from "@segnavia/html";
|
|
4
|
+
//#region src/openers.ts
|
|
5
|
+
function firstPages(model) {
|
|
6
|
+
const result = /* @__PURE__ */ new Map();
|
|
7
|
+
let ordinal = 0;
|
|
8
|
+
for (const spread of model.spreads) for (const page of spread.pages) {
|
|
9
|
+
for (const story of page.stories) if (!result.has(story)) result.set(story, {
|
|
10
|
+
page,
|
|
11
|
+
ordinal
|
|
12
|
+
});
|
|
13
|
+
ordinal += 1;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function storyFramePages(model) {
|
|
18
|
+
const result = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const spread of model.spreads) for (const page of spread.pages) for (const frame of page.bodyFrames ?? []) {
|
|
20
|
+
const pages = result.get(frame.story) ?? /* @__PURE__ */ new Set();
|
|
21
|
+
pages.add(page.id);
|
|
22
|
+
result.set(frame.story, pages);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
function textLength(node) {
|
|
27
|
+
if (!node) return 0;
|
|
28
|
+
return (node.text?.length ?? 0) + node.children.reduce((total, child) => total + textLength(child), 0);
|
|
29
|
+
}
|
|
30
|
+
function frameRank(page, story) {
|
|
31
|
+
return (page.bodyFrames ?? []).filter((frame) => frame.story === story).reduce((best, frame) => {
|
|
32
|
+
const area = (frame.bounds.right - frame.bounds.left) * (frame.bounds.bottom - frame.bounds.top);
|
|
33
|
+
return area > best.area || area === best.area && frame.bounds.top > best.top ? {
|
|
34
|
+
area,
|
|
35
|
+
top: frame.bounds.top
|
|
36
|
+
} : best;
|
|
37
|
+
}, {
|
|
38
|
+
area: 0,
|
|
39
|
+
top: Number.NEGATIVE_INFINITY
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function selectedFlows(stories, first, framePages) {
|
|
43
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const entry of stories) {
|
|
45
|
+
const page = first.get(entry.story.id)?.page;
|
|
46
|
+
if (page) candidates.set(page, [...candidates.get(page) ?? [], entry]);
|
|
47
|
+
}
|
|
48
|
+
const selected = /* @__PURE__ */ new Map();
|
|
49
|
+
for (const [page, entries] of candidates) {
|
|
50
|
+
entries.sort((left, right) => {
|
|
51
|
+
const preferred = (entry) => (framePages.get(entry.story.id)?.size ?? 0) > 1 || (entry.story.frame?.columns ?? 0) >= 2;
|
|
52
|
+
const preference = Number(preferred(right)) - Number(preferred(left));
|
|
53
|
+
if (preference !== 0) return preference;
|
|
54
|
+
const leftFrame = frameRank(page, left.story.id);
|
|
55
|
+
const rightFrame = frameRank(page, right.story.id);
|
|
56
|
+
return rightFrame.area - leftFrame.area || rightFrame.top - leftFrame.top;
|
|
57
|
+
});
|
|
58
|
+
const winner = entries[0];
|
|
59
|
+
if (winner) selected.set(page, winner);
|
|
60
|
+
}
|
|
61
|
+
return selected;
|
|
62
|
+
}
|
|
63
|
+
function resolvedAsset(uri, asset, cache, report) {
|
|
64
|
+
if (cache.has(uri)) return cache.get(uri);
|
|
65
|
+
const markup = asset?.(uri);
|
|
66
|
+
if (markup !== void 0) for (const finding of findActiveContent(markup)) report.strippedAsset(uri, finding);
|
|
67
|
+
const clean = markup === void 0 ? void 0 : sanitize(markup);
|
|
68
|
+
cache.set(uri, clean);
|
|
69
|
+
return clean;
|
|
70
|
+
}
|
|
71
|
+
function titleBlock(model, use, headers, masters, report, asset, cache) {
|
|
72
|
+
const page = use.page;
|
|
73
|
+
const itemBounds = [
|
|
74
|
+
...(page.art ?? []).map((item) => item.bounds),
|
|
75
|
+
...headers.map((item) => item.frame.bounds),
|
|
76
|
+
...(page.links ?? []).map((item) => item.bounds)
|
|
77
|
+
];
|
|
78
|
+
if (itemBounds.length === 0) return "";
|
|
79
|
+
const top = Math.min(...itemBounds.map((item) => item.top));
|
|
80
|
+
const bottom = Math.max(...itemBounds.map((item) => item.bottom));
|
|
81
|
+
const style = (bounds) => positionedBoxStyle(bounds, {
|
|
82
|
+
pageWidth: model.preferences.pageWidth,
|
|
83
|
+
margin: masters.marginFor(page, use.ordinal),
|
|
84
|
+
rtl: model.preferences.binding === "right-to-left",
|
|
85
|
+
blockOrigin: top,
|
|
86
|
+
unit: "pt"
|
|
87
|
+
});
|
|
88
|
+
const children = [];
|
|
89
|
+
for (const art of page.art ?? []) children.push(artSvg(art, model.swatches ?? [], style(art.bounds), report));
|
|
90
|
+
for (const header of headers) children.push(`<div style="${style(header.frame.bounds)}">${header.html}</div>`);
|
|
91
|
+
for (const link of page.links ?? []) {
|
|
92
|
+
const markup = resolvedAsset(link.uri, asset, cache, report) ?? "";
|
|
93
|
+
children.push(`<div style="${style(link.bounds)}">${markup}</div>`);
|
|
94
|
+
}
|
|
95
|
+
return `<div class="idml-title-block" style="position:relative;inline-size:100%;block-size:${formatRoundedNumber(bottom - top)}pt">${children.join("")}</div>`;
|
|
96
|
+
}
|
|
97
|
+
function claimHeaders(page, flow, claimed, framePages, byId) {
|
|
98
|
+
const headers = [];
|
|
99
|
+
const flowLength = textLength(flow.story.root);
|
|
100
|
+
for (const frame of page.bodyFrames ?? []) {
|
|
101
|
+
if (frame.story === flow.story.id || claimed.has(frame.story) || framePages.get(frame.story)?.size !== 1) continue;
|
|
102
|
+
const header = byId.get(frame.story);
|
|
103
|
+
if (!header || textLength(header.story.root) > flowLength || headers.some((candidate) => candidate.frame.story === frame.story)) continue;
|
|
104
|
+
claimed.add(header.story.id);
|
|
105
|
+
headers.push({
|
|
106
|
+
frame,
|
|
107
|
+
html: header.mapped.html
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return headers;
|
|
111
|
+
}
|
|
112
|
+
function mapOpeners(model, stories, masters, report, asset) {
|
|
113
|
+
const first = firstPages(model);
|
|
114
|
+
const framePages = storyFramePages(model);
|
|
115
|
+
const byId = new Map(stories.map((entry) => [entry.story.id, entry]));
|
|
116
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
117
|
+
const html = /* @__PURE__ */ new Map();
|
|
118
|
+
const consumedPages = /* @__PURE__ */ new Set();
|
|
119
|
+
const cache = /* @__PURE__ */ new Map();
|
|
120
|
+
for (const [page, flow] of selectedFlows(stories, first, framePages)) {
|
|
121
|
+
const use = first.get(flow.story.id);
|
|
122
|
+
if (!use) continue;
|
|
123
|
+
const block = titleBlock(model, use, claimHeaders(page, flow, claimed, framePages, byId), masters, report, asset, cache);
|
|
124
|
+
if (block) consumedPages.add(page);
|
|
125
|
+
html.set(flow.story.id, `${block}${flow.mapped.html}`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
claimed,
|
|
129
|
+
html,
|
|
130
|
+
consumedPages
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//#endregion
|
|
134
|
+
export { mapOpeners };
|