@segnavia/idml 0.38.0 → 0.40.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,7 @@
1
+ import { IdmlAssetMap } from "./types.js";
2
+ import { ResolvePlacement } from "@segnavia/html";
3
+ //#region src/asset-from-placement.d.ts
4
+ export declare function idmlAssetFrom<R extends {
5
+ kind: string;
6
+ }>(resolve: ResolvePlacement<R>, uris: Iterable<string>, context: Parameters<ResolvePlacement<R>>[1]): Promise<IdmlAssetMap>;
7
+ //#endregion
@@ -0,0 +1,21 @@
1
+ import { classifyMediaAnswer } from "@segnavia/html";
2
+ //#region src/asset-from-placement.ts
3
+ function escapeAttribute(value) {
4
+ return value.replaceAll("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
5
+ }
6
+ async function idmlAssetFrom(resolve, uris, context) {
7
+ const entries = await Promise.all([...new Set(uris)].map(async (uri) => {
8
+ const answer = await resolve({
9
+ kind: "media",
10
+ address: uri
11
+ }, context);
12
+ const classified = classifyMediaAnswer({ answer });
13
+ if (classified.status !== "ok") return [uri, void 0];
14
+ const alt = classified.value.media.alt?.text ?? "";
15
+ return [uri, `<img src="${escapeAttribute(classified.value.src)}" alt="${escapeAttribute(alt)}">`];
16
+ }));
17
+ const resolved = new Map(entries);
18
+ return (uri) => resolved.get(uri);
19
+ }
20
+ //#endregion
21
+ export { idmlAssetFrom };
package/dist/import.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { IdmlReportData } from "./report.js";
2
1
  import { IdmlImportOptions } from "./types.js";
2
+ import { IdmlReportData } from "./report.js";
3
3
  import { Book } from "@segnavia/format";
4
4
  //#region src/import.d.ts
5
5
  export interface IdmlImportReport extends IdmlReportData {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { IdmlFontUse, IdmlLinkUse, IdmlReportData } from "./report.js";
2
1
  import { IdmlAssetMap, IdmlDocumentPreferences, IdmlFont, IdmlFontMap, IdmlImportOptions, IdmlMargin, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory } from "./types.js";
2
+ import { idmlAssetFrom } from "./asset-from-placement.js";
3
+ import { IdmlFontUse, IdmlLinkUse, IdmlReportData } from "./report.js";
3
4
  import { IDML_FONT_WEIGHTS, idmlFontStyle } from "./fonts.js";
4
5
  import { IdmlImportReport, IdmlImportResult, importIdml } from "./import.js";
5
6
  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";
6
7
  import { IdmlModel } from "./model.js";
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 };
8
+ 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, idmlAssetFrom, idmlFontStyle, importIdml };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
+ import { idmlAssetFrom } from "./asset-from-placement.js";
1
2
  import { IDML_FONT_WEIGHTS, idmlFontStyle } from "./fonts.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
4
  import { importIdml } from "./import.js";
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 };
5
+ 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, idmlAssetFrom, idmlFontStyle, importIdml };
package/dist/map-book.js CHANGED
@@ -10,9 +10,10 @@ import { storyHtml } from "./stories.js";
10
10
  import { chapterParents, mapMasters, openingPage, storyPageUses } from "./masters.js";
11
11
  import { mapOpeners } from "./openers.js";
12
12
  import { reportPlacements } from "./placement-report.js";
13
+ import { TableStyleMapper } from "./table-styles.js";
13
14
  import { matchVariables } from "./variables.js";
14
- import { Book, MM_PER_PT } from "@segnavia/format";
15
15
  import { findActiveContent } from "@segnavia/html";
16
+ import { Book, MM_PER_PT } from "@segnavia/format";
16
17
  //#region src/map-book.ts
17
18
  function orderedStories(model, anchoredOnly = /* @__PURE__ */ new Set()) {
18
19
  const order = /* @__PURE__ */ new Map();
@@ -52,6 +53,7 @@ function modelToBook(model, report = new ReportCollector(model.report), options
52
53
  const bookDirection = rightBinding ? "rtl" : "ltr";
53
54
  const fonts = new FontMapper(model.fonts ?? [], options.font, report);
54
55
  const styles = new StyleMapper(model.styles ?? [], model.swatches ?? [], report, bookDirection, fonts);
56
+ const tableStyles = new TableStyleMapper(model.styles ?? [], model.swatches ?? [], report, (css) => styles.cssOverrideClass(css));
55
57
  const matchedVariables = matchVariables(model.textVariables ?? [], report);
56
58
  const bookLanguage = styles.rootLanguage();
57
59
  const storyPages = storyPageUses(model);
@@ -64,6 +66,7 @@ function modelToBook(model, report = new ReportCollector(model.report), options
64
66
  textVariables: matchedVariables,
65
67
  master
66
68
  }, styles, report, bookDirection, bookLanguage, {
69
+ tableStyles,
67
70
  inlineParagraphs: depth > 0,
68
71
  anchored: (location, direction) => {
69
72
  const frame = frameByLocation.get(location);
@@ -117,7 +120,7 @@ function modelToBook(model, report = new ReportCollector(model.report), options
117
120
  if (chapters.length === 0) throw refusal("the IDML has no stories", "add at least one Story");
118
121
  refuseActiveBookHtml(chapters, masters.parents);
119
122
  const columnGap = bodyColumnGap(model, stories, report);
120
- const css = `${styles.css()}${masters.css}`;
123
+ const css = `${tableStyles.css()}${styles.css()}${masters.css}`;
121
124
  const bookFonts = fonts.bookFonts();
122
125
  return Book.parse({
123
126
  title: model.id,
package/dist/model.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { IdmlReportData } from "./report.js";
2
1
  import { IdmlAnchoredFrame, IdmlDocumentPreferences, IdmlFont, IdmlFramePreference, IdmlMargin, IdmlMasterPage, IdmlMasterSpread, IdmlPage, IdmlSpread, IdmlStory, IdmlStyle, IdmlSwatch, IdmlTextVariable } from "./types.js";
2
+ import { IdmlReportData } from "./report.js";
3
3
  //#region src/model.d.ts
4
4
  export interface IdmlModel {
5
5
  id: string;
package/dist/stories.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { tableNode } from "./table-html.js";
2
+ import { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN } from "@segnavia/html";
1
3
  import { BOOK_FURNITURE, MOTIF_ATTRIBUTE, motifFieldAttribute, resolveDirection } from "@segnavia/format";
2
4
  import { Element, Text } from "domhandler";
3
- import { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN } from "@segnavia/html";
4
5
  import serialize from "dom-serializer";
5
6
  import { parseDocument } from "htmlparser2";
6
7
  //#region src/stories.ts
@@ -8,7 +9,6 @@ const OMITTED = /* @__PURE__ */ new Set([
8
9
  "Rectangle",
9
10
  "Group",
10
11
  "Polygon",
11
- "Table",
12
12
  "Footnote",
13
13
  "Note"
14
14
  ]);
@@ -46,6 +46,11 @@ function tokens(root, report) {
46
46
  character,
47
47
  location: node.location
48
48
  });
49
+ else if (node.name === "Table") result.push({
50
+ kind: "table",
51
+ node,
52
+ location: node.location
53
+ });
49
54
  else if (node.name === "TextVariableInstance") result.push({
50
55
  kind: "variable",
51
56
  variable: node.attributes.AssociatedTextVariable,
@@ -58,8 +63,7 @@ function tokens(root, report) {
58
63
  });
59
64
  else if (OMITTED.has(node.name)) report.add(report.unsupportedElements, node.name, node.location);
60
65
  else if (node.name === "Properties") return true;
61
- else if (node.name === "Change" && node.attributes.ChangeType !== void 0 && !node.attributes.ChangeType.includes("Inserted")) report.add(report.unsupportedElements, "Change", node.location);
62
- else return false;
66
+ else return reportedChange(node, report);
63
67
  return true;
64
68
  };
65
69
  const walk = (node, character) => {
@@ -86,6 +90,11 @@ function tokens(root, report) {
86
90
  walk(root);
87
91
  return result;
88
92
  }
93
+ function reportedChange(node, report) {
94
+ if (node.name !== "Change" || node.attributes.ChangeType === void 0 || node.attributes.ChangeType.includes("Inserted")) return false;
95
+ report.add(report.unsupportedElements, "Change", node.location);
96
+ return true;
97
+ }
89
98
  function contentNodes(token, styles, direction) {
90
99
  const character = token.character;
91
100
  const characterAttributes = rangeAttributes(character);
@@ -205,6 +214,45 @@ function paragraphNode(paragraph, paragraphTokens, styles, report, bookDirection
205
214
  style: "display:block"
206
215
  } : htmlAttributes, children);
207
216
  }
217
+ function mappedParagraph(paragraph, paragraphTokens, context, frame = context.story.frame) {
218
+ return paragraphNode(paragraph, paragraphTokens, context.styles, context.report, context.bookDirection, context.bookLanguage, context.story.direction, frame, context.story.textVariables ?? [], context.story.master ?? false, context.options);
219
+ }
220
+ function tableContent(cell, context) {
221
+ const output = [];
222
+ for (const paragraph of cell.children.filter((child) => child.name === "ParagraphStyleRange")) {
223
+ let current = [];
224
+ const flush = () => {
225
+ output.push(mappedParagraph(paragraph, current, context, void 0));
226
+ current = [];
227
+ };
228
+ for (const token of tokens(paragraph, context.report)) if (token.kind === "paragraph") flush();
229
+ else if (token.kind === "table") context.report.add(context.report.unsupportedElements, "Table", token.location);
230
+ else current.push(token);
231
+ flush();
232
+ }
233
+ return output;
234
+ }
235
+ function renderParagraphRange(paragraph, context) {
236
+ const output = [];
237
+ const all = tokens(paragraph, context.report);
238
+ let current = [];
239
+ let wroteTable = false;
240
+ const visible = () => current.some((token) => token.kind !== "text" || Boolean(token.text));
241
+ const flush = (empty = true) => {
242
+ if (empty || visible()) output.push(mappedParagraph(paragraph, current, context));
243
+ current = [];
244
+ };
245
+ for (const token of all) if (token.kind === "paragraph") flush();
246
+ else if (token.kind !== "table" || !token.node) current.push(token);
247
+ else {
248
+ flush(false);
249
+ if (!context.options.tableStyles) context.report.add(context.report.unsupportedElements, "Table", token.location);
250
+ else output.push(tableNode(token.node, context.options.tableStyles, (cell) => tableContent(cell, context)));
251
+ wroteTable = true;
252
+ }
253
+ if (visible() || !wroteTable && (all.length === 0 || all.at(-1)?.kind !== "paragraph")) flush();
254
+ return output;
255
+ }
208
256
  function storyHtml(story, styles, report, bookDirection, bookLanguage, options = {}) {
209
257
  if (!story.root) {
210
258
  const empty = new Element(options.inlineParagraphs ? "span" : "p", options.inlineParagraphs ? { style: "display:block" } : {}, [new Element("br", {}, [])]);
@@ -220,17 +268,15 @@ function storyHtml(story, styles, report, bookDirection, bookLanguage, options =
220
268
  else if (node.name === "Story" || node.name === "XMLElement") for (const child of node.children) collectParagraphs(child);
221
269
  };
222
270
  collectParagraphs(story.root);
223
- for (const paragraph of paragraphRanges) {
224
- const all = tokens(paragraph, report);
225
- let current = [];
226
- const flush = () => {
227
- paragraphs.push(paragraphNode(paragraph, current, styles, report, bookDirection, bookLanguage, story.direction, story.frame, story.textVariables ?? [], story.master ?? false, options));
228
- current = [];
229
- };
230
- for (const token of all) if (token.kind === "paragraph") flush();
231
- else current.push(token);
232
- if (current.length > 0 || all.length === 0 || all.at(-1)?.kind !== "paragraph") flush();
233
- }
271
+ const context = {
272
+ story,
273
+ styles,
274
+ report,
275
+ bookDirection,
276
+ bookLanguage,
277
+ options
278
+ };
279
+ for (const paragraph of paragraphRanges) paragraphs.push(...renderParagraphRange(paragraph, context));
234
280
  if (paragraphs.length === 0) paragraphs.push(new Element(options.inlineParagraphs ? "span" : "p", options.inlineParagraphs ? { style: "display:block" } : {}, [new Element("br", {}, [])]));
235
281
  return {
236
282
  html: serialize(paragraphs, {
package/dist/styles.js CHANGED
@@ -26,7 +26,9 @@ function parseStyles(roots, part) {
26
26
  for (const kind of [
27
27
  "ParagraphStyle",
28
28
  "CharacterStyle",
29
- "ObjectStyle"
29
+ "ObjectStyle",
30
+ "TableStyle",
31
+ "CellStyle"
30
32
  ]) for (const element of roots.filter((candidate) => localName(candidate.name) === kind)) {
31
33
  const values = properties(element);
32
34
  const id = values.Self;
@@ -34,7 +36,7 @@ function parseStyles(roots, part) {
34
36
  result.push({
35
37
  id,
36
38
  name: values.Name ?? id.split("/").at(-1) ?? id,
37
- kind: kind === "ParagraphStyle" ? "paragraph" : kind === "CharacterStyle" ? "character" : "object",
39
+ kind: STYLE_KINDS[kind],
38
40
  ...values.BasedOn ? { basedOn: values.BasedOn } : {},
39
41
  properties: Object.fromEntries(Object.entries(values).filter(([name]) => ![
40
42
  "Self",
@@ -46,6 +48,13 @@ function parseStyles(roots, part) {
46
48
  }
47
49
  return result;
48
50
  }
51
+ const STYLE_KINDS = {
52
+ ParagraphStyle: "paragraph",
53
+ CharacterStyle: "character",
54
+ ObjectStyle: "object",
55
+ TableStyle: "table",
56
+ CellStyle: "cell"
57
+ };
49
58
  function parseSwatches(roots) {
50
59
  const result = [];
51
60
  for (const element of roots.filter((candidate) => localName(candidate.name) === "Color")) {
@@ -65,7 +74,11 @@ function parseSwatches(roots) {
65
74
  return result;
66
75
  }
67
76
  function rootName(kind) {
68
- return kind === "paragraph" ? PARAGRAPH_ROOT : kind === "character" ? CHARACTER_ROOT : OBJECT_ROOT;
77
+ if (kind === "paragraph") return PARAGRAPH_ROOT;
78
+ if (kind === "character") return CHARACTER_ROOT;
79
+ if (kind === "table") return "$ID/[No table style]";
80
+ if (kind === "cell") return "$ID/[None]";
81
+ return OBJECT_ROOT;
69
82
  }
70
83
  function findStyle(styles, id, kind) {
71
84
  if (!id) return void 0;
@@ -147,6 +160,8 @@ var StyleMapper = class {
147
160
  classes = /* @__PURE__ */ new Map();
148
161
  used = /* @__PURE__ */ new Set();
149
162
  overrides = /* @__PURE__ */ new Map();
163
+ rawOverrides = /* @__PURE__ */ new Map();
164
+ nextOverride = 1;
150
165
  reportedStyleProperties = /* @__PURE__ */ new Set();
151
166
  constructor(styles, swatches, report, bookDirection, fonts) {
152
167
  this.swatches = swatches;
@@ -156,7 +171,7 @@ var StyleMapper = class {
156
171
  this.byId = new Map(styles.map((style) => [style.id, style]));
157
172
  const taken = /* @__PURE__ */ new Set();
158
173
  for (const style of styles) {
159
- if (style.kind === "object") continue;
174
+ if (style.kind === "object" || style.kind === "table" || style.kind === "cell") continue;
160
175
  const base = `${style.kind === "paragraph" ? "idml-p-" : "idml-c-"}${slug(style.name)}`;
161
176
  let candidate = base;
162
177
  let suffix = 2;
@@ -198,7 +213,7 @@ var StyleMapper = class {
198
213
  const key = JSON.stringify(Object.entries(cssProperties).sort(([a], [b]) => a.localeCompare(b))) + directionValue + resetShading;
199
214
  const found = this.overrides.get(key);
200
215
  if (found) return found.name;
201
- const name = `idml-o-${this.overrides.size + 1}`;
216
+ const name = `idml-o-${this.nextOverride++}`;
202
217
  this.overrides.set(key, {
203
218
  name,
204
219
  properties: cssProperties,
@@ -208,6 +223,17 @@ var StyleMapper = class {
208
223
  });
209
224
  return name;
210
225
  }
226
+ cssOverrideClass(declarations) {
227
+ if (!declarations) return void 0;
228
+ const found = this.rawOverrides.get(declarations);
229
+ if (found) return found.name;
230
+ const name = `idml-o-${this.nextOverride++}`;
231
+ this.rawOverrides.set(declarations, {
232
+ name,
233
+ declarations
234
+ });
235
+ return name;
236
+ }
211
237
  paragraphDirection(properties) {
212
238
  return idmlDirection(properties.ParagraphDirection);
213
239
  }
@@ -241,6 +267,7 @@ var StyleMapper = class {
241
267
  const declarations = this.declarations(value.properties, value.direction, value.location, this.report.unsupportedOverrideProperties, value.resetShading);
242
268
  if (declarations) rules.push(`.${value.name}{${declarations}}`);
243
269
  }
270
+ for (const value of this.rawOverrides.values()) rules.push(`.${value.name}{${value.declarations}}`);
244
271
  return rules.join("");
245
272
  }
246
273
  declarations(values, dir, location, unsupported, resetShading) {
@@ -320,4 +347,4 @@ var StyleMapper = class {
320
347
  }
321
348
  };
322
349
  //#endregion
323
- export { StyleMapper, flattenedStyle, parseStyles, parseSwatches };
350
+ export { StyleMapper, flattenedStyle, parseStyles, parseSwatches, slug, styleChain };
@@ -0,0 +1,49 @@
1
+ import { tableGrid } from "./table-model.js";
2
+ import { Element } from "domhandler";
3
+ import "dom-serializer";
4
+ //#region src/table-html.ts
5
+ function regionElement(region) {
6
+ return region === "header" ? "thead" : region === "footer" ? "tfoot" : "tbody";
7
+ }
8
+ function tableNode(node, styles, content) {
9
+ const grid = tableGrid(node);
10
+ const tableStyle = node.attributes.AppliedTableStyle;
11
+ const className = styles.tableClass(tableStyle);
12
+ const columns = new Element("colgroup", {}, grid.columns.map((width) => new Element("col", { style: `width:${width}pt` }, [])));
13
+ const groups = [];
14
+ let currentRegion;
15
+ let currentRows = [];
16
+ const flush = () => {
17
+ if (currentRegion && currentRows.length > 0) groups.push(new Element(regionElement(currentRegion), {}, currentRows));
18
+ currentRows = [];
19
+ };
20
+ for (const [rowIndex, row] of grid.rows.entries()) {
21
+ if (row.region !== currentRegion) {
22
+ flush();
23
+ currentRegion = row.region;
24
+ }
25
+ const cells = grid.cells.filter((cell) => cell.row === rowIndex);
26
+ currentRows.push(new Element("tr", row.fixed && row.height !== void 0 ? { style: `height:${row.height}pt` } : {}, cells.map((cell) => {
27
+ const classes = styles.cellClasses({
28
+ tableStyle,
29
+ region: row.region,
30
+ column: cell.column,
31
+ columnCount: grid.columns.length,
32
+ node: cell.node,
33
+ direction: grid.direction
34
+ });
35
+ return new Element("td", {
36
+ ...cell.rowSpan > 1 ? { rowspan: String(cell.rowSpan) } : {},
37
+ ...cell.columnSpan > 1 ? { colspan: String(cell.columnSpan) } : {},
38
+ ...classes.length > 0 ? { class: classes.join(" ") } : {}
39
+ }, content(cell.node));
40
+ })));
41
+ }
42
+ flush();
43
+ return new Element("table", {
44
+ ...className ? { class: className } : {},
45
+ dir: grid.direction
46
+ }, [columns, ...groups]);
47
+ }
48
+ //#endregion
49
+ export { tableNode };
@@ -0,0 +1,71 @@
1
+ import { refusal } from "./refusal.js";
2
+ //#region src/table-model.ts
3
+ function integer(value, fallback) {
4
+ const parsed = Number(value);
5
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
6
+ }
7
+ function children(node, name) {
8
+ return node.children.filter((child) => child.name === name);
9
+ }
10
+ function cellLabel(node) {
11
+ return node.attributes.Name ?? node.location;
12
+ }
13
+ function placeCell(table, cell, occupied, rowCount, columnCount) {
14
+ const match = /^(\d+):(\d+)$/.exec(cell.attributes.Name ?? "");
15
+ const column = match ? Number(match[1]) : -1;
16
+ const row = match ? Number(match[2]) : -1;
17
+ const rowSpan = Math.max(1, integer(cell.attributes.RowSpan, 1));
18
+ const columnSpan = Math.max(1, integer(cell.attributes.ColumnSpan, 1));
19
+ if (row < 0 || column < 0 || row + rowSpan > rowCount || column + columnSpan > columnCount) throw refusal(`table at ${table.location} has cell ${cellLabel(cell)} outside the grid`, "give every Cell a Name inside the declared rows and columns");
20
+ for (let y = row; y < row + rowSpan; y += 1) {
21
+ const rowCells = occupied[y];
22
+ if (!rowCells) throw refusal(`table at ${table.location} has cell ${cellLabel(cell)} outside the grid`, "give every Cell a Name inside the declared rows");
23
+ for (let x = column; x < column + columnSpan; x += 1) {
24
+ const found = rowCells[x];
25
+ if (found) throw refusal(`table at ${table.location} has overlapping cells ${cellLabel(found)} and ${cellLabel(cell)}`, "remove the overlapping cell spans");
26
+ rowCells[x] = cell;
27
+ }
28
+ }
29
+ return {
30
+ row,
31
+ column,
32
+ rowSpan,
33
+ columnSpan,
34
+ node: cell
35
+ };
36
+ }
37
+ function requireAnchors(table, occupied) {
38
+ for (const [row, columns] of occupied.entries()) for (const [column, cell] of columns.entries()) if (!cell) throw refusal(`table at ${table.location} has a missing anchor at ${column}:${row}`, "add a Cell that covers that grid position");
39
+ }
40
+ function tableGrid(node) {
41
+ const columns = children(node, "Column").map((column) => Number(column.attributes.SingleColumnWidth ?? column.attributes.Width));
42
+ const rowNodes = children(node, "Row");
43
+ const header = integer(node.attributes.HeaderRowCount, 0);
44
+ const footer = integer(node.attributes.FooterRowCount, 0);
45
+ const declaredBody = integer(node.attributes.BodyRowCount, rowNodes.length - header - footer);
46
+ const rowCount = header + declaredBody + footer;
47
+ const rows = rowNodes.map((row, index) => {
48
+ const height = Number(row.attributes.SingleRowHeight ?? row.attributes.Height);
49
+ return {
50
+ region: index < header ? "header" : index >= header + declaredBody ? "footer" : "body",
51
+ ...Number.isFinite(height) ? { height } : {},
52
+ fixed: row.attributes.AutoGrow === "false"
53
+ };
54
+ });
55
+ const widthCount = columns.length || integer(node.attributes.ColumnCount, 0);
56
+ if (columns.some((width) => !Number.isFinite(width))) throw refusal(`table at ${node.location} has a column without a finite width`, "give every Column a numeric SingleColumnWidth or Width");
57
+ if (widthCount === 0) throw refusal(`table at ${node.location} has no columns`, "export at least one Column or declare a positive ColumnCount");
58
+ if (rowCount !== rows.length) throw refusal(`table at ${node.location} declares ${rowCount} rows but contains ${rows.length} Row elements`, "make HeaderRowCount, BodyRowCount, and FooterRowCount add up to the number of Row elements");
59
+ const occupied = Array.from({ length: rowCount }, () => Array.from({ length: widthCount }));
60
+ const cells = children(node, "Cell").map((cell) => placeCell(node, cell, occupied, rowCount, widthCount));
61
+ requireAnchors(node, occupied);
62
+ cells.sort((a, b) => a.row - b.row || a.column - b.column);
63
+ return {
64
+ direction: node.attributes.TableDirection === "RightToLeft" ? "rtl" : "ltr",
65
+ columns,
66
+ rows,
67
+ cells
68
+ };
69
+ }
70
+ //#endregion
71
+ export { tableGrid };
@@ -0,0 +1,246 @@
1
+ import { resolveSwatchColor } from "./swatch-color.js";
2
+ import { reportableStyleProperties } from "./style-property-report.js";
3
+ import { flattenedStyle, slug, styleChain } from "./styles.js";
4
+ //#region src/table-styles.ts
5
+ const TABLE_HANDLED = /* @__PURE__ */ new Set([
6
+ "SpaceBefore",
7
+ "SpaceAfter",
8
+ "HeaderRegionCellStyle",
9
+ "BodyRegionCellStyle",
10
+ "FooterRegionCellStyle",
11
+ "LeftColumnRegionCellStyle",
12
+ "RightColumnRegionCellStyle",
13
+ "AlternatingFills",
14
+ "StartRowFillColor",
15
+ "StartRowFillCount",
16
+ "EndRowFillColor",
17
+ "EndRowFillCount",
18
+ "SkipFirstAlternatingFillRows",
19
+ "SkipLastAlternatingFillRows",
20
+ "StartColumnFillColor",
21
+ "StartColumnFillCount",
22
+ "EndColumnFillColor",
23
+ "EndColumnFillCount",
24
+ "SkipFirstAlternatingFillColumns",
25
+ "SkipLastAlternatingFillColumns"
26
+ ]);
27
+ const CELL_HANDLED = /* @__PURE__ */ new Set([
28
+ "AppliedCellStyle",
29
+ "FillColor",
30
+ "FillTint",
31
+ "VerticalJustification",
32
+ "TopInset",
33
+ "BottomInset",
34
+ "LeftInset",
35
+ "RightInset",
36
+ "TopEdgeStrokeWeight",
37
+ "BottomEdgeStrokeWeight",
38
+ "LeftEdgeStrokeWeight",
39
+ "RightEdgeStrokeWeight",
40
+ "TopEdgeStrokeColor",
41
+ "BottomEdgeStrokeColor",
42
+ "LeftEdgeStrokeColor",
43
+ "RightEdgeStrokeColor",
44
+ "TopEdgeStrokeType",
45
+ "BottomEdgeStrokeType",
46
+ "LeftEdgeStrokeType",
47
+ "RightEdgeStrokeType",
48
+ "CellType",
49
+ "TextRotation",
50
+ "DiagonalLineInFront",
51
+ "TopLeftDiagonalLine",
52
+ "TopRightDiagonalLine"
53
+ ]);
54
+ function uniqueClasses(styles, kind, prefix) {
55
+ const result = /* @__PURE__ */ new Map();
56
+ const taken = /* @__PURE__ */ new Set();
57
+ for (const style of styles.filter((candidate) => candidate.kind === kind)) {
58
+ const base = `${prefix}${slug(style.name)}`;
59
+ let value = base;
60
+ let suffix = 2;
61
+ while (taken.has(value)) value = `${base}-${suffix++}`;
62
+ taken.add(value);
63
+ result.set(style.id, value);
64
+ }
65
+ return result;
66
+ }
67
+ function tint(color, raw) {
68
+ const value = Number(raw);
69
+ return Number.isFinite(value) && value >= 0 && value < 100 ? `color-mix(in srgb, ${color} ${value}%, #fff)` : color;
70
+ }
71
+ function active(value) {
72
+ return value !== void 0 && ![
73
+ "",
74
+ "false",
75
+ "0",
76
+ "None",
77
+ "$ID/None"
78
+ ].includes(value);
79
+ }
80
+ var TableStyleMapper = class {
81
+ styles;
82
+ swatches;
83
+ report;
84
+ overrideClass;
85
+ tableClasses;
86
+ cellClassesById;
87
+ usedTables = /* @__PURE__ */ new Set();
88
+ usedRegionCells = /* @__PURE__ */ new Set();
89
+ usedOwnCells = /* @__PURE__ */ new Set();
90
+ reported = /* @__PURE__ */ new Set();
91
+ constructor(styles, swatches, report, overrideClass) {
92
+ this.styles = styles;
93
+ this.swatches = swatches;
94
+ this.report = report;
95
+ this.overrideClass = overrideClass;
96
+ this.tableClasses = uniqueClasses(styles, "table", "idml-t-");
97
+ this.cellClassesById = uniqueClasses(styles, "cell", "idml-cell-");
98
+ }
99
+ tableClass(id) {
100
+ const style = this.find(id, "table");
101
+ if (!style) return void 0;
102
+ this.usedTables.add(style.id);
103
+ return this.tableClasses.get(style.id);
104
+ }
105
+ cellClasses(input) {
106
+ const table = flattenedStyle(this.styles, input.tableStyle, "table");
107
+ const regionProperty = `${input.region[0]?.toUpperCase()}${input.region.slice(1)}RegionCellStyle`;
108
+ const physicalFirst = input.direction === "rtl" ? input.columnCount - 1 : 0;
109
+ const physicalLast = input.direction === "rtl" ? 0 : input.columnCount - 1;
110
+ const regionIds = [
111
+ table[regionProperty],
112
+ input.column === physicalFirst ? table.LeftColumnRegionCellStyle : void 0,
113
+ input.column === physicalLast ? table.RightColumnRegionCellStyle : void 0
114
+ ].filter((value) => Boolean(value));
115
+ const ownId = input.node.attributes.AppliedCellStyle;
116
+ const output = [];
117
+ for (const id of regionIds) {
118
+ const style = this.find(id, "cell");
119
+ if (!style) continue;
120
+ this.usedRegionCells.add(style.id);
121
+ const className = this.cellClassesById.get(style.id);
122
+ if (className) output.push(className);
123
+ }
124
+ if (ownId) {
125
+ const style = this.find(ownId, "cell");
126
+ if (style) {
127
+ this.usedOwnCells.add(style.id);
128
+ const className = this.cellClassesById.get(style.id);
129
+ if (className) output.push(className);
130
+ }
131
+ }
132
+ const ids = [...regionIds, ownId].filter((value) => Boolean(value));
133
+ const base = Object.assign({}, ...ids.map((id) => flattenedStyle(this.styles, id, "cell")));
134
+ const local = Object.fromEntries(Object.entries(input.node.attributes).filter(([name, value]) => CELL_HANDLED.has(name) && name !== "AppliedCellStyle" && value !== base[name]));
135
+ const override = this.overrideClass(this.cellDeclarations(local, input.node.location));
136
+ if (override) output.push(override);
137
+ this.reportFeatures(input.node);
138
+ return output;
139
+ }
140
+ css() {
141
+ const rules = [];
142
+ for (const id of this.usedTables) {
143
+ const style = this.find(id, "table");
144
+ const className = style && this.tableClasses.get(style.id);
145
+ if (!style || !className) continue;
146
+ const values = flattenedStyle(this.styles, id, "table");
147
+ const declarations = [`border-collapse:collapse`];
148
+ const before = Number(values.SpaceBefore);
149
+ const after = Number(values.SpaceAfter);
150
+ if (Number.isFinite(before)) declarations.push(`margin-block-start:${before}pt`);
151
+ if (Number.isFinite(after)) declarations.push(`margin-block-end:${after}pt`);
152
+ rules.push(`.${className}{${declarations.join(";")}}`, ...this.alternatingRules(className, values, style.location));
153
+ this.reportUnmapped(style, TABLE_HANDLED);
154
+ }
155
+ for (const id of [...this.usedRegionCells, ...this.usedOwnCells]) {
156
+ const style = this.find(id, "cell");
157
+ const className = style && this.cellClassesById.get(style.id);
158
+ if (!style || !className) continue;
159
+ rules.push(`.${className}{${this.cellDeclarations(flattenedStyle(this.styles, id, "cell"), style.location)}}`);
160
+ this.reportUnmapped(style, CELL_HANDLED);
161
+ }
162
+ return rules.join("");
163
+ }
164
+ find(id, kind) {
165
+ if (!id) return void 0;
166
+ return this.styles.find((style) => style.kind === kind && (style.id === id || style.name === id || style.id.endsWith(`/${id}`)));
167
+ }
168
+ reportUnmapped(style, handled) {
169
+ const resolved = {};
170
+ for (const source of styleChain(this.styles, style.id, style.kind)) {
171
+ Object.assign(resolved, source.properties);
172
+ for (const property of reportableStyleProperties(source.properties, resolved).filter((name) => !handled.has(name))) {
173
+ const key = `${source.id}\0${property}`;
174
+ if (this.reported.has(key)) continue;
175
+ this.reported.add(key);
176
+ this.report.add(this.report.unsupportedStyleProperties, property, source.location);
177
+ }
178
+ }
179
+ }
180
+ reportFeatures(node) {
181
+ if (node.attributes.CellType === "GraphicTypeCell") this.report.add(this.report.unsupportedElements, "Table:GraphicCell", node.location);
182
+ if (active(node.attributes.TextRotation)) this.report.add(this.report.unsupportedElements, "Table:RotatedCell", node.location);
183
+ if (active(node.attributes.DiagonalLineInFront) || active(node.attributes.TopLeftDiagonalLine) || active(node.attributes.TopRightDiagonalLine)) this.report.add(this.report.unsupportedElements, "Table:DiagonalLine", node.location);
184
+ }
185
+ cellDeclarations(values, location) {
186
+ const output = [];
187
+ const edge = (name) => {
188
+ const logical = name === "Top" ? "block-start" : name === "Bottom" ? "block-end" : name === "Left" ? "inline-start" : "inline-end";
189
+ const weight = Number(values[`${name}EdgeStrokeWeight`]);
190
+ if (Number.isFinite(weight)) output.push(`border-${logical}-width:${weight}pt`);
191
+ const type = values[`${name}EdgeStrokeType`];
192
+ if (type) {
193
+ const normalized = type.replace(/^\$ID\//, "").toLowerCase();
194
+ if ([
195
+ "solid",
196
+ "dashed",
197
+ "dotted"
198
+ ].includes(normalized)) output.push(`border-${logical}-style:${normalized}`);
199
+ else this.report.add(this.report.unsupportedElements, `Table:StrokeType:${type}`, location);
200
+ }
201
+ const color = resolveSwatchColor(values[`${name}EdgeStrokeColor`], this.swatches, this.report, location, "fill", `${name}EdgeStrokeColor`);
202
+ if (color && color !== "none") output.push(`border-${logical}-color:${color}`);
203
+ };
204
+ for (const name of [
205
+ "Top",
206
+ "Bottom",
207
+ "Left",
208
+ "Right"
209
+ ]) edge(name);
210
+ for (const [name, property] of [
211
+ ["TopInset", "block-start"],
212
+ ["BottomInset", "block-end"],
213
+ ["LeftInset", "inline-start"],
214
+ ["RightInset", "inline-end"]
215
+ ]) {
216
+ const value = Number(values[name]);
217
+ if (Number.isFinite(value)) output.push(`padding-${property}:${value}pt`);
218
+ }
219
+ const fill = resolveSwatchColor(values.FillColor, this.swatches, this.report, location, "fill");
220
+ if (fill && fill !== "none") output.push(`background-color:${tint(fill, values.FillTint)}`);
221
+ const vertical = values.VerticalJustification?.replace(/^\$ID\//, "").toLowerCase();
222
+ if (vertical === "topalign" || vertical === "top") output.push("vertical-align:top");
223
+ else if (vertical === "centeralign" || vertical === "center") output.push("vertical-align:middle");
224
+ else if (vertical === "bottomalign" || vertical === "bottom") output.push("vertical-align:bottom");
225
+ else if (vertical && vertical !== "none") this.report.add(this.report.unsupportedElements, "Table:VerticalJustify", location);
226
+ return output.join(";");
227
+ }
228
+ alternatingRules(className, values, location) {
229
+ const axis = values.AlternatingFills?.includes("Column") ? "Column" : values.AlternatingFills?.includes("Row") ? "Row" : void 0;
230
+ if (!axis) return [];
231
+ const a = Math.max(0, Number(values[`Start${axis}FillCount`]) || 0);
232
+ const period = a + Math.max(0, Number(values[`End${axis}FillCount`]) || 0);
233
+ if (period === 0) return [];
234
+ const selector = axis === "Row" ? "tr" : "td";
235
+ const skip = Math.max(0, Number(values[`SkipFirstAlternatingFill${axis}s`]) || 0);
236
+ const colors = [values[`Start${axis}FillColor`], values[`End${axis}FillColor`]];
237
+ const rules = [];
238
+ for (let index = 0; index < period; index += 1) {
239
+ const color = resolveSwatchColor(colors[index < a ? 0 : 1], this.swatches, this.report, location, "fill");
240
+ if (color && color !== "none") rules.push(`.${className} ${selector}:nth-child(${period}n+${skip + index + 1}){background-color:${color}}`);
241
+ }
242
+ return rules;
243
+ }
244
+ };
245
+ //#endregion
246
+ export { TableStyleMapper };
package/dist/types.d.ts CHANGED
@@ -10,7 +10,7 @@ export interface IdmlTextNode {
10
10
  export interface IdmlStyle {
11
11
  id: string;
12
12
  name: string;
13
- kind: 'paragraph' | 'character' | 'object';
13
+ kind: 'paragraph' | 'character' | 'object' | 'table' | 'cell';
14
14
  basedOn?: string;
15
15
  properties: Record<string, string>;
16
16
  location: string;
@@ -37,8 +37,14 @@ const MAPPED_ELEMENTS = /* @__PURE__ */ new Set([
37
37
  "ParagraphStyleGroup",
38
38
  "RootCharacterStyleGroup",
39
39
  "CharacterStyleGroup",
40
+ "RootTableStyleGroup",
41
+ "TableStyleGroup",
42
+ "RootCellStyleGroup",
43
+ "CellStyleGroup",
40
44
  "ParagraphStyle",
41
45
  "CharacterStyle",
46
+ "TableStyle",
47
+ "CellStyle",
42
48
  "AnchoredObjectSetting",
43
49
  "Color",
44
50
  "TextVariable",
@@ -49,7 +55,7 @@ function insideMappedResource(element) {
49
55
  let parent = element.parent;
50
56
  while (parent && "name" in parent) {
51
57
  const name = localName(parent.name);
52
- if (name === "ParagraphStyle" || name === "CharacterStyle" || name === "ObjectStyle" || name === "Color") return true;
58
+ if (name === "ParagraphStyle" || name === "CharacterStyle" || name === "ObjectStyle" || name === "TableStyle" || name === "CellStyle" || name === "Color") return true;
53
59
  parent = parent.parent;
54
60
  }
55
61
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segnavia/idml",
3
- "version": "0.38.0",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,8 +26,8 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@segnavia/format": "0.38.0",
30
- "@segnavia/html": "0.38.0",
29
+ "@segnavia/format": "0.40.0",
30
+ "@segnavia/html": "0.40.0",
31
31
  "dom-serializer": "^2.0.0",
32
32
  "domhandler": "^5.0.3",
33
33
  "htmlparser2": "^10.0.0"