@segnavia/idml 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/model.js CHANGED
@@ -1,21 +1,15 @@
1
1
  import { refusal } from "./refusal.js";
2
+ import { localName, numberAttribute } from "./xml.js";
3
+ import { readMargin } from "./margins.js";
4
+ import { ReportCollector } from "./report.js";
5
+ import { parseStyles, parseSwatches } from "./styles.js";
6
+ import { FramePreferenceIndex, framePreference } from "./frame-preferences.js";
2
7
  import { IDML_LIMITS } from "./limits.js";
8
+ import { parseParts } from "./parse-parts.js";
9
+ import { parseStories } from "./story-model.js";
10
+ import { collectUnsupportedElements } from "./unsupported-elements.js";
3
11
  import { isTag } from "domhandler";
4
- import { Parser, parseDocument } from "htmlparser2";
5
12
  //#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
13
  function elements(root, name) {
20
14
  const found = [];
21
15
  const pending = [root];
@@ -35,12 +29,6 @@ function attribute(element, ...names) {
35
29
  if (value !== void 0) return value;
36
30
  }
37
31
  }
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
32
  function boolAttribute(element, name, fallback) {
45
33
  const raw = attribute(element, name);
46
34
  if (raw === void 0) return fallback;
@@ -48,57 +36,14 @@ function boolAttribute(element, name, fallback) {
48
36
  if (raw === "false") return false;
49
37
  throw refusal(`IDML ${name} must be true or false`, `set ${name} to true or false`);
50
38
  }
51
- function checkXmlDepth(source, part, limit) {
52
- let depth = 0;
53
- new Parser({
54
- onopentag() {
55
- depth += 1;
56
- if (depth > limit) throw refusal(`IDML XML part ${part} exceeds the ${limit} level XML depth limit`, `reduce its nesting to ${limit} levels or fewer`);
57
- },
58
- onclosetag() {
59
- depth -= 1;
60
- }
61
- }, {
62
- xmlMode: true,
63
- recognizeSelfClosing: true
64
- }).end(source);
65
- }
66
- function parseParts(entries, limits) {
67
- const decoder = new TextDecoder("utf-8", { fatal: true });
68
- return entries.filter((entry) => entry.name.toLowerCase().endsWith(".xml")).map((entry) => {
69
- let source;
70
- try {
71
- source = decoder.decode(entry.bytes);
72
- } catch {
73
- throw refusal(`IDML XML part ${entry.name} is not valid UTF-8`, "export that XML part as UTF-8");
74
- }
75
- let document;
76
- try {
77
- checkXmlDepth(source, entry.name, limits.xmlDepth);
78
- document = parseDocument(source, {
79
- xmlMode: true,
80
- recognizeSelfClosing: true
81
- });
82
- } catch (error) {
83
- if (error instanceof Error && error.message.includes("XML depth limit")) throw error;
84
- throw refusal(`IDML XML part ${entry.name} could not be parsed: ${String(error)}`, "export well-formed XML");
85
- }
86
- const all = elements(document);
87
- if (all.length === 0) throw refusal(`IDML XML part ${entry.name} has no root element`, "export a non-empty well-formed XML part");
88
- return {
89
- name: entry.name,
90
- document,
91
- all
92
- };
93
- });
94
- }
95
39
  function page(element) {
96
40
  const id = attribute(element, "Self");
97
41
  if (!id) throw refusal("an IDML Page has no Self identifier", "give every Page a Self identifier");
98
42
  return {
99
43
  id,
100
44
  appliedMaster: attribute(element, "AppliedMaster"),
101
- stories: []
45
+ stories: [],
46
+ side: "single"
102
47
  };
103
48
  }
104
49
  const IDENTITY = [
@@ -140,12 +85,19 @@ function pageArea(element) {
140
85
  apply(matrix, [left, bottom])
141
86
  ];
142
87
  }
143
- function frameCenter(element, matrix) {
144
- const anchors = 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));
145
- if (anchors.length === 0) return void 0;
146
- const xs = anchors.map(([x]) => x);
147
- const ys = anchors.map(([, y]) => y);
148
- return [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2];
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)));
149
101
  }
150
102
  function contains(area, [x, y]) {
151
103
  let inside = false;
@@ -157,7 +109,22 @@ function contains(area, [x, y]) {
157
109
  }
158
110
  return inside;
159
111
  }
160
- function spreadFrames(spread) {
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) {
161
128
  const result = [];
162
129
  const pending = [...spread.children].reverse().map((node) => ({
163
130
  node,
@@ -170,11 +137,8 @@ function spreadFrames(spread) {
170
137
  if (name === "Page" || name === "Spread" || name === "MasterSpread") continue;
171
138
  const matrix = name === "Group" || name === "TextFrame" ? compose(current.matrix, transform(current.node)) : current.matrix;
172
139
  if (name === "TextFrame") {
173
- const story = attribute(current.node, "ParentStory");
174
- if (story) result.push({
175
- story,
176
- center: frameCenter(current.node, matrix)
177
- });
140
+ const placement = framePlacement(current.node, matrix, `${location}#TextFrame[${result.length + 1}]`);
141
+ if (placement) result.push(placement);
178
142
  continue;
179
143
  }
180
144
  for (let index = current.node.children.length - 1; index >= 0; index -= 1) pending.push({
@@ -184,25 +148,99 @@ function spreadFrames(spread) {
184
148
  }
185
149
  return result;
186
150
  }
187
- function placeStories(element, pageElements, pages, unsupportedCounts) {
151
+ function assignSides(pageElements, pages) {
152
+ if (pages.length < 2) return;
153
+ const order = pageElements.map((element) => {
154
+ const area = pageArea(element);
155
+ return area ? bounds(area) : void 0;
156
+ }).map((area, index) => ({
157
+ index,
158
+ x: area ? (area.left + area.right) / 2 : index
159
+ })).sort((a, b) => a.x - b.x);
160
+ const left = order[0];
161
+ const right = order.at(-1);
162
+ const leftPage = left ? pages[left.index] : void 0;
163
+ const rightPage = right ? pages[right.index] : void 0;
164
+ if (leftPage) leftPage.side = "left";
165
+ if (rightPage && right?.index !== left?.index) rightPage.side = "right";
166
+ }
167
+ function masterFrames(frames, pageElements, pages) {
168
+ const result = pages.map(() => []);
169
+ const areas = pageElements.map((candidate) => {
170
+ const area = pageArea(candidate);
171
+ return area ? bounds(area) : void 0;
172
+ });
173
+ for (const frame of frames) {
174
+ const absolute = frame.bounds;
175
+ if (!absolute || !frame.center) continue;
176
+ const index = pageElements.findIndex((candidate) => {
177
+ const area = pageArea(candidate);
178
+ return area ? contains(area, frame.center) : false;
179
+ });
180
+ const area = areas[index];
181
+ if (index < 0 || !area) continue;
182
+ result[index]?.push({
183
+ id: frame.id,
184
+ story: frame.story,
185
+ bounds: {
186
+ left: absolute.left - area.left,
187
+ top: absolute.top - area.top,
188
+ width: absolute.right - absolute.left,
189
+ height: absolute.bottom - absolute.top
190
+ }
191
+ });
192
+ }
193
+ return result;
194
+ }
195
+ function masterSpread(element, pageElements, pages, placements) {
196
+ const frames = masterFrames(placements, pageElements, pages);
197
+ const prefix = attribute(element, "NamePrefix") ?? "";
198
+ const base = attribute(element, "BaseName") ?? attribute(element, "Name") ?? attribute(element, "Self") ?? "master";
199
+ const name = prefix ? `${prefix}-${base}` : base;
200
+ const frameIds = new Set(frames.flat().map((frame) => frame.id));
201
+ const unmappedItems = element.children.flatMap((child) => {
202
+ if (!isTag(child)) return [];
203
+ const type = localName(child.name);
204
+ if (type === "Page" || type === "Properties") return [];
205
+ const id = attribute(child, "Self") ?? type;
206
+ if (type === "TextFrame" && frameIds.has(id)) return [];
207
+ return [{
208
+ type,
209
+ id
210
+ }];
211
+ });
212
+ return {
213
+ id: attribute(element, "Self") ?? base,
214
+ name,
215
+ ...attribute(element, "BasedOn") ? { basedOn: attribute(element, "BasedOn") } : {},
216
+ pages: pages.map((value, index) => ({
217
+ ...value,
218
+ margin: readMargin(elements(pageElements[index], "MarginPreference")[0]),
219
+ frames: frames[index] ?? []
220
+ })),
221
+ unmappedItems
222
+ };
223
+ }
224
+ function placeStories(frames, pageElements, pages, report, framePreferences) {
188
225
  const areas = pageElements.map(pageArea);
189
226
  const frameStories = /* @__PURE__ */ new Set();
190
- for (const frame of spreadFrames(element)) {
227
+ for (const frame of frames) {
191
228
  frameStories.add(frame.story);
229
+ framePreferences.add(frame.story, frame.preference);
192
230
  const pageIndex = frame.center ? areas.findIndex((area) => area !== void 0 && contains(area, frame.center)) : -1;
193
231
  const target = pages[pageIndex];
194
232
  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);
233
+ else if (pageIndex < 0) report.add(report.unsupportedElements, "TextFrame off page", frame.story);
196
234
  }
197
235
  return frameStories;
198
236
  }
199
- function spreads(parts, kind, orderedNames, unsupportedCounts) {
237
+ function spreads(parts, kind, orderedNames, report, framePreferences) {
200
238
  const result = [];
201
239
  const frameStories = /* @__PURE__ */ new Set();
202
240
  const byName = new Map(parts.map((part) => [part.name, part]));
203
241
  const named = new Set(orderedNames);
204
242
  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);
243
+ if (unlisted > 0) report.add(report.unsupportedElements, "unlisted spread part", kind, unlisted);
206
244
  for (const name of orderedNames) {
207
245
  const part = byName.get(name);
208
246
  if (!part) throw refusal(`designmap.xml ${kind} src ${name} has no matching ZIP entry`, `add the named ${kind} part to the package`);
@@ -211,8 +249,10 @@ function spreads(parts, kind, orderedNames, unsupportedCounts) {
211
249
  if (!id) continue;
212
250
  const pageElements = elements(element, "Page");
213
251
  const pages = pageElements.map(page);
214
- for (const story of placeStories(element, pageElements, pages, unsupportedCounts)) frameStories.add(story);
215
- result.push({
252
+ 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) : {
216
256
  id,
217
257
  pages
218
258
  });
@@ -223,31 +263,23 @@ function spreads(parts, kind, orderedNames, unsupportedCounts) {
223
263
  frameStories
224
264
  };
225
265
  }
226
- function spreadSources(document, kind) {
227
- return document.all.filter((element) => localName(element.name) === kind).map((element) => attribute(element, "src")).filter((source) => source !== void 0);
228
- }
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")) {
266
+ function textVariables(designmap) {
267
+ return designmap.all.filter((element) => localName(element.name) === "TextVariable").flatMap((element) => {
233
268
  const id = attribute(element, "Self");
234
- if (!id || seen.has(id)) continue;
235
- seen.add(id);
236
- result.push({
269
+ if (!id) return [];
270
+ const preference = element.children.find((child) => isTag(child) && localName(child.name).endsWith("Preference"));
271
+ return [{
237
272
  id,
238
- part: part.name
239
- });
240
- }
241
- return result;
273
+ name: attribute(element, "Name") ?? id,
274
+ type: attribute(element, "VariableType") ?? "unknown",
275
+ appliedCharacterStyle: preference ? attribute(preference, "AppliedCharacterStyle") : void 0,
276
+ searchStrategy: preference ? attribute(preference, "SearchStrategy") : void 0,
277
+ location: `designmap.xml#${id}`
278
+ }];
279
+ });
242
280
  }
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;
281
+ function spreadSources(document, kind) {
282
+ return document.all.filter((element) => localName(element.name) === kind).map((element) => attribute(element, "src")).filter((source) => source !== void 0);
251
283
  }
252
284
  function idmlModel(entries, limits = IDML_LIMITS) {
253
285
  const parts = parseParts(entries, limits);
@@ -260,11 +292,12 @@ function idmlModel(entries, limits = IDML_LIMITS) {
260
292
  const margin = parts.flatMap((part) => part.all).find((element) => localName(element.name) === "MarginPreference");
261
293
  const binding = attribute(preference, "PageBinding") ?? attribute(document, "PageBinding") ?? "LeftToRight";
262
294
  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);
295
+ const report = new ReportCollector();
296
+ collectUnsupportedElements(parts, report);
297
+ const framePreferences = new FramePreferenceIndex();
298
+ const spreadResult = spreads(parts, "Spread", spreadSources(designmap, "Spread"), report, framePreferences);
299
+ const masterResult = spreads(parts, "MasterSpread", spreadSources(designmap, "MasterSpread"), report, new FramePreferenceIndex());
266
300
  const masterStoryIds = [...masterResult.frameStories].filter((story) => !spreadResult.frameStories.has(story));
267
- if (masterStoryIds.length > 0) unsupportedCounts.set("master story", masterStoryIds.length);
268
301
  return {
269
302
  id: attribute(document, "Self") ?? "IDML document",
270
303
  preferences: {
@@ -272,19 +305,18 @@ function idmlModel(entries, limits = IDML_LIMITS) {
272
305
  pageHeight: numberAttribute(preference, "PageHeight"),
273
306
  facingPages: boolAttribute(preference, "FacingPages", true),
274
307
  binding: binding === "RightToLeft" ? "right-to-left" : "left-to-right",
275
- margin: {
276
- top: numberAttribute(margin, "Top", 0),
277
- bottom: numberAttribute(margin, "Bottom", 0),
278
- left: numberAttribute(margin, "Left", 0),
279
- right: numberAttribute(margin, "Right", 0)
280
- }
308
+ margin: readMargin(margin)
281
309
  },
282
310
  spreads: spreadResult.values,
283
311
  masterSpreads: masterResult.values,
284
- stories: stories(parts),
312
+ stories: parseStories(parts, framePreferences, report),
313
+ bodyFramePreferences: framePreferences.values(),
314
+ textVariables: textVariables(designmap),
285
315
  masterStoryIds,
286
316
  partsRead: parts.map((part) => part.name),
287
- unsupportedElements: Object.fromEntries([...unsupportedCounts].sort(([left], [right]) => left.localeCompare(right)))
317
+ styles: parts.flatMap((part) => part.name.endsWith("/Styles.xml") || part.name === "Resources/Styles.xml" ? parseStyles(part.all, part.name) : []),
318
+ swatches: parts.flatMap((part) => part.name.endsWith("/Graphic.xml") || part.name === "Resources/Graphic.xml" ? parseSwatches(part.all) : []),
319
+ report: report.data()
288
320
  };
289
321
  }
290
322
  //#endregion
@@ -0,0 +1,61 @@
1
+ import { refusal } from "./refusal.js";
2
+ import { isTag } from "domhandler";
3
+ import { Parser, parseDocument } from "htmlparser2";
4
+ //#region src/parse-parts.ts
5
+ function allElements(root) {
6
+ const found = [];
7
+ const pending = [root];
8
+ while (pending.length > 0) {
9
+ const node = pending.pop();
10
+ if (!node) continue;
11
+ if (isTag(node)) found.push(node);
12
+ if ("children" in node) for (const child of [...node.children].reverse()) pending.push(child);
13
+ }
14
+ return found;
15
+ }
16
+ function checkDepth(source, part, limit) {
17
+ let depth = 0;
18
+ new Parser({
19
+ onopentag() {
20
+ depth += 1;
21
+ if (depth > limit) throw refusal(`IDML XML part ${part} exceeds the ${limit} level XML depth limit`, `reduce its nesting to ${limit} levels or fewer`);
22
+ },
23
+ onclosetag() {
24
+ depth -= 1;
25
+ }
26
+ }, {
27
+ xmlMode: true,
28
+ recognizeSelfClosing: true
29
+ }).end(source);
30
+ }
31
+ function parseParts(entries, limits) {
32
+ const decoder = new TextDecoder("utf-8", { fatal: true });
33
+ return entries.filter((entry) => entry.name.toLowerCase().endsWith(".xml")).map((entry) => {
34
+ let source;
35
+ try {
36
+ source = decoder.decode(entry.bytes);
37
+ } catch {
38
+ throw refusal(`IDML XML part ${entry.name} is not valid UTF-8`, "export that XML part as UTF-8");
39
+ }
40
+ let document;
41
+ try {
42
+ checkDepth(source, entry.name, limits.xmlDepth);
43
+ document = parseDocument(source, {
44
+ xmlMode: true,
45
+ recognizeSelfClosing: true
46
+ });
47
+ } catch (error) {
48
+ if (error instanceof Error && error.message.includes("XML depth limit")) throw error;
49
+ throw refusal(`IDML XML part ${entry.name} could not be parsed: ${String(error)}`, "export well-formed XML");
50
+ }
51
+ const all = allElements(document);
52
+ if (all.length === 0) throw refusal(`IDML XML part ${entry.name} has no root element`, "export a non-empty well-formed XML part");
53
+ return {
54
+ name: entry.name,
55
+ document,
56
+ all
57
+ };
58
+ });
59
+ }
60
+ //#endregion
61
+ export { parseParts };
@@ -0,0 +1,24 @@
1
+ //#region src/report.d.ts
2
+ export interface IdmlFontUse {
3
+ appliedFont: string;
4
+ fontStyle?: string;
5
+ locations: string[];
6
+ }
7
+ export interface IdmlMixedMasterChapter {
8
+ chapter: string;
9
+ side: 'recto' | 'verso';
10
+ masters: string[];
11
+ chosen: string;
12
+ }
13
+ export interface IdmlReportData {
14
+ unsupportedElements: Record<string, number>;
15
+ unsupportedStyleProperties: Record<string, number>;
16
+ unsupportedOverrideProperties: Record<string, number>;
17
+ unsupportedSpecialCharacters: Record<string, number>;
18
+ unsupportedLanguages: Record<string, number>;
19
+ spotColors: Record<string, number>;
20
+ locations: Record<string, string[]>;
21
+ fonts: IdmlFontUse[];
22
+ mixedMasterChapters: IdmlMixedMasterChapter[];
23
+ }
24
+ //#endregion
package/dist/report.js ADDED
@@ -0,0 +1,78 @@
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
+ mixedMasterChapters;
14
+ buckets = Object.fromEntries(Object.keys(BUCKET_NAMES).map((name) => [name, /* @__PURE__ */ new Map()]));
15
+ get unsupportedElements() {
16
+ return this.buckets.unsupportedElements;
17
+ }
18
+ get unsupportedStyleProperties() {
19
+ return this.buckets.unsupportedStyleProperties;
20
+ }
21
+ get unsupportedOverrideProperties() {
22
+ return this.buckets.unsupportedOverrideProperties;
23
+ }
24
+ get unsupportedSpecialCharacters() {
25
+ return this.buckets.unsupportedSpecialCharacters;
26
+ }
27
+ get unsupportedLanguages() {
28
+ return this.buckets.unsupportedLanguages;
29
+ }
30
+ get spotColors() {
31
+ return this.buckets.spotColors;
32
+ }
33
+ constructor(seed) {
34
+ this.mixedMasterChapters = [...seed?.mixedMasterChapters ?? []];
35
+ for (const name of Object.keys(BUCKET_NAMES)) for (const [key, count] of Object.entries(seed?.[name] ?? {})) this.buckets[name].set(key, count);
36
+ for (const [key, locations] of Object.entries(seed?.locations ?? {})) this.locations.set(key, [...locations]);
37
+ for (const font of seed?.fonts ?? []) this.font(font.appliedFont, font.fontStyle, font.locations[0] ?? "Resources/Styles.xml");
38
+ }
39
+ add(bucket, name, location, count = 1) {
40
+ bucket.set(name, (bucket.get(name) ?? 0) + count);
41
+ const key = `${this.bucketName(bucket)}:${name}`;
42
+ const values = this.locations.get(key) ?? [];
43
+ if (values.length < 5 && !values.includes(location)) values.push(location);
44
+ this.locations.set(key, values);
45
+ }
46
+ addOnce(bucket, name, location) {
47
+ const key = `${this.bucketName(bucket)}:${name}`;
48
+ if (this.locations.get(key)?.includes(location)) return;
49
+ this.add(bucket, name, location);
50
+ }
51
+ font(appliedFont, fontStyle, location) {
52
+ if (!appliedFont) return;
53
+ const key = `${appliedFont}\u0000${fontStyle ?? ""}`;
54
+ const found = this.fonts.get(key) ?? {
55
+ appliedFont,
56
+ ...fontStyle ? { fontStyle } : {},
57
+ locations: []
58
+ };
59
+ if (found.locations.length < 5 && !found.locations.includes(location)) found.locations.push(location);
60
+ this.fonts.set(key, found);
61
+ }
62
+ data() {
63
+ const record = (values) => Object.fromEntries([...values].sort(([a], [b]) => a.localeCompare(b)));
64
+ return {
65
+ ...Object.fromEntries(Object.keys(BUCKET_NAMES).map((name) => [name, record(this.buckets[name])])),
66
+ locations: Object.fromEntries([...this.locations].sort(([a], [b]) => a.localeCompare(b))),
67
+ fonts: [...this.fonts.values()],
68
+ mixedMasterChapters: [...this.mixedMasterChapters]
69
+ };
70
+ }
71
+ bucketName(bucket) {
72
+ const found = Object.keys(BUCKET_NAMES).find((name) => this.buckets[name] === bucket);
73
+ if (!found) throw new Error("Report bucket is not registered.");
74
+ return BUCKET_NAMES[found];
75
+ }
76
+ };
77
+ //#endregion
78
+ export { ReportCollector };