@segnavia/idml 0.32.0 → 0.34.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,81 @@
1
+ import { applyTransform, itemTransform, placementBounds, pointValue } from "./placements.js";
2
+ import { formatRoundedNumber, logicalCornerRadii } from "./positioned-box.js";
3
+ import { flattenedStyle } from "./styles.js";
4
+ //#region src/anchored-frames.ts
5
+ function descendants(node) {
6
+ return node.children.flatMap((child) => [child, ...descendants(child)]);
7
+ }
8
+ function width(node) {
9
+ const transform = itemTransform(node.attributes.ItemTransform);
10
+ const anchors = descendants(node).flatMap((child) => {
11
+ const point = pointValue(child.attributes.Anchor);
12
+ return point ? [applyTransform(transform, point)] : [];
13
+ });
14
+ const bounds = placementBounds(anchors);
15
+ if (!bounds) return void 0;
16
+ const value = bounds.right - bounds.left;
17
+ return value > 0 ? value : void 0;
18
+ }
19
+ const finite = (value) => {
20
+ const parsed = Number(value);
21
+ return Number.isFinite(parsed) ? parsed : void 0;
22
+ };
23
+ function corner(values, name) {
24
+ const option = values[`${name}CornerOption`] ?? values.CornerOption;
25
+ const radius = finite(values[`${name}CornerRadius`] ?? values.CornerRadius);
26
+ return option && !option.endsWith("None") && radius && radius > 0 ? radius : void 0;
27
+ }
28
+ function anchoredFrame(node, styles) {
29
+ const story = node.attributes.ParentStory;
30
+ if (!story) return void 0;
31
+ const setting = node.children.find((child) => child.name === "Properties")?.children.find((child) => child.name === "AnchoredObjectSetting");
32
+ const values = {
33
+ ...flattenedStyle(styles, node.attributes.AppliedObjectStyle, "object"),
34
+ ...node.attributes
35
+ };
36
+ const strokeWeight = finite(values.StrokeWeight);
37
+ const strokeColor = values.StrokeColor;
38
+ const corners = {
39
+ topLeft: corner(values, "TopLeft"),
40
+ topRight: corner(values, "TopRight"),
41
+ bottomLeft: corner(values, "BottomLeft"),
42
+ bottomRight: corner(values, "BottomRight")
43
+ };
44
+ const anchorYoffset = finite(setting?.attributes.AnchorYoffset);
45
+ return {
46
+ story,
47
+ location: node.location,
48
+ width: width(node),
49
+ position: setting?.attributes.AnchoredPosition ?? "InlinePosition",
50
+ ...anchorYoffset === void 0 ? {} : { anchorYoffset },
51
+ ...values.FillColor && !values.FillColor.endsWith("/None") && values.FillColor !== "None" ? { fill: values.FillColor } : {},
52
+ ...strokeWeight && strokeWeight > 0 && strokeColor && !strokeColor.endsWith("/None") && strokeColor !== "None" ? { stroke: {
53
+ color: strokeColor,
54
+ weight: strokeWeight
55
+ } } : {},
56
+ ...Object.values(corners).some((value) => value !== void 0) ? { corners } : {}
57
+ };
58
+ }
59
+ function parseAnchoredFrames(stories, styles) {
60
+ return stories.flatMap((story) => story.root ? descendants(story.root).flatMap((node) => node.name === "TextFrame" ? [anchoredFrame(node, styles)].filter(Boolean) : []) : []);
61
+ }
62
+ function cornerDeclarations(frame, direction) {
63
+ const corners = frame.corners;
64
+ if (!corners) return [];
65
+ return Object.entries(logicalCornerRadii(corners, direction)).flatMap(([property, value]) => value === void 0 ? [] : [`${property}:${formatRoundedNumber(value)}pt`]);
66
+ }
67
+ function anchoredFrameStyle(frame, resolveColor, direction) {
68
+ const output = [
69
+ `display:inline-block`,
70
+ `inline-size:${formatRoundedNumber(frame.width ?? 0)}pt`,
71
+ `vertical-align:baseline`
72
+ ];
73
+ const fill = frame.fill ? resolveColor(frame.fill) : void 0;
74
+ if (fill && fill !== "none") output.push(`background:${fill}`);
75
+ const stroke = frame.stroke ? resolveColor(frame.stroke.color) : void 0;
76
+ if (stroke && stroke !== "none" && frame.stroke) output.push(`border:${formatRoundedNumber(frame.stroke.weight)}pt solid ${stroke}`);
77
+ output.push(...cornerDeclarations(frame, direction));
78
+ return output.join(";");
79
+ }
80
+ //#endregion
81
+ export { anchoredFrameStyle, parseAnchoredFrames };
@@ -0,0 +1,16 @@
1
+ //#region src/chapter-openings.ts
2
+ function chapterOpenings(openings, facingPages) {
3
+ if (!facingPages) return {
4
+ rectoOpenings: false,
5
+ verso: []
6
+ };
7
+ return {
8
+ rectoOpenings: openings.length > 0 && openings.every((opening) => opening.role === "recto"),
9
+ verso: openings.filter((opening) => opening.role === "verso").map(({ story, page }) => ({
10
+ story,
11
+ page
12
+ }))
13
+ };
14
+ }
15
+ //#endregion
16
+ export { chapterOpenings };
package/dist/map-book.js CHANGED
@@ -1,21 +1,24 @@
1
1
  import { refusal } from "./refusal.js";
2
2
  import { ReportCollector } from "./report.js";
3
3
  import { FontMapper } from "./fonts.js";
4
+ import { resolveSwatchColor } from "./swatch-color.js";
5
+ import { StyleMapper } from "./styles.js";
6
+ import { anchoredFrameStyle } from "./anchored-frames.js";
7
+ import { chapterOpenings } from "./chapter-openings.js";
4
8
  import { logicalMargin } from "./margins.js";
5
9
  import { storyHtml } from "./stories.js";
6
- import { chapterParents, mapMasters } from "./masters.js";
10
+ import { chapterParents, mapMasters, openingPage, storyPageUses } from "./masters.js";
7
11
  import { mapOpeners } from "./openers.js";
8
12
  import { reportPlacements } from "./placement-report.js";
9
- import { StyleMapper } from "./styles.js";
10
13
  import { matchVariables } from "./variables.js";
11
14
  import { Book, MM_PER_PT } from "@segnavia/format";
12
15
  import { findActiveContent } from "@segnavia/html";
13
16
  //#region src/map-book.ts
14
- function orderedStories(model) {
17
+ function orderedStories(model, anchoredOnly = /* @__PURE__ */ new Set()) {
15
18
  const order = /* @__PURE__ */ new Map();
16
19
  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);
17
20
  const masterStories = new Set(model.masterStoryIds);
18
- 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));
21
+ return model.stories.filter((story) => !masterStories.has(story.id) && !anchoredOnly.has(story.id)).sort((left, right) => (order.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (order.get(right.id) ?? Number.MAX_SAFE_INTEGER));
19
22
  }
20
23
  function bodyColumnGap(model, stories, report) {
21
24
  const storyFrames = stories.flatMap((story) => story.frame?.columns && story.frame.columns >= 2 ? [story.frame] : []);
@@ -31,6 +34,13 @@ function bodyColumnGap(model, stories, report) {
31
34
  }
32
35
  return selected;
33
36
  }
37
+ function anchoredFrameIssue(frame, storyPages, depth, ancestors) {
38
+ if (frame.position !== "InlinePosition") return frame.position;
39
+ if (ancestors.has(frame.story)) return "cycle";
40
+ if (storyPages.has(frame.story)) return "InlinePosition";
41
+ if (depth >= 3) return "depth";
42
+ if (frame.width === void 0) return "missing geometry";
43
+ }
34
44
  function refuseActiveBookHtml(chapters, parents) {
35
45
  const html = [...chapters.map((chapter) => chapter.html), ...parents.flatMap((parent) => [parent.head ?? "", parent.foot ?? ""])].join("");
36
46
  const active = findActiveContent(html);
@@ -44,19 +54,60 @@ function modelToBook(model, report = new ReportCollector(model.report), options
44
54
  const styles = new StyleMapper(model.styles ?? [], model.swatches ?? [], report, bookDirection, fonts);
45
55
  const matchedVariables = matchVariables(model.textVariables ?? [], report);
46
56
  const bookLanguage = styles.rootLanguage();
47
- const stories = orderedStories(model);
48
- const masters = mapMasters(model, matchedVariables, styles, report);
57
+ const storyPages = storyPageUses(model);
58
+ const anchoredStoryIds = new Set((model.anchoredFrames ?? []).map((frame) => frame.story));
59
+ const stories = orderedStories(model, new Set([...anchoredStoryIds].filter((story) => !storyPages.has(story))));
60
+ const storyById = new Map(model.stories.map((story) => [story.id, story]));
61
+ const frameByLocation = new Map((model.anchoredFrames ?? []).map((frame) => [frame.location, frame]));
62
+ const renderStory = (story, depth = 0, ancestors = /* @__PURE__ */ new Set([story.id]), master = false) => storyHtml({
63
+ ...story,
64
+ textVariables: matchedVariables,
65
+ master
66
+ }, styles, report, bookDirection, bookLanguage, {
67
+ inlineParagraphs: depth > 0,
68
+ anchored: (location, direction) => {
69
+ const frame = frameByLocation.get(location);
70
+ if (!frame) {
71
+ report.add(report.unsupportedElements, "TextFrame", location);
72
+ return;
73
+ }
74
+ if (frame.position === "InlinePosition" && frame.anchorYoffset !== void 0 && frame.anchorYoffset !== 0) report.add(report.unsupportedElements, "TextFrame anchored:AnchorYoffset", frame.location);
75
+ const issue = anchoredFrameIssue(frame, storyPages, depth, ancestors);
76
+ if (issue) {
77
+ report.add(report.unsupportedElements, `TextFrame anchored:${issue}`, frame.location);
78
+ return;
79
+ }
80
+ const nested = storyById.get(frame.story);
81
+ if (!nested) {
82
+ report.add(report.unsupportedElements, "TextFrame anchored:missing story", frame.location);
83
+ return;
84
+ }
85
+ const nestedAncestors = new Set(ancestors);
86
+ nestedAncestors.add(frame.story);
87
+ const html = renderStory(nested, depth + 1, nestedAncestors, master).html;
88
+ return `<span class="idml-anchored" style="${anchoredFrameStyle(frame, (color) => resolveSwatchColor(color, model.swatches ?? [], report, frame.location, "fill"), direction)}">${html}</span>`;
89
+ }
90
+ });
91
+ const masters = mapMasters(model, matchedVariables, styles, report, (story) => renderStory(story, 0, /* @__PURE__ */ new Set([story.id]), true));
49
92
  const mapped = stories.map((story) => ({
50
93
  story,
51
- mapped: storyHtml({
52
- ...story,
53
- textVariables: matchedVariables
54
- }, styles, report, bookDirection, bookLanguage)
94
+ mapped: renderStory(story)
55
95
  }));
56
- const openers = mapOpeners(model, mapped, masters, report, options.asset);
96
+ const openers = mapOpeners(model, mapped, storyPages, masters, report, options.asset);
57
97
  reportPlacements(model, openers.consumedPages, report);
58
- const chapters = mapped.filter(({ story }) => !openers.claimed.has(story.id)).map(({ story, mapped: value }) => {
59
- const parents = chapterParents(story.id, model, masters, report);
98
+ const emitted = mapped.filter(({ story }) => !openers.claimed.has(story.id));
99
+ const openings = emitted.flatMap(({ story }) => {
100
+ const opening = openingPage(story.id, model, storyPages);
101
+ return opening ? [{
102
+ story: story.id,
103
+ page: opening.page.id,
104
+ role: opening.role
105
+ }] : [];
106
+ });
107
+ const openingPolicy = chapterOpenings(openings, preferences.facingPages);
108
+ for (const opening of openingPolicy.verso) report.versoOpening(opening.story, opening.page);
109
+ const chapters = emitted.map(({ story, mapped: value }) => {
110
+ const parents = chapterParents(story.id, model, storyPages, masters, report);
60
111
  return {
61
112
  title: story.id,
62
113
  html: openers.html.get(story.id) ?? value.html,
@@ -84,7 +135,7 @@ function modelToBook(model, report = new ReportCollector(model.report), options
84
135
  ...bookLanguage ? { lang: bookLanguage } : {},
85
136
  ...css ? { css } : {},
86
137
  ...bookFonts.length > 0 ? { fonts: bookFonts } : {},
87
- rectoOpenings: preferences.facingPages,
138
+ rectoOpenings: openingPolicy.rectoOpenings,
88
139
  parentPages: masters.parents,
89
140
  chapters
90
141
  });
@@ -0,0 +1,89 @@
1
+ import { positionedBoxStyle } from "./positioned-box.js";
2
+ import { storyHtml } from "./stories.js";
3
+ import { Element } from "domhandler";
4
+ import serialize from "dom-serializer";
5
+ import { parseDocument } from "htmlparser2";
6
+ const MAX_BASE_DEPTH = 8;
7
+ function masterPage(master, role, rtl) {
8
+ if (master.pages.length === 1) return master.pages[0];
9
+ const side = pageSideForRole(role, rtl);
10
+ return master.pages.find((page) => page.side === side);
11
+ }
12
+ function pageSideForRole(role, rtl) {
13
+ return role === "recto" ? rtl ? "left" : "right" : rtl ? "right" : "left";
14
+ }
15
+ function frameRegion(page, frame, height) {
16
+ if (frame.bounds.top + frame.bounds.height <= page.margin.top) return "head";
17
+ if (frame.bounds.top >= height - page.margin.bottom) return "foot";
18
+ }
19
+ function itemBaseId(master, page) {
20
+ if (master.showMasterItems === false || page.showMasterItems === false) return void 0;
21
+ if (page.appliedMaster && page.appliedMaster !== "n") return page.appliedMaster;
22
+ return master.basedOn;
23
+ }
24
+ function ownBoxes(master, page, applyingPage, region, context) {
25
+ const boxes = [];
26
+ for (const frame of page.frames) {
27
+ const mappedRegion = frameRegion(applyingPage, frame, context.model.preferences.pageHeight);
28
+ if (!mappedRegion) context.report.addOnce(context.report.unsupportedElements, "master TextFrame", `${master.name}#${frame.id}`);
29
+ if (mappedRegion !== region) continue;
30
+ const story = context.stories.get(frame.story);
31
+ if (!story) {
32
+ context.report.add(context.report.unsupportedElements, "master story missing", `${master.name}#${frame.id}`);
33
+ continue;
34
+ }
35
+ const className = `idml-master-frame-${context.css.length}`;
36
+ const mapped = context.renderStory ? context.renderStory(story) : storyHtml({
37
+ ...story,
38
+ textVariables: context.variables,
39
+ master: true
40
+ }, context.styles, context.report, context.rtl ? "rtl" : "ltr", void 0);
41
+ boxes.push(new Element("div", { class: className }, parseDocument(mapped.html, { xmlMode: false }).children));
42
+ const blockStart = region === "head" ? frame.bounds.top : frame.bounds.top - (context.model.preferences.pageHeight - applyingPage.margin.bottom);
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: applyingPage.margin,
52
+ rtl: context.rtl,
53
+ blockOrigin: 0,
54
+ unit: "mm"
55
+ })}}`);
56
+ context.mappedStories.add(frame.story);
57
+ }
58
+ return boxes;
59
+ }
60
+ function regionBoxes(master, page, role, region, byId, context, applyingPage, ancestors, depth) {
61
+ const boxes = [];
62
+ const baseId = itemBaseId(master, page);
63
+ if (baseId) {
64
+ const location = `${master.name}#${page.id}`;
65
+ if (ancestors.has(baseId)) context.report.addOnce(context.report.unsupportedElements, "master base cycle", location);
66
+ else if (depth >= MAX_BASE_DEPTH) context.report.addOnce(context.report.unsupportedElements, "master base depth", location);
67
+ else {
68
+ const base = byId.get(baseId);
69
+ const basePage = base ? masterPage(base, role, context.rtl) : void 0;
70
+ if (base && basePage) {
71
+ const nextAncestors = new Set(ancestors);
72
+ nextAncestors.add(base.id);
73
+ boxes.push(...regionBoxes(base, basePage, role, region, byId, context, applyingPage, nextAncestors, depth + 1));
74
+ } else context.report.addOnce(context.report.unsupportedElements, `master base unknown:${baseId}`, location);
75
+ }
76
+ }
77
+ boxes.push(...ownBoxes(master, page, applyingPage, region, context));
78
+ return boxes;
79
+ }
80
+ function regionHtml(master, page, role, region, byId, context, empty = false) {
81
+ const boxes = regionBoxes(master, page, role, region, byId, context, page, /* @__PURE__ */ new Set([master.id]), 0);
82
+ if (boxes.length === 0 && !empty) return void 0;
83
+ return serialize(new Element("div", { class: "idml-master-region" }, boxes), {
84
+ xmlMode: false,
85
+ encodeEntities: "utf8"
86
+ });
87
+ }
88
+ //#endregion
89
+ export { frameRegion, masterPage, pageSideForRole, regionHtml };
package/dist/masters.js CHANGED
@@ -1,77 +1,26 @@
1
1
  import { logicalMargin } from "./margins.js";
2
- import { positionedBoxStyle } from "./positioned-box.js";
3
- import { storyHtml } from "./stories.js";
2
+ import { masterPage, pageSideForRole, regionHtml } from "./master-regions.js";
4
3
  import { BOOK_PARENT_NAMES } from "@segnavia/format";
5
- import { Element } from "domhandler";
6
- import serialize from "dom-serializer";
7
- import { parseDocument } from "htmlparser2";
4
+ //#region src/masters.ts
8
5
  const slug = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "master";
9
6
  function roleFor(page, rtl, ordinal) {
10
7
  if (!page.side || page.side === "single") return (ordinal ?? 0) % 2 === 0 ? "recto" : "verso";
11
8
  return page.side === (rtl ? "left" : "right") ? "recto" : "verso";
12
9
  }
13
- function masterPage(master, role, rtl) {
14
- if (master.pages.length === 1) return master.pages[0];
15
- const side = role === "recto" ? rtl ? "left" : "right" : rtl ? "right" : "left";
16
- return master.pages.find((page) => page.side === side);
17
- }
18
10
  function parentName(master, role) {
19
11
  return `idml-${slug(master.name)}-${master.pages.length === 1 ? "recto" : role}`;
20
12
  }
21
- function frameRegion(page, frame, height) {
22
- if (frame.bounds.top + frame.bounds.height <= page.margin.top) return "head";
23
- if (frame.bounds.top >= height - page.margin.bottom) return "foot";
24
- }
25
- function regionHtml(master, page, region, context) {
26
- const boxes = [];
27
- for (const frame of page.frames) {
28
- if (frameRegion(page, frame, context.model.preferences.pageHeight) !== region) continue;
29
- const story = context.stories.get(frame.story);
30
- if (!story) {
31
- context.report.add(context.report.unsupportedElements, "master story missing", `${master.name}#${frame.id}`);
32
- continue;
33
- }
34
- const className = `idml-master-frame-${context.css.length}`;
35
- const mapped = storyHtml({
36
- ...story,
37
- textVariables: context.variables,
38
- master: true
39
- }, context.styles, context.report, context.rtl ? "rtl" : "ltr", void 0);
40
- const nodes = parseDocument(mapped.html, { xmlMode: false }).children;
41
- boxes.push(new Element("div", { class: className }, nodes));
42
- const blockStart = region === "head" ? frame.bounds.top : frame.bounds.top - (context.model.preferences.pageHeight - page.margin.bottom);
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
- })}}`);
56
- context.mappedStories.add(frame.story);
57
- }
58
- if (boxes.length === 0) return void 0;
59
- return serialize(new Element("div", { class: "idml-master-region" }, boxes), {
60
- xmlMode: false,
61
- encodeEntities: "utf8"
62
- });
63
- }
64
13
  function pageSide(page, role, rtl) {
65
14
  if (page.side === "left" || page.side === "right") return page.side;
66
- return role === "recto" ? rtl ? "left" : "right" : rtl ? "right" : "left";
15
+ return pageSideForRole(role, rtl);
67
16
  }
68
17
  function buildParent(master, role, byId, context) {
69
18
  const page = masterPage(master, role, context.rtl);
70
19
  if (!page) return void 0;
71
20
  const base = master.basedOn ? byId.get(master.basedOn) : void 0;
72
- const head = regionHtml(master, page, "head", context);
73
- const foot = regionHtml(master, page, "foot", context);
74
- for (const frame of page.frames) if (!frameRegion(page, frame, context.model.preferences.pageHeight)) context.report.add(context.report.unsupportedElements, "master TextFrame", `${master.name}#${frame.id}`);
21
+ const shadowsSpreadBase = Boolean(base && (page.appliedMaster && page.appliedMaster !== "n" || master.showMasterItems === false || page.showMasterItems === false));
22
+ const head = regionHtml(master, page, role, "head", byId, context, shadowsSpreadBase);
23
+ const foot = regionHtml(master, page, role, "foot", byId, context, shadowsSpreadBase);
75
24
  return {
76
25
  name: parentName(master, role),
77
26
  ...base ? { basedOn: parentName(base, role) } : {},
@@ -131,13 +80,14 @@ function appliedBookMargin(model, applied, report) {
131
80
  }
132
81
  return logicalMargin(first, model.preferences.binding === "right-to-left" ? "left" : "right");
133
82
  }
134
- function mapMasters(model, variables, styles, report) {
83
+ function mapMasters(model, variables, styles, report, renderStory) {
135
84
  const rtl = model.preferences.binding === "right-to-left";
136
85
  const applied = new Set(model.spreads.flatMap((spread) => spread.pages.map((page) => page.appliedMaster).filter((id) => Boolean(id && id !== "n"))));
137
86
  const masters = emittedMasters(model, applied);
138
87
  const byId = new Map(model.masterSpreads.map((master) => [master.id, master]));
139
88
  for (const spread of model.spreads) for (const page of spread.pages) {
140
89
  const id = page.appliedMaster;
90
+ if (page.showMasterItems === false) report.addOnce(report.unsupportedElements, "Page ShowMasterItems", page.id);
141
91
  if (id && id !== "n" && !byId.has(id)) report.addOnce(report.unsupportedElements, `applied master unknown:${id}`, page.id);
142
92
  }
143
93
  const css = [":where(.idml-master-region){position:relative;block-size:100%}"];
@@ -150,7 +100,8 @@ function mapMasters(model, variables, styles, report) {
150
100
  rtl,
151
101
  stories: new Map(model.stories.map((story) => [story.id, story])),
152
102
  mappedStories,
153
- css
103
+ css,
104
+ renderStory
154
105
  });
155
106
  if (model.spreads.some((spread) => spread.pages.some((page) => !page.appliedMaster || page.appliedMaster === "n"))) parents.push({
156
107
  name: "idml-none",
@@ -177,23 +128,35 @@ function mapMasters(model, variables, styles, report) {
177
128
  marginFor
178
129
  };
179
130
  }
180
- function chapterParents(story, model, mapping, report) {
181
- const rtl = model.preferences.binding === "right-to-left";
182
- const pages = [];
131
+ function storyPageUses(model) {
132
+ const result = /* @__PURE__ */ new Map();
183
133
  let ordinal = 0;
184
134
  for (const spread of model.spreads) for (const page of spread.pages) {
185
- if (page.stories.includes(story)) pages.push({
135
+ for (const story of page.stories) result.set(story, [...result.get(story) ?? [], {
186
136
  page,
187
137
  ordinal
188
- });
138
+ }]);
189
139
  ordinal += 1;
190
140
  }
191
- const first = pages[0];
141
+ return result;
142
+ }
143
+ function openingPage(story, model, pages) {
144
+ const rtl = model.preferences.binding === "right-to-left";
145
+ const first = pages.get(story)?.[0];
146
+ return first ? {
147
+ page: first.page,
148
+ role: roleFor(first.page, rtl, first.ordinal)
149
+ } : void 0;
150
+ }
151
+ function chapterParents(story, model, pages, mapping, report) {
152
+ const rtl = model.preferences.binding === "right-to-left";
153
+ const uses = pages.get(story);
154
+ const first = uses?.[0];
192
155
  if (!first) return void 0;
193
156
  const firstRole = roleFor(first.page, rtl, first.ordinal);
194
157
  const result = { opener: mapping.parentFor(first.page.appliedMaster, firstRole, first.page.id) };
195
158
  for (const role of ["recto", "verso"]) {
196
- const candidates = pages.slice(1).filter((entry) => roleFor(entry.page, rtl, entry.ordinal) === role);
159
+ const candidates = uses.slice(1).filter((entry) => roleFor(entry.page, rtl, entry.ordinal) === role);
197
160
  if (candidates.length === 0) continue;
198
161
  const counts = /* @__PURE__ */ new Map();
199
162
  for (const entry of candidates) {
@@ -212,4 +175,4 @@ function chapterParents(story, model, mapping, report) {
212
175
  return result;
213
176
  }
214
177
  //#endregion
215
- export { chapterParents, mapMasters };
178
+ export { chapterParents, mapMasters, openingPage, storyPageUses };
package/dist/model.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IdmlReportData } from "./report.js";
2
- import { IdmlDocumentPreferences, IdmlFont, IdmlFramePreference, IdmlMargin, IdmlMasterPage, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory, IdmlStyle, IdmlSwatch, IdmlTextVariable } from "./types.js";
2
+ import { IdmlAnchoredFrame, 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;
@@ -7,6 +7,7 @@ export interface IdmlModel {
7
7
  spreads: IdmlSpread[];
8
8
  masterSpreads: IdmlMasterSpread[];
9
9
  stories: IdmlStory[];
10
+ anchoredFrames?: IdmlAnchoredFrame[];
10
11
  masterStoryIds: string[];
11
12
  partsRead: string[];
12
13
  styles?: IdmlStyle[];
package/dist/model.js CHANGED
@@ -2,13 +2,14 @@ import { refusal } from "./refusal.js";
2
2
  import { ReportCollector } from "./report.js";
3
3
  import { attribute, elements, localName, numberAttribute } from "./xml.js";
4
4
  import { parseFonts } from "./fonts.js";
5
- import { readMargin } from "./margins.js";
6
- import { parseStyles, parseSwatches } from "./styles.js";
7
5
  import { FramePreferenceIndex } from "./frame-preferences.js";
8
- import { IDML_LIMITS } from "./limits.js";
9
- import { parseParts } from "./parse-parts.js";
10
6
  import { GRAPHIC_ELEMENTS, SHAPE_ELEMENTS, collectUnsupportedElements } from "./unsupported-elements.js";
11
7
  import { attachPlacements, contains, pageArea, placementBounds, spreadPlacements } from "./placements.js";
8
+ import { parseStyles, parseSwatches } from "./styles.js";
9
+ import { parseAnchoredFrames } from "./anchored-frames.js";
10
+ import { readMargin } from "./margins.js";
11
+ import { IDML_LIMITS } from "./limits.js";
12
+ import { parseParts } from "./parse-parts.js";
12
13
  import { parseStories } from "./story-model.js";
13
14
  import { isTag } from "domhandler";
14
15
  //#region src/model.ts
@@ -22,9 +23,11 @@ function boolAttribute(element, name, fallback) {
22
23
  function page(element) {
23
24
  const id = attribute(element, "Self");
24
25
  if (!id) throw refusal("an IDML Page has no Self identifier", "give every Page a Self identifier");
26
+ const showMasterItems = boolAttribute(element, "ShowMasterItems", true);
25
27
  return {
26
28
  id,
27
29
  appliedMaster: attribute(element, "AppliedMaster"),
30
+ ...showMasterItems ? {} : { showMasterItems },
28
31
  stories: [],
29
32
  side: "single"
30
33
  };
@@ -96,6 +99,7 @@ function masterSpread(element, pageElements, pages, placements) {
96
99
  id: attribute(element, "Self") ?? base,
97
100
  name,
98
101
  ...attribute(element, "BasedOn") ? { basedOn: attribute(element, "BasedOn") } : {},
102
+ ...boolAttribute(element, "ShowMasterItems", true) ? {} : { showMasterItems: false },
99
103
  pages: pages.map((value, index) => ({
100
104
  ...value,
101
105
  margin: readMargin(elements(pageElements[index], "MarginPreference")[0]),
@@ -183,6 +187,8 @@ function idmlModel(entries, limits = IDML_LIMITS) {
183
187
  const spreadResult = spreads(parts, "Spread", spreadSources(designmap, "Spread"), report, framePreferences);
184
188
  const masterResult = spreads(parts, "MasterSpread", spreadSources(designmap, "MasterSpread"), report, new FramePreferenceIndex());
185
189
  const masterStoryIds = [...masterResult.frameStories].filter((story) => !spreadResult.frameStories.has(story));
190
+ const stories = parseStories(parts, framePreferences, report);
191
+ const styles = parts.flatMap((part) => part.name.endsWith("/Styles.xml") || part.name === "Resources/Styles.xml" ? parseStyles(part.all, part.name) : []);
186
192
  return {
187
193
  id: attribute(document, "Self") ?? "IDML document",
188
194
  preferences: {
@@ -194,12 +200,13 @@ function idmlModel(entries, limits = IDML_LIMITS) {
194
200
  },
195
201
  spreads: spreadResult.values,
196
202
  masterSpreads: masterResult.values,
197
- stories: parseStories(parts, framePreferences, report),
203
+ stories,
204
+ anchoredFrames: parseAnchoredFrames(stories, styles),
198
205
  bodyFramePreferences: framePreferences.values(),
199
206
  textVariables: textVariables(designmap),
200
207
  masterStoryIds,
201
208
  partsRead: parts.map((part) => part.name),
202
- styles: parts.flatMap((part) => part.name.endsWith("/Styles.xml") || part.name === "Resources/Styles.xml" ? parseStyles(part.all, part.name) : []),
209
+ styles,
203
210
  fonts: parts.flatMap((part) => part.name.endsWith("/Fonts.xml") || part.name === "Resources/Fonts.xml" ? parseFonts(part.all) : []),
204
211
  swatches: parts.flatMap((part) => part.name.endsWith("/Graphic.xml") || part.name === "Resources/Graphic.xml" ? parseSwatches(part.all) : []),
205
212
  report: report.data()
package/dist/openers.js CHANGED
@@ -2,18 +2,6 @@ import { formatRoundedNumber, positionedBoxStyle } from "./positioned-box.js";
2
2
  import { artSvg } from "./svg-path.js";
3
3
  import { findActiveContent, sanitize } from "@segnavia/html";
4
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
5
  function storyFramePages(model) {
18
6
  const result = /* @__PURE__ */ new Map();
19
7
  for (const spread of model.spreads) for (const page of spread.pages) for (const frame of page.bodyFrames ?? []) {
@@ -109,8 +97,8 @@ function claimHeaders(page, flow, claimed, framePages, byId) {
109
97
  }
110
98
  return headers;
111
99
  }
112
- function mapOpeners(model, stories, masters, report, asset) {
113
- const first = firstPages(model);
100
+ function mapOpeners(model, stories, pages, masters, report, asset) {
101
+ const first = new Map([...pages].flatMap(([story, uses]) => uses[0] ? [[story, uses[0]]] : []));
114
102
  const framePages = storyFramePages(model);
115
103
  const byId = new Map(stories.map((entry) => [entry.story.id, entry]));
116
104
  const claimed = /* @__PURE__ */ new Set();