@segnavia/idml 0.28.0 → 0.29.0

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