@segnavia/html 0.27.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.
Files changed (40) hide show
  1. package/dist/bindings.d.ts +1 -1
  2. package/dist/bindings.js +4 -1
  3. package/dist/book/book-document.d.ts +2 -0
  4. package/dist/book/book-document.js +149 -66
  5. package/dist/book/book-held.js +182 -0
  6. package/dist/book/book-view.d.ts +5 -0
  7. package/dist/book/book-view.js +26 -0
  8. package/dist/book/furniture.d.ts +14 -0
  9. package/dist/book/furniture.js +36 -0
  10. package/dist/book/notes.js +18 -9
  11. package/dist/book/paginate-book.d.ts +11 -0
  12. package/dist/book/paginate-book.js +195 -0
  13. package/dist/book/paginate-columns.d.ts +8 -0
  14. package/dist/book/paginate-columns.js +119 -0
  15. package/dist/book/paginate-keeps.d.ts +4 -0
  16. package/dist/book/paginate-keeps.js +80 -0
  17. package/dist/book/paginate-marks.d.ts +7 -0
  18. package/dist/book/paginate-marks.js +34 -0
  19. package/dist/book/paginate-notes.d.ts +21 -0
  20. package/dist/book/paginate-notes.js +105 -0
  21. package/dist/book/paginate-sequence.d.ts +4 -0
  22. package/dist/book/paginate-sequence.js +267 -0
  23. package/dist/book/paginate-target.d.ts +13 -0
  24. package/dist/book/paginate-target.js +36 -0
  25. package/dist/book/paginate-types.d.ts +170 -0
  26. package/dist/book/types.d.ts +1 -1
  27. package/dist/book-css.d.ts +5 -0
  28. package/dist/book-css.js +14 -0
  29. package/dist/css-tokens.js +189 -0
  30. package/dist/index.d.ts +9 -3
  31. package/dist/index.js +8 -3
  32. package/dist/motifs.d.ts +1 -0
  33. package/dist/motifs.js +5 -1
  34. package/dist/printed-pages.d.ts +18 -4
  35. package/dist/printed-pages.js +41 -19
  36. package/dist/sanitize-css.d.ts +6 -0
  37. package/dist/sanitize-css.js +308 -0
  38. package/dist/sanitize.d.ts +1 -0
  39. package/dist/sanitize.js +37 -15
  40. package/package.json +2 -2
@@ -3,7 +3,7 @@ import { SanitizeOptions } from "./sanitize.js";
3
3
  export declare const BINDINGS: readonly ["value", "href", "src", "each", "when", "present", "width"];
4
4
  export declare function bindingRoot(path: string): string;
5
5
  export declare function bindingNames(prefix: string): Record<(typeof BINDINGS)[number], string>;
6
- export interface FillOptions extends Pick<SanitizeOptions, 'linkOrigins'> {
6
+ export interface FillOptions extends Pick<SanitizeOptions, 'linkOrigins' | 'dataFonts'> {
7
7
  prefix: string;
8
8
  data: Record<string, unknown>;
9
9
  unavailable?: Iterable<string>;
package/dist/bindings.js CHANGED
@@ -77,7 +77,10 @@ function fill(html, options) {
77
77
  const unavailable = new Set(options.unavailable ?? []);
78
78
  const values = [];
79
79
  const deferred = new Set(options.deferPaths ?? []);
80
- const document = parse(sanitize(html, { linkOrigins: options.linkOrigins }));
80
+ const document = parse(sanitize(html, {
81
+ linkOrigins: options.linkOrigins,
82
+ dataFonts: options.dataFonts
83
+ }));
81
84
  const resolve = (path, stack) => {
82
85
  const { context, rest } = contextAt(stack, path);
83
86
  const value = atPath(context, rest);
@@ -1,5 +1,7 @@
1
1
  import { BookDocument, BookDocumentOptions } from "./types.js";
2
2
  import { Book } from "@segnavia/format";
3
3
  //#region src/book/book-document.d.ts
4
+ export declare const BOOK_COLUMN_MIN = 2;
5
+ export declare const BOOK_COLUMN_MAX = 4;
4
6
  export declare function bookDocument(book: Book, options?: BookDocumentOptions): BookDocument;
5
7
  //#endregion
@@ -1,34 +1,34 @@
1
1
  import { elements, parse, serialize } from "../sanitize.js";
2
- import { element, linkChildren, text, textContent } from "./dom.js";
3
- import { noteState, transformNotes } from "./notes.js";
2
+ import { element, linkChildren, text } from "./dom.js";
3
+ import { collectHeadings, designedSection, flowAttribs, markPlacementIssues, sectionNode, templateHolders, templateRegionNameIssues, templatedSection, undeclaredRegionIssues } from "./book-held.js";
4
+ import { endnotesInOrder, noteState, transformNotes } from "./notes.js";
4
5
  import { bookPrintPart } from "./types.js";
5
6
  import { isTag } from "domhandler";
6
- import { isBookContents, noteMarkerIssues, resolveDirection } from "@segnavia/format";
7
+ import { isBookContents, isDesignedSection, isTemplatedSection, noteMarkerIssues, resolveDirection } from "@segnavia/format";
7
8
  //#region src/book/book-document.ts
8
- const PRINT_CSS = ":where(.segnavia-contents-list){list-style:none;padding:0;margin:0}:where(.segnavia-contents-entry){display:flex}:where(.segnavia-contents-page){margin-inline-start:auto}:where(.segnavia-contents-entry[data-level=\"2\"]){padding-inline-start:1em}:where(.segnavia-contents-entry[data-level=\"3\"]){padding-inline-start:2em}:where(.segnavia-contents-entry[data-level=\"4\"]){padding-inline-start:3em}:where(.segnavia-contents-entry[data-level=\"5\"]){padding-inline-start:4em}:where(.segnavia-contents-entry[data-level=\"6\"]){padding-inline-start:5em}@media print{:where(.segnavia-front,.segnavia-contents)~:where(.segnavia-front,.segnavia-contents),:where(.segnavia-chapter,.segnavia-endnotes,.segnavia-blank){break-before:page}:where(.segnavia-blank){break-after:page}:where(.segnavia-targets){position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%)}}";
9
+ const CONTENTS_CSS = ":where(.segnavia-contents-list){list-style:none;padding:0;margin:0}:where(.segnavia-contents-entry){display:flex}:where(.segnavia-contents-page){margin-inline-start:auto}:where(.segnavia-contents-entry[data-level=\"2\"]){padding-inline-start:1em}:where(.segnavia-contents-entry[data-level=\"3\"]){padding-inline-start:2em}:where(.segnavia-contents-entry[data-level=\"4\"]){padding-inline-start:3em}:where(.segnavia-contents-entry[data-level=\"5\"]){padding-inline-start:4em}:where(.segnavia-contents-entry[data-level=\"6\"]){padding-inline-start:5em}";
10
+ const PRINT_CSS = `${CONTENTS_CSS}@media print{:where(.segnavia-front,.segnavia-contents)~:where(.segnavia-front,.segnavia-contents),:where(.segnavia-chapter,.segnavia-endnotes,.segnavia-blank){break-before:page}:where(.segnavia-blank){break-after:page}:where(.segnavia-targets){position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%)}}`;
11
+ const GALLEY_CSS = `${CONTENTS_CSS}:where([data-segnavia-galleys]){position:relative}:where([data-segnavia-flow]){orphans:inherit;widows:inherit}:where([data-segnavia-kind="fixed"],[data-segnavia-kind="templated"],[data-segnavia-template]){display:none}:where([data-segnavia-notes-galley]){border-block-start:1px solid;padding-block-start:8px}:where([data-segnavia-note]){break-inside:avoid}:where([data-segnavia-galleys] img,[data-segnavia-window] img,[data-segnavia-page-note] img){display:block;max-block-size:var(--segnavia-text-height);max-inline-size:100%;block-size:auto;inline-size:auto}`;
9
12
  function direction(book, value) {
10
13
  return resolveDirection(book.dir, book.lang, value);
11
14
  }
12
- function sectionNode(kind, id, children = []) {
13
- return element("section", {
14
- class: `segnavia-section segnavia-${kind}`,
15
- id
16
- }, children);
17
- }
18
- function authoredSection(section, id) {
19
- const node = sectionNode("front", id, parse(section.html).children);
15
+ function authoredSection(section, id, galley = false) {
16
+ const document = parse(section.html);
17
+ const runningHead = section.title;
18
+ const node = sectionNode("front", id, document.children, galley, runningHead, flowAttribs(section.opens, section.parents));
20
19
  return {
21
20
  info: {
22
21
  id,
23
22
  kind: "front",
24
23
  title: section.title,
25
- runningHead: section.title
24
+ runningHead
26
25
  },
27
26
  node
28
27
  };
29
28
  }
30
- function contentsSection(title) {
29
+ function contentsSection(entry, galley = false) {
31
30
  const id = "segnavia-contents";
31
+ const title = entry.title;
32
32
  const children = title !== void 0 ? [element("h1", {
33
33
  class: "segnavia-contents-title",
34
34
  dir: "ltr"
@@ -40,39 +40,39 @@ function contentsSection(title) {
40
40
  title,
41
41
  runningHead: title
42
42
  },
43
- node: sectionNode("contents", id, children)
43
+ node: sectionNode("contents", id, children, galley, title, flowAttribs(entry.opens, entry.parents))
44
44
  };
45
45
  }
46
- function chapterSection(chapter, index) {
46
+ function chapterSection(chapter, index, galley = false) {
47
47
  const id = `segnavia-chapter-${index}`;
48
48
  const document = parse(chapter.html);
49
+ const runningHead = chapter.runningHead ?? chapter.title;
49
50
  return {
50
51
  info: {
51
52
  id,
52
53
  kind: "chapter",
53
54
  title: chapter.title,
54
- runningHead: chapter.runningHead ?? chapter.title
55
+ runningHead
55
56
  },
56
- node: sectionNode("chapter", id, document.children)
57
+ node: sectionNode("chapter", id, document.children, galley, runningHead, flowAttribs(chapter.opens, chapter.parents))
57
58
  };
58
59
  }
60
+ function appendHeld(built, heldHeadings, extraIssues, book, notes, galley, sections, headings, issues, notesOwner = 0) {
61
+ sections.push(built);
62
+ issues.push(...reservedIdIssues(built), ...columnIssues(built), ...extraIssues);
63
+ headings.push(...heldHeadings);
64
+ transformNotes(built.node, book, 0, notes, galley, notesOwner);
65
+ }
66
+ function appendDesigned(entry, id, kind, book, galley, notes, sections, headings, issues, notesOwner = 0) {
67
+ const held = designedSection(entry, id, kind, galley, book.contents.depth);
68
+ appendHeld(held.built, held.headings, [], book, notes, galley, sections, headings, issues, notesOwner);
69
+ }
70
+ function appendTemplated(entry, id, kind, book, galley, notes, templates, sections, headings, issues, notesOwner = 0) {
71
+ const held = templatedSection(entry, id, kind, galley, book.contents.depth);
72
+ appendHeld(held.built, held.headings, undeclaredRegionIssues(entry, id, templates), book, notes, galley, sections, headings, issues, notesOwner);
73
+ }
59
74
  function chapterHeadings(section, chapter, depth) {
60
- let generated = 0;
61
- return elements(section.node.children).flatMap((heading) => {
62
- const match = /^h([2-6])$/.exec(heading.name);
63
- if (!match || heading.attribs["data-contents"] === "exclude") return [];
64
- const level = Number(match[1]);
65
- if (level > depth) return [];
66
- generated += 1;
67
- const id = heading.attribs.id ?? `segnavia-h-${chapter}-${generated}`;
68
- heading.attribs.id = id;
69
- return [{
70
- id,
71
- level,
72
- text: textContent(heading, true).replace(/\s+/g, " ").trim(),
73
- sectionId: section.info.id
74
- }];
75
- });
75
+ return collectHeadings(section.node.children, section.info.id, depth, 2, (n) => `segnavia-h-${chapter}-${n}`, { n: 0 });
76
76
  }
77
77
  function contentsList(book, headings, options) {
78
78
  const entries = headings.map((heading) => {
@@ -125,23 +125,46 @@ function reservedIdIssues(section) {
125
125
  return [`authored id ${id} in ${section.info.title ?? section.info.id} (${section.info.id}) uses the reserved segnavia- prefix`];
126
126
  });
127
127
  }
128
- function endnotesSection(book, paragraphs) {
129
- const id = "segnavia-endnotes";
128
+ const BOOK_COLUMN_MIN = 2;
129
+ const BOOK_COLUMN_MAX = 4;
130
+ function columnsValueOk(value) {
131
+ if (!/^[0-9]+$/.test(value)) return false;
132
+ const count = Number(value);
133
+ return count >= 2 && count <= 4;
134
+ }
135
+ function columnIssues(section) {
136
+ const where = `${section.info.title ?? section.info.id} (${section.info.id})`;
137
+ return elements(section.node.children).flatMap((node) => {
138
+ const value = node.attribs["data-columns"];
139
+ if (value === void 0 || columnsValueOk(value)) return [];
140
+ return [`The ${node.name} in ${where} could not establish data-columns ${JSON.stringify(value)}; set it to an integer from 2 to 4 to clear this`];
141
+ });
142
+ }
143
+ function endnotesHeading(book) {
130
144
  const titleValue = book.endnotes.title;
131
- const titleNode = element("h1", {
145
+ return element("h1", {
132
146
  class: "segnavia-endnotes-title",
133
147
  dir: direction(book, titleValue)
134
148
  }, [text(titleValue)]);
149
+ }
150
+ function endnotesSection(book, paragraphs, galley, id, runningHead, opens) {
135
151
  return {
136
152
  info: {
137
153
  id,
138
154
  kind: "endnotes",
139
- title: titleValue,
140
- runningHead: titleValue
155
+ title: book.endnotes.title,
156
+ runningHead
141
157
  },
142
- node: sectionNode("endnotes", id, [titleNode, ...paragraphs])
158
+ node: sectionNode("endnotes", id, [endnotesHeading(book), ...paragraphs], galley, runningHead, opens ? flowAttribs(opens) : {})
143
159
  };
144
160
  }
161
+ function notesGalley(paragraphs) {
162
+ const notes = paragraphs.map((paragraph) => element("div", { "data-segnavia-note": paragraph.attribs.id ?? "" }, [paragraph]));
163
+ return element("aside", {
164
+ "data-segnavia-notes-galley": "",
165
+ "aria-hidden": "true"
166
+ }, notes);
167
+ }
145
168
  function withBlankPages(sections, blankBefore) {
146
169
  const counts = /* @__PURE__ */ new Map();
147
170
  for (const id of blankBefore ?? []) counts.set(id, (counts.get(id) ?? 0) + 1);
@@ -150,50 +173,108 @@ function withBlankPages(sections, blankBefore) {
150
173
  "aria-hidden": "true"
151
174
  })), section.node]);
152
175
  }
153
- function bookDocument(book, options = {}) {
154
- const notes = noteState();
155
- const sections = [];
156
- const headings = [];
157
- const issues = [];
158
- let frontNumber = 0;
176
+ function takeHeldNotesOwner(build) {
177
+ build.heldNotesOwner -= 1;
178
+ return build.heldNotesOwner;
179
+ }
180
+ function pushChapterEndnotes(book, build, id, owner) {
181
+ if (book.endnotes.placement !== "chapter") return;
182
+ const paragraphs = build.notes.chapterEndnotes.get(owner) ?? [];
183
+ if (paragraphs.length === 0) return;
184
+ const runningHead = build.sections.at(-1)?.info.runningHead;
185
+ build.sections.push(endnotesSection(book, paragraphs, build.galley, id, runningHead, "any"));
186
+ }
187
+ function addFrontMatter(book, build) {
159
188
  let contents;
189
+ let frontNumber = 0;
160
190
  for (const entry of book.frontMatter) {
161
191
  if (isBookContents(entry)) {
162
- contents = contentsSection(entry.title);
163
- sections.push(contents);
192
+ contents = contentsSection(entry, build.galley);
193
+ build.sections.push(contents);
194
+ continue;
195
+ }
196
+ if (isDesignedSection(entry)) {
197
+ build.designedNumber += 1;
198
+ appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "front", book, build.galley, build.notes, build.sections, build.headings, build.issues);
199
+ continue;
200
+ }
201
+ if (isTemplatedSection(entry)) {
202
+ build.templatedNumber += 1;
203
+ appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "front", book, build.galley, build.notes, build.templates, build.sections, build.headings, build.issues);
164
204
  continue;
165
205
  }
166
206
  frontNumber += 1;
167
- const section = authoredSection(entry, `segnavia-front-${frontNumber}`);
168
- sections.push(section);
169
- issues.push(...reservedIdIssues(section));
170
- if (entry.contents && entry.title !== void 0) headings.push({
207
+ const section = authoredSection(entry, `segnavia-front-${frontNumber}`, build.galley);
208
+ build.sections.push(section);
209
+ build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
210
+ if (entry.contents && entry.title !== void 0) build.headings.push({
171
211
  id: section.info.id,
172
212
  level: 1,
173
213
  text: entry.title,
174
214
  sectionId: section.info.id
175
215
  });
176
- transformNotes(section.node, book, 0, notes);
216
+ transformNotes(section.node, book, 0, build.notes, build.galley);
177
217
  }
178
- for (const [offset, chapter] of book.chapters.entries()) {
179
- const number = offset + 1;
180
- const section = chapterSection(chapter, number);
181
- sections.push(section);
182
- issues.push(...reservedIdIssues(section));
183
- headings.push({
218
+ return contents;
219
+ }
220
+ function addChapters(book, build) {
221
+ let chapterNumber = 0;
222
+ for (const entry of book.chapters) {
223
+ if (isDesignedSection(entry)) {
224
+ build.designedNumber += 1;
225
+ const owner = takeHeldNotesOwner(build);
226
+ appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "designed", book, build.galley, build.notes, build.sections, build.headings, build.issues, owner);
227
+ pushChapterEndnotes(book, build, `segnavia-endnotes-designed-${build.designedNumber}`, owner);
228
+ continue;
229
+ }
230
+ if (isTemplatedSection(entry)) {
231
+ build.templatedNumber += 1;
232
+ const owner = takeHeldNotesOwner(build);
233
+ appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "templated", book, build.galley, build.notes, build.templates, build.sections, build.headings, build.issues, owner);
234
+ pushChapterEndnotes(book, build, `segnavia-endnotes-templated-${build.templatedNumber}`, owner);
235
+ continue;
236
+ }
237
+ chapterNumber += 1;
238
+ const section = chapterSection(entry, chapterNumber, build.galley);
239
+ build.sections.push(section);
240
+ build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
241
+ build.headings.push({
184
242
  id: section.info.id,
185
243
  level: 1,
186
- text: chapter.title,
244
+ text: entry.title,
187
245
  sectionId: section.info.id
188
- }, ...chapterHeadings(section, number, book.contents.depth));
189
- transformNotes(section.node, book, number, notes);
246
+ }, ...chapterHeadings(section, chapterNumber, book.contents.depth));
247
+ transformNotes(section.node, book, chapterNumber, build.notes, build.galley);
248
+ pushChapterEndnotes(book, build, `segnavia-endnotes-${chapterNumber}`, chapterNumber);
190
249
  }
191
- if (notes.endnotes) sections.push(endnotesSection(book, notes.endnoteParagraphs));
192
- if (book.endnotes.placement === "chapter") issues.push("endnotes at the end of each chapter belong to paginated mode, which is not built yet");
250
+ }
251
+ function bookDocument(book, options = {}) {
252
+ const build = {
253
+ galley: book.mode === "paginated",
254
+ notes: noteState(),
255
+ sections: [],
256
+ headings: [],
257
+ issues: [],
258
+ templates: new Map(book.templates.map((template) => [template.name, template])),
259
+ designedNumber: 0,
260
+ templatedNumber: 0,
261
+ heldNotesOwner: 0
262
+ };
263
+ const contents = addFrontMatter(book, build);
264
+ addChapters(book, build);
265
+ const { galley, notes, sections, headings, issues } = build;
266
+ if (book.endnotes.placement === "chapter") {
267
+ const leftover = notes.chapterEndnotes.get(0) ?? [];
268
+ if (leftover.length > 0) sections.push(endnotesSection(book, leftover, galley, "segnavia-endnotes", book.endnotes.title));
269
+ } else if (notes.endnotes) sections.push(endnotesSection(book, endnotesInOrder(notes), galley, "segnavia-endnotes", book.endnotes.title));
193
270
  if (contents) addContents(book, contents, headings, options);
194
271
  const visible = sections.filter((section) => options.part === void 0 || bookPrintPart(section.info.kind) === options.part);
195
272
  const document = parse("");
196
- document.children = [
273
+ document.children = galley ? [element("style", {}, [text(GALLEY_CSS)]), element("div", { "data-segnavia-galleys": "" }, [
274
+ ...templateHolders(book.templates),
275
+ ...visible.map((section) => section.node),
276
+ ...notes.footnoteParagraphs.length > 0 ? [notesGalley(notes.footnoteParagraphs)] : []
277
+ ])] : [
197
278
  element("style", {}, [text(PRINT_CSS)]),
198
279
  ...options.targets ? [targetLinks(visible)] : [],
199
280
  ...withBlankPages(visible, options.blankBefore)
@@ -208,10 +289,12 @@ function bookDocument(book, options = {}) {
208
289
  footnotes: notes.footnotes,
209
290
  endnotes: notes.endnotes
210
291
  }),
292
+ ...markPlacementIssues(book.parentPages),
293
+ ...templateRegionNameIssues(book.templates),
211
294
  ...issues,
212
295
  ...duplicateIdIssues(sections, headings)
213
296
  ]
214
297
  };
215
298
  }
216
299
  //#endregion
217
- export { bookDocument };
300
+ export { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, bookDocument };
@@ -0,0 +1,182 @@
1
+ import { elements, parse } from "../sanitize.js";
2
+ import { element, textContent } from "./dom.js";
3
+ import { BOOK_FURNITURE, MOTIF_ATTRIBUTE, REGION_NAME, motifFieldAttribute } from "@segnavia/format";
4
+ //#region src/book/book-held.ts
5
+ function flowAttribs(opens, parents, extra = {}) {
6
+ const attribs = { ...extra };
7
+ if (opens) attribs["data-opens"] = opens;
8
+ if (!parents) return attribs;
9
+ for (const role of [
10
+ "opener",
11
+ "recto",
12
+ "verso",
13
+ "blank",
14
+ "fixed"
15
+ ]) {
16
+ const name = parents[role];
17
+ if (name) attribs[`data-parent-${role}`] = name;
18
+ }
19
+ return attribs;
20
+ }
21
+ function sectionNode(kind, id, children = [], galley = false, runningHead, extra = {}) {
22
+ const attribs = {
23
+ class: `segnavia-section segnavia-${kind}`,
24
+ id,
25
+ ...extra
26
+ };
27
+ if (galley) attribs["data-segnavia-flow"] = id;
28
+ if (galley && runningHead !== void 0) attribs["data-running-head"] = runningHead;
29
+ return element("section", attribs, children);
30
+ }
31
+ function collectHeadings(children, sectionId, depth, firstLevel, idFor, generated) {
32
+ return elements(children).flatMap((heading) => {
33
+ const match = /^h([1-6])$/.exec(heading.name);
34
+ if (!match || heading.attribs["data-contents"] === "exclude") return [];
35
+ const level = Number(match[1]);
36
+ if (level < firstLevel || level > depth) return [];
37
+ generated.n += 1;
38
+ const id = heading.attribs.id ?? idFor(generated.n);
39
+ heading.attribs.id = id;
40
+ return [{
41
+ id,
42
+ level,
43
+ text: textContent(heading, true).replace(/\s+/g, " ").trim(),
44
+ sectionId
45
+ }];
46
+ });
47
+ }
48
+ function heldHeadings(children, sectionId, depth, generated) {
49
+ return collectHeadings(children, sectionId, depth, 1, (n) => `segnavia-h-${sectionId.replace(/^segnavia-/, "")}-${n}`, generated);
50
+ }
51
+ function designedSection(entry, id, kind, galley, depth) {
52
+ const extra = flowAttribs(entry.opens, entry.parents, {
53
+ "data-segnavia-kind": "fixed",
54
+ ...kind === "front" ? { "data-sequence": "front" } : {}
55
+ });
56
+ const headings = [];
57
+ const generated = { n: 0 };
58
+ const holders = entry.pages.map((page, offset) => {
59
+ const document = parse(page.html);
60
+ headings.push(...heldHeadings(document.children, id, depth, generated));
61
+ return element("div", {
62
+ "data-segnavia-designed-page": String(offset + 1),
63
+ hidden: ""
64
+ }, document.children);
65
+ });
66
+ if (entry.contents && entry.title !== void 0) headings.unshift({
67
+ id,
68
+ level: 1,
69
+ text: entry.title,
70
+ sectionId: id
71
+ });
72
+ const runningHead = entry.title;
73
+ return {
74
+ built: {
75
+ info: {
76
+ id,
77
+ kind,
78
+ title: entry.title,
79
+ runningHead
80
+ },
81
+ node: sectionNode(kind, id, holders, galley, runningHead, extra)
82
+ },
83
+ headings
84
+ };
85
+ }
86
+ function declaredRegions(html) {
87
+ return new Set(elements(parse(html).children).flatMap((node) => node.attribs["data-region"] ? [node.attribs["data-region"]] : []));
88
+ }
89
+ const regionNameClear = "use a letter followed by letters, digits, hyphens or underscores to clear this refusal";
90
+ function templateRegionNameIssues(templates) {
91
+ return templates.flatMap((template) => [...declaredRegions(template.html)].flatMap((key) => REGION_NAME.test(key) ? [] : [`template "${template.name}" marks region "${key}" which is not an HTML name token; ${regionNameClear}`]));
92
+ }
93
+ const markFurniture = BOOK_FURNITURE.find((item) => item.key === "segnavia:mark");
94
+ const markFields = new Set(markFurniture.fields.map((field) => motifFieldAttribute(field.key)));
95
+ const allowedMarkFields = markFurniture.fields.map((field) => field.key).join(" and ");
96
+ function markNodeIssues(parent, node) {
97
+ if (node.attribs[MOTIF_ATTRIBUTE] !== markFurniture.key) return [];
98
+ const issues = Object.keys(node.attribs).filter((attribute) => attribute.startsWith(`${MOTIF_ATTRIBUTE}-`) && !markFields.has(attribute)).map((attribute) => {
99
+ const field = attribute.slice(`${MOTIF_ATTRIBUTE}-`.length);
100
+ return `parent page "${parent.name}" mark has unknown field "${field}"; use only ${allowedMarkFields} to clear this refusal`;
101
+ });
102
+ for (const field of markFurniture.fields) {
103
+ const attribute = motifFieldAttribute(field.key);
104
+ const value = node.attribs[attribute] ?? ("default" in field ? field.default : void 0);
105
+ if ("required" in field && field.required && (value === void 0 || value.trim() === "")) issues.push(`parent page "${parent.name}" mark has an empty ${field.key}; set a non-empty ${attribute} on the segnavia:mark placement to clear this refusal`);
106
+ if ("choices" in field && value !== void 0) {
107
+ const choices = field.choices;
108
+ if (!choices.includes(value)) issues.push(`parent page "${parent.name}" mark has unknown ${field.key} value "${value}"; use ${choices.join(" or ")} to clear this refusal`);
109
+ }
110
+ }
111
+ return issues;
112
+ }
113
+ function markPlacementIssues(parents) {
114
+ return parents.flatMap((parent) => ["head", "foot"].flatMap((region) => {
115
+ const html = parent[region];
116
+ if (html === void 0) return [];
117
+ return elements(parse(html).children).flatMap((node) => markNodeIssues(parent, node));
118
+ }));
119
+ }
120
+ function undeclaredRegionIssues(entry, id, templates) {
121
+ const label = entry.title ?? id;
122
+ return entry.pages.flatMap((page, index) => {
123
+ const name = page.template ?? entry.template;
124
+ const template = templates.get(name);
125
+ if (!template) return [];
126
+ const declared = declaredRegions(template.html);
127
+ return Object.keys(page.regions).flatMap((region) => declared.has(region) ? [] : [`section ${label} page ${index + 1} fills region "${region}" which template "${name}" does not declare; add that region to the template or remove the fill to clear this refusal`]);
128
+ });
129
+ }
130
+ function templatedSection(entry, id, kind, galley, depth) {
131
+ const extra = flowAttribs(entry.opens, entry.parents, {
132
+ "data-segnavia-kind": "templated",
133
+ "data-template": entry.template,
134
+ ...kind === "front" ? { "data-sequence": "front" } : {}
135
+ });
136
+ const headings = [];
137
+ const generated = { n: 0 };
138
+ const holders = entry.pages.map((page, offset) => {
139
+ const name = page.template ?? entry.template;
140
+ const regions = Object.entries(page.regions).map(([region, html]) => {
141
+ const document = parse(html);
142
+ headings.push(...heldHeadings(document.children, id, depth, generated));
143
+ return element("div", {
144
+ "data-region": region,
145
+ hidden: ""
146
+ }, document.children);
147
+ });
148
+ return element("div", {
149
+ "data-segnavia-templated-page": String(offset + 1),
150
+ "data-template": name,
151
+ hidden: ""
152
+ }, regions);
153
+ });
154
+ if (entry.contents && entry.title !== void 0) headings.unshift({
155
+ id,
156
+ level: 1,
157
+ text: entry.title,
158
+ sectionId: id
159
+ });
160
+ const runningHead = entry.title;
161
+ return {
162
+ built: {
163
+ info: {
164
+ id,
165
+ kind,
166
+ title: entry.title,
167
+ runningHead
168
+ },
169
+ node: sectionNode(kind, id, holders, galley, runningHead, extra)
170
+ },
171
+ headings
172
+ };
173
+ }
174
+ function templateHolders(templates) {
175
+ return templates.map((template) => element("div", {
176
+ "data-segnavia-template": template.name,
177
+ hidden: "",
178
+ ...flowAttribs(void 0, template.parents)
179
+ }, parse(template.html).children));
180
+ }
181
+ //#endregion
182
+ export { collectHeadings, designedSection, flowAttribs, markPlacementIssues, sectionNode, templateHolders, templateRegionNameIssues, templatedSection, undeclaredRegionIssues };
@@ -0,0 +1,5 @@
1
+ import { Book, BookView } from "@segnavia/format";
2
+ //#region src/book/book-view.d.ts
3
+ export declare function bookViewFromHtml(book: Book, html: string): BookView;
4
+ export declare function bookView(book: Book): BookView;
5
+ //#endregion
@@ -0,0 +1,26 @@
1
+ import { bookDocument } from "./book-document.js";
2
+ import { resolveBookPagination } from "@segnavia/format";
3
+ //#region src/book/book-view.ts
4
+ function bookViewFromHtml(book, html) {
5
+ const view = {
6
+ html,
7
+ title: book.title,
8
+ scripts: book.scripts,
9
+ ...book.dir === void 0 ? {} : { dir: book.dir },
10
+ ...book.lang === void 0 ? {} : { lang: book.lang },
11
+ ...book.fonts === void 0 ? {} : { fonts: book.fonts },
12
+ ...book.css === void 0 ? {} : { css: book.css }
13
+ };
14
+ const pagination = resolveBookPagination(book);
15
+ return pagination ? {
16
+ ...view,
17
+ pagination
18
+ } : view;
19
+ }
20
+ function bookView(book) {
21
+ const document = bookDocument(book);
22
+ if (document.issues.length > 0) throw new Error(`The book cannot be shown: ${document.issues.join("; ")}.`);
23
+ return bookViewFromHtml(book, document.html);
24
+ }
25
+ //#endregion
26
+ export { bookView, bookViewFromHtml };
@@ -0,0 +1,14 @@
1
+ import { PaginatePageMarks } from "./paginate-types.js";
2
+ //#region src/book/furniture.d.ts
3
+ export interface BookFurnitureValues {
4
+ folio?: string;
5
+ runningHead?: string;
6
+ marks?: PaginatePageMarks;
7
+ }
8
+ export declare function fillBookFurniture(html: string, values: BookFurnitureValues, dir?: string, lang?: string): string;
9
+ export declare function embedFunction(name: string, fn: {
10
+ name: string;
11
+ toString(): string;
12
+ }): string;
13
+ export declare function furnitureScript(): string;
14
+ //#endregion
@@ -0,0 +1,36 @@
1
+ import { resolveDirection, textDirection } from "@segnavia/format";
2
+ //#region src/book/furniture.ts
3
+ function fillBookFurniture(html, values, dir, lang) {
4
+ const motifField = {
5
+ "segnavia:folio": "folio",
6
+ "segnavia:running-head": "runningHead"
7
+ };
8
+ const declared = dir === "ltr" || dir === "rtl" || dir === "auto" ? dir : void 0;
9
+ function attribute(attrs, name) {
10
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
+ const found = new RegExp(`\\s${escaped}\\s*=\\s*(?:(["'])([^"']*)\\1|([^\\s>]+))`, "i").exec(attrs);
12
+ return found?.[2] ?? found?.[3];
13
+ }
14
+ function escapeHtml(value) {
15
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
16
+ }
17
+ return html.replace(/<([a-zA-Z][\w:-]*)([^>]*?)\sdata-motif\s*=\s*(?:(["'])([^"']+)\3|([^\s>]+))([^>]*)>([\s\S]*?)<\/\1>/gi, (match, tag, before, _quote, quoted, unquoted, after) => {
18
+ const key = quoted ?? unquoted;
19
+ const field = motifField[key];
20
+ if (!field && key !== "segnavia:mark") return match;
21
+ const attrs = `${before}${after}`;
22
+ const name = key === "segnavia:mark" ? attribute(attrs, "data-motif-name") : void 0;
23
+ const which = key === "segnavia:mark" && attribute(attrs, "data-motif-which") === "last" ? "last" : "first";
24
+ const value = field ? values[field] : name === void 0 ? void 0 : values.marks?.[name]?.[which];
25
+ if (value === void 0) return "";
26
+ return `<${tag}${attrs.replace(/\sdata-motif(?:-[\w-]+)?\s*=\s*(?:(["'])([^"']*)\1|[^\s>]+)/gi, "")}><bdi dir="${resolveDirection(declared, lang, value)}">${escapeHtml(value)}</bdi></${tag}>`;
27
+ });
28
+ }
29
+ function embedFunction(name, fn) {
30
+ return `var ${fn.name}=${fn.toString()};var ${name}=${fn.name};`;
31
+ }
32
+ function furnitureScript() {
33
+ return `${embedFunction("textDirection", textDirection)}\n${embedFunction("resolveDirection", resolveDirection)}\n${embedFunction("fillBookFurniture", fillBookFurniture)}`;
34
+ }
35
+ //#endregion
36
+ export { embedFunction, fillBookFurniture, furnitureScript };