@segnavia/html 0.27.0 → 0.28.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 (39) hide show
  1. package/dist/bindings.d.ts +1 -1
  2. package/dist/bindings.js +4 -1
  3. package/dist/book/book-document.js +146 -65
  4. package/dist/book/book-held.js +182 -0
  5. package/dist/book/book-view.d.ts +5 -0
  6. package/dist/book/book-view.js +26 -0
  7. package/dist/book/furniture.d.ts +14 -0
  8. package/dist/book/furniture.js +36 -0
  9. package/dist/book/notes.js +18 -9
  10. package/dist/book/paginate-book.d.ts +11 -0
  11. package/dist/book/paginate-book.js +195 -0
  12. package/dist/book/paginate-columns.d.ts +8 -0
  13. package/dist/book/paginate-columns.js +119 -0
  14. package/dist/book/paginate-keeps.d.ts +4 -0
  15. package/dist/book/paginate-keeps.js +80 -0
  16. package/dist/book/paginate-marks.d.ts +7 -0
  17. package/dist/book/paginate-marks.js +34 -0
  18. package/dist/book/paginate-notes.d.ts +21 -0
  19. package/dist/book/paginate-notes.js +105 -0
  20. package/dist/book/paginate-sequence.d.ts +4 -0
  21. package/dist/book/paginate-sequence.js +267 -0
  22. package/dist/book/paginate-target.d.ts +13 -0
  23. package/dist/book/paginate-target.js +36 -0
  24. package/dist/book/paginate-types.d.ts +170 -0
  25. package/dist/book/types.d.ts +1 -1
  26. package/dist/book-css.d.ts +5 -0
  27. package/dist/book-css.js +14 -0
  28. package/dist/css-tokens.js +189 -0
  29. package/dist/index.d.ts +8 -2
  30. package/dist/index.js +7 -2
  31. package/dist/motifs.d.ts +1 -0
  32. package/dist/motifs.js +5 -1
  33. package/dist/printed-pages.d.ts +18 -4
  34. package/dist/printed-pages.js +41 -19
  35. package/dist/sanitize-css.d.ts +6 -0
  36. package/dist/sanitize-css.js +308 -0
  37. package/dist/sanitize.d.ts +1 -0
  38. package/dist/sanitize.js +37 -15
  39. 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,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,44 @@ 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
+ function columnsValueOk(value) {
129
+ if (!/^[0-9]+$/.test(value)) return false;
130
+ const count = Number(value);
131
+ return count >= 2 && count <= 4;
132
+ }
133
+ function columnIssues(section) {
134
+ const where = `${section.info.title ?? section.info.id} (${section.info.id})`;
135
+ return elements(section.node.children).flatMap((node) => {
136
+ const value = node.attribs["data-columns"];
137
+ if (value === void 0 || columnsValueOk(value)) return [];
138
+ 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`];
139
+ });
140
+ }
141
+ function endnotesHeading(book) {
130
142
  const titleValue = book.endnotes.title;
131
- const titleNode = element("h1", {
143
+ return element("h1", {
132
144
  class: "segnavia-endnotes-title",
133
145
  dir: direction(book, titleValue)
134
146
  }, [text(titleValue)]);
147
+ }
148
+ function endnotesSection(book, paragraphs, galley, id, runningHead, opens) {
135
149
  return {
136
150
  info: {
137
151
  id,
138
152
  kind: "endnotes",
139
- title: titleValue,
140
- runningHead: titleValue
153
+ title: book.endnotes.title,
154
+ runningHead
141
155
  },
142
- node: sectionNode("endnotes", id, [titleNode, ...paragraphs])
156
+ node: sectionNode("endnotes", id, [endnotesHeading(book), ...paragraphs], galley, runningHead, opens ? flowAttribs(opens) : {})
143
157
  };
144
158
  }
159
+ function notesGalley(paragraphs) {
160
+ const notes = paragraphs.map((paragraph) => element("div", { "data-segnavia-note": paragraph.attribs.id ?? "" }, [paragraph]));
161
+ return element("aside", {
162
+ "data-segnavia-notes-galley": "",
163
+ "aria-hidden": "true"
164
+ }, notes);
165
+ }
145
166
  function withBlankPages(sections, blankBefore) {
146
167
  const counts = /* @__PURE__ */ new Map();
147
168
  for (const id of blankBefore ?? []) counts.set(id, (counts.get(id) ?? 0) + 1);
@@ -150,50 +171,108 @@ function withBlankPages(sections, blankBefore) {
150
171
  "aria-hidden": "true"
151
172
  })), section.node]);
152
173
  }
153
- function bookDocument(book, options = {}) {
154
- const notes = noteState();
155
- const sections = [];
156
- const headings = [];
157
- const issues = [];
158
- let frontNumber = 0;
174
+ function takeHeldNotesOwner(build) {
175
+ build.heldNotesOwner -= 1;
176
+ return build.heldNotesOwner;
177
+ }
178
+ function pushChapterEndnotes(book, build, id, owner) {
179
+ if (book.endnotes.placement !== "chapter") return;
180
+ const paragraphs = build.notes.chapterEndnotes.get(owner) ?? [];
181
+ if (paragraphs.length === 0) return;
182
+ const runningHead = build.sections.at(-1)?.info.runningHead;
183
+ build.sections.push(endnotesSection(book, paragraphs, build.galley, id, runningHead, "any"));
184
+ }
185
+ function addFrontMatter(book, build) {
159
186
  let contents;
187
+ let frontNumber = 0;
160
188
  for (const entry of book.frontMatter) {
161
189
  if (isBookContents(entry)) {
162
- contents = contentsSection(entry.title);
163
- sections.push(contents);
190
+ contents = contentsSection(entry, build.galley);
191
+ build.sections.push(contents);
192
+ continue;
193
+ }
194
+ if (isDesignedSection(entry)) {
195
+ build.designedNumber += 1;
196
+ appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "front", book, build.galley, build.notes, build.sections, build.headings, build.issues);
197
+ continue;
198
+ }
199
+ if (isTemplatedSection(entry)) {
200
+ build.templatedNumber += 1;
201
+ appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "front", book, build.galley, build.notes, build.templates, build.sections, build.headings, build.issues);
164
202
  continue;
165
203
  }
166
204
  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({
205
+ const section = authoredSection(entry, `segnavia-front-${frontNumber}`, build.galley);
206
+ build.sections.push(section);
207
+ build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
208
+ if (entry.contents && entry.title !== void 0) build.headings.push({
171
209
  id: section.info.id,
172
210
  level: 1,
173
211
  text: entry.title,
174
212
  sectionId: section.info.id
175
213
  });
176
- transformNotes(section.node, book, 0, notes);
214
+ transformNotes(section.node, book, 0, build.notes, build.galley);
177
215
  }
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({
216
+ return contents;
217
+ }
218
+ function addChapters(book, build) {
219
+ let chapterNumber = 0;
220
+ for (const entry of book.chapters) {
221
+ if (isDesignedSection(entry)) {
222
+ build.designedNumber += 1;
223
+ const owner = takeHeldNotesOwner(build);
224
+ appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "designed", book, build.galley, build.notes, build.sections, build.headings, build.issues, owner);
225
+ pushChapterEndnotes(book, build, `segnavia-endnotes-designed-${build.designedNumber}`, owner);
226
+ continue;
227
+ }
228
+ if (isTemplatedSection(entry)) {
229
+ build.templatedNumber += 1;
230
+ const owner = takeHeldNotesOwner(build);
231
+ appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "templated", book, build.galley, build.notes, build.templates, build.sections, build.headings, build.issues, owner);
232
+ pushChapterEndnotes(book, build, `segnavia-endnotes-templated-${build.templatedNumber}`, owner);
233
+ continue;
234
+ }
235
+ chapterNumber += 1;
236
+ const section = chapterSection(entry, chapterNumber, build.galley);
237
+ build.sections.push(section);
238
+ build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
239
+ build.headings.push({
184
240
  id: section.info.id,
185
241
  level: 1,
186
- text: chapter.title,
242
+ text: entry.title,
187
243
  sectionId: section.info.id
188
- }, ...chapterHeadings(section, number, book.contents.depth));
189
- transformNotes(section.node, book, number, notes);
244
+ }, ...chapterHeadings(section, chapterNumber, book.contents.depth));
245
+ transformNotes(section.node, book, chapterNumber, build.notes, build.galley);
246
+ pushChapterEndnotes(book, build, `segnavia-endnotes-${chapterNumber}`, chapterNumber);
190
247
  }
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");
248
+ }
249
+ function bookDocument(book, options = {}) {
250
+ const build = {
251
+ galley: book.mode === "paginated",
252
+ notes: noteState(),
253
+ sections: [],
254
+ headings: [],
255
+ issues: [],
256
+ templates: new Map(book.templates.map((template) => [template.name, template])),
257
+ designedNumber: 0,
258
+ templatedNumber: 0,
259
+ heldNotesOwner: 0
260
+ };
261
+ const contents = addFrontMatter(book, build);
262
+ addChapters(book, build);
263
+ const { galley, notes, sections, headings, issues } = build;
264
+ if (book.endnotes.placement === "chapter") {
265
+ const leftover = notes.chapterEndnotes.get(0) ?? [];
266
+ if (leftover.length > 0) sections.push(endnotesSection(book, leftover, galley, "segnavia-endnotes", book.endnotes.title));
267
+ } else if (notes.endnotes) sections.push(endnotesSection(book, endnotesInOrder(notes), galley, "segnavia-endnotes", book.endnotes.title));
193
268
  if (contents) addContents(book, contents, headings, options);
194
269
  const visible = sections.filter((section) => options.part === void 0 || bookPrintPart(section.info.kind) === options.part);
195
270
  const document = parse("");
196
- document.children = [
271
+ document.children = galley ? [element("style", {}, [text(GALLEY_CSS)]), element("div", { "data-segnavia-galleys": "" }, [
272
+ ...templateHolders(book.templates),
273
+ ...visible.map((section) => section.node),
274
+ ...notes.footnoteParagraphs.length > 0 ? [notesGalley(notes.footnoteParagraphs)] : []
275
+ ])] : [
197
276
  element("style", {}, [text(PRINT_CSS)]),
198
277
  ...options.targets ? [targetLinks(visible)] : [],
199
278
  ...withBlankPages(visible, options.blankBefore)
@@ -208,6 +287,8 @@ function bookDocument(book, options = {}) {
208
287
  footnotes: notes.footnotes,
209
288
  endnotes: notes.endnotes
210
289
  }),
290
+ ...markPlacementIssues(book.parentPages),
291
+ ...templateRegionNameIssues(book.templates),
211
292
  ...issues,
212
293
  ...duplicateIdIssues(sections, headings)
213
294
  ]
@@ -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 };
@@ -37,9 +37,13 @@ function noteState() {
37
37
  globalFootnote: 0,
38
38
  chapterFootnotes: /* @__PURE__ */ new Map(),
39
39
  endnoteCount: 0,
40
- endnoteParagraphs: []
40
+ chapterEndnotes: /* @__PURE__ */ new Map(),
41
+ footnoteParagraphs: []
41
42
  };
42
43
  }
44
+ function endnotesInOrder(state) {
45
+ return [...state.chapterEndnotes.values()].flat();
46
+ }
43
47
  function nearestBlock(note) {
44
48
  for (let parent = note.parent; parent; parent = parent.parent) if (isTag(parent) && BLOCKS.has(parent.name)) return parent;
45
49
  }
@@ -79,7 +83,7 @@ function addFootnoteBlock(block, paragraph, blocks) {
79
83
  if (INSIDE_BLOCKS.has(block.name)) append(block, notes);
80
84
  else insertAfter(block, notes);
81
85
  }
82
- function transformFootnote(note, book, chapter, state, blocks) {
86
+ function transformFootnote(note, book, chapter, state, blocks, galley) {
83
87
  const block = nearestBlock(note);
84
88
  if (!block) return;
85
89
  const number = footnoteNumber(book, chapter, state);
@@ -88,10 +92,12 @@ function transformFootnote(note, book, chapter, state, blocks) {
88
92
  const marker = noteMarker(number, book.footnotes.marker);
89
93
  const content = [...note.children];
90
94
  replace(note, element("sup", { class: "segnavia-note-ref" }, [markerLink(refId, noteId, marker)]));
91
- addFootnoteBlock(block, noteParagraph("segnavia-footnote", noteId, refId, marker, content), blocks);
95
+ const paragraph = noteParagraph("segnavia-footnote", noteId, refId, marker, content);
96
+ if (galley) state.footnoteParagraphs.push(paragraph);
97
+ else addFootnoteBlock(block, paragraph, blocks);
92
98
  state.footnotes = true;
93
99
  }
94
- function transformEndnote(note, book, state) {
100
+ function transformEndnote(note, book, state, owner) {
95
101
  state.endnoteCount += 1;
96
102
  const number = state.endnoteCount;
97
103
  const noteId = `segnavia-en-${number}`;
@@ -99,14 +105,17 @@ function transformEndnote(note, book, state) {
99
105
  const marker = noteMarker(number, book.endnotes.marker);
100
106
  const content = [...note.children];
101
107
  replace(note, element("sup", { class: "segnavia-note-ref" }, [markerLink(refId, noteId, marker)]));
102
- state.endnoteParagraphs.push(noteParagraph("segnavia-endnote", noteId, refId, marker, content));
108
+ const paragraph = noteParagraph("segnavia-endnote", noteId, refId, marker, content);
109
+ const held = state.chapterEndnotes.get(owner) ?? [];
110
+ held.push(paragraph);
111
+ state.chapterEndnotes.set(owner, held);
103
112
  state.endnotes = true;
104
113
  }
105
- function transformNotes(root, book, chapter, state) {
114
+ function transformNotes(root, book, chapter, state, galley = false, notesOwner = chapter) {
106
115
  const blocks = /* @__PURE__ */ new Map();
107
116
  const notes = elements(root.children).filter((node) => node.name === "span" && (node.attribs["data-note"] === "footnote" || node.attribs["data-note"] === "endnote"));
108
- for (const note of notes) if (note.attribs["data-note"] === "footnote") transformFootnote(note, book, chapter, state, blocks);
109
- else transformEndnote(note, book, state);
117
+ for (const note of notes) if (note.attribs["data-note"] === "footnote") transformFootnote(note, book, chapter, state, blocks, galley);
118
+ else transformEndnote(note, book, state, notesOwner);
110
119
  }
111
120
  //#endregion
112
- export { noteState, transformNotes };
121
+ export { endnotesInOrder, noteState, transformNotes };