@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.
@@ -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, _options?: IdmlImportOptions): Promise<IdmlImportResult>;
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, _options = {}) {
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, IdmlImportReport, IdmlImportResult, importIdml } from "./import.js";
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 { ReportCollector } from "./report.js";
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 styles = new StyleMapper(model.styles ?? [], model.swatches ?? [], report, bookDirection);
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 chapters = stories.map((story) => ({
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
- })).map(({ story, mapped: value }) => {
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, MM_PER_PT } from "@segnavia/format";
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
- 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)}}`);
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, framePreference } from "./frame-preferences.js";
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 ? bounds(area) : void 0;
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 ? bounds(area) : void 0;
52
+ return area ? placementBounds(area) : void 0;
172
53
  });
173
54
  for (const frame of frames) {
174
55
  const absolute = frame.bounds;
175
- if (!absolute || !frame.center) continue;
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, frame.center) : false;
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 pageIndex = frame.center ? areas.findIndex((area) => area !== void 0 && contains(area, frame.center)) : -1;
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 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) : {
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
  };
@@ -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 };