@segnavia/html 0.36.0 → 0.38.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,33 @@
1
+ import { element, linkChildren, text } from "./dom.js";
2
+ import { isTag } from "domhandler";
3
+ import { resolveDirection } from "@segnavia/format";
4
+ //#region src/book/book-contents.ts
5
+ function contentsList(book, headings, options) {
6
+ const entries = headings.map((heading) => {
7
+ const page = book.contents.pageNumbers ? options.pageNumbers?.[heading.id] ?? "" : "";
8
+ const level = heading.level + (heading.depth ?? 0);
9
+ const attributes = {
10
+ class: "segnavia-contents-entry",
11
+ "data-level": String(level),
12
+ ...heading.depth === void 0 ? {} : { "data-depth": String(heading.depth) }
13
+ };
14
+ return element("li", attributes, [element("a", {
15
+ href: options.crossLinks ? `segnavia:target/${heading.id}` : `#${heading.id}`,
16
+ dir: resolveDirection(book.dir, book.lang, heading.text)
17
+ }, [text(heading.text)]), element("span", {
18
+ class: "segnavia-contents-page",
19
+ "data-target": heading.id,
20
+ dir: resolveDirection(book.dir, book.lang, page)
21
+ }, [text(page)])]);
22
+ });
23
+ return element("ol", { class: "segnavia-contents-list" }, entries);
24
+ }
25
+ function addContents(book, section, headings, options) {
26
+ const title = section.info.title;
27
+ const titleNode = section.node.children.find((child) => isTag(child) && child.name === "h1");
28
+ if (title !== void 0 && titleNode && isTag(titleNode)) titleNode.attribs.dir = resolveDirection(book.dir, book.lang, title);
29
+ section.node.children.push(contentsList(book, headings, options));
30
+ linkChildren(section.node);
31
+ }
32
+ //#endregion
33
+ export { addContents };
@@ -0,0 +1,30 @@
1
+ import { elements } from "../sanitize.js";
2
+ import { element, text } from "./dom.js";
3
+ //#region src/book/book-document-nodes.ts
4
+ function targetLinks(sections) {
5
+ const ids = sections.flatMap((section) => [section.info.id, ...elements(section.node.children).flatMap((node) => node.attribs.id ? [node.attribs.id] : [])]);
6
+ return element("div", {
7
+ class: "segnavia-targets",
8
+ "aria-hidden": "true"
9
+ }, [...new Set(ids)].map((id) => element("a", {
10
+ href: `#${id}`,
11
+ tabindex: "-1"
12
+ }, [text("​")])));
13
+ }
14
+ function notesGalley(paragraphs) {
15
+ const notes = paragraphs.map((paragraph) => element("div", { "data-segnavia-note": paragraph.attribs.id ?? "" }, [paragraph]));
16
+ return element("aside", {
17
+ "data-segnavia-notes-galley": "",
18
+ "aria-hidden": "true"
19
+ }, notes);
20
+ }
21
+ function withBlankPages(sections, blankBefore) {
22
+ const counts = /* @__PURE__ */ new Map();
23
+ for (const id of blankBefore ?? []) counts.set(id, (counts.get(id) ?? 0) + 1);
24
+ return sections.flatMap((section) => [...Array.from({ length: counts.get(section.info.id) ?? 0 }, () => element("div", {
25
+ class: "segnavia-blank",
26
+ "aria-hidden": "true"
27
+ })), section.node]);
28
+ }
29
+ //#endregion
30
+ export { notesGalley, targetLinks, withBlankPages };
@@ -1,7 +1,7 @@
1
1
  import { BookDocument, BookDocumentOptions } from "./types.js";
2
+ import "./book-held.js";
3
+ import "./notes.js";
2
4
  import { Book } from "@segnavia/format";
3
5
  //#region src/book/book-document.d.ts
4
- export declare const BOOK_COLUMN_MIN = 2;
5
- export declare const BOOK_COLUMN_MAX = 4;
6
6
  export declare function bookDocument(book: Book, options?: BookDocumentOptions): BookDocument;
7
7
  //#endregion
@@ -1,32 +1,16 @@
1
1
  import { elements, parse, serialize } from "../sanitize.js";
2
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";
3
+ import { addContents } from "./book-contents.js";
4
+ import { notesGalley, targetLinks, withBlankPages } from "./book-document-nodes.js";
5
+ import { bookDocumentCss, bookFlowPageGeometry, markBlockPositions, resolveBookBuild } from "./book-flow.js";
6
+ import { authoredSection, collectHeadings, designedSection, flowAttribs, markPlacementIssues, sectionNode, templateHolders, templateRegionIssues, templatedSection, templatedSectionIssues } from "./book-held.js";
7
+ import { duplicateIdIssues, reservedIdIssues } from "./book-issues.js";
8
+ import { addPart, attachPendingPartHeadings, partNames } from "./book-part-build.js";
9
+ import { noteState, transformNotes } from "./notes.js";
5
10
  import { bookPrintPart } from "./types.js";
6
- import { isTag } from "domhandler";
7
- import { isBookContents, isDesignedSection, isTemplatedSection, noteMarkerIssues, resolveDirection } from "@segnavia/format";
11
+ import { bookSections, isBookContents, isDesignedSection, isPart, isTemplatedSection, noteMarkerIssues, resolveDirection } from "@segnavia/format";
8
12
  //#region src/book/book-document.ts
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}`;
12
- function direction(book, value) {
13
- return resolveDirection(book.dir, book.lang, value);
14
- }
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));
19
- return {
20
- info: {
21
- id,
22
- kind: "front",
23
- title: section.title,
24
- runningHead
25
- },
26
- node
27
- };
28
- }
29
- function contentsSection(entry, galley = false) {
13
+ function contentsSection(entry, build) {
30
14
  const id = "segnavia-contents";
31
15
  const title = entry.title;
32
16
  const children = title !== void 0 ? [element("h1", {
@@ -37,13 +21,14 @@ function contentsSection(entry, galley = false) {
37
21
  info: {
38
22
  id,
39
23
  kind: "contents",
24
+ division: "front",
40
25
  title,
41
26
  runningHead: title
42
27
  },
43
- node: sectionNode("contents", id, children, galley, title, flowAttribs(entry.opens, entry.parents))
28
+ node: sectionNode("contents", id, children, build, title, flowAttribs(entry.opens, entry.parents))
44
29
  };
45
30
  }
46
- function chapterSection(chapter, index, galley = false) {
31
+ function chapterSection(chapter, index, division, build) {
47
32
  const id = `segnavia-chapter-${index}`;
48
33
  const document = parse(chapter.html);
49
34
  const runningHead = chapter.runningHead ?? chapter.title;
@@ -51,82 +36,40 @@ function chapterSection(chapter, index, galley = false) {
51
36
  info: {
52
37
  id,
53
38
  kind: "chapter",
39
+ division,
54
40
  title: chapter.title,
55
41
  runningHead
56
42
  },
57
- node: sectionNode("chapter", id, document.children, galley, runningHead, flowAttribs(chapter.opens, chapter.parents))
43
+ node: sectionNode("chapter", id, document.children, build, runningHead, flowAttribs(chapter.opens, chapter.parents))
58
44
  };
59
45
  }
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);
46
+ function appendDesigned(entry, id, kind, book, build, sectionEntry) {
47
+ const held = designedSection(entry, id, kind, sectionEntry.division, build.build, book.contents.depth, build.build === "flow" ? bookFlowPageGeometry(book) : void 0);
48
+ build.sections.push(held.built);
49
+ decorateSection(sectionEntry, build);
50
+ build.issues.push(...reservedIdIssues(held.built), ...columnIssues(held.built));
51
+ build.headings.push(...withSectionDepth(held.headings, sectionEntry.depth));
52
+ const context = noteContext(sectionEntry, held.built.info.id, 0, entry.title, build);
53
+ transformNotes(held.built.node, book, context, build.notes, build.build === "galley");
65
54
  }
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);
55
+ function appendTemplated(entry, id, kind, book, build, sectionEntry) {
56
+ const held = templatedSection(entry, id, kind, sectionEntry.division, build.build, book.contents.depth, build.templates, build.build === "flow" ? bookFlowPageGeometry(book) : void 0);
57
+ build.sections.push(held.built);
58
+ decorateSection(sectionEntry, build);
59
+ build.issues.push(...reservedIdIssues(held.built), ...columnIssues(held.built), ...templatedSectionIssues(entry, id, build.templates));
60
+ build.headings.push(...withSectionDepth(held.headings, sectionEntry.depth));
61
+ const context = noteContext(sectionEntry, held.built.info.id, 0, entry.title, build);
62
+ transformNotes(held.built.node, book, context, build.notes, build.build === "galley");
69
63
  }
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);
64
+ function withSectionDepth(headings, depth) {
65
+ return depth > 0 ? headings.map((heading) => ({
66
+ ...heading,
67
+ depth
68
+ })) : headings;
73
69
  }
74
70
  function chapterHeadings(section, chapter, depth) {
75
71
  return collectHeadings(section.node.children, section.info.id, depth, 2, (n) => `segnavia-h-${chapter}-${n}`, { n: 0 });
76
72
  }
77
- function contentsList(book, headings, options) {
78
- const entries = headings.map((heading) => {
79
- const page = book.contents.pageNumbers ? options.pageNumbers?.[heading.id] ?? "" : "";
80
- return element("li", {
81
- class: "segnavia-contents-entry",
82
- "data-level": String(heading.level)
83
- }, [element("a", {
84
- href: options.crossLinks ? `segnavia:target/${heading.id}` : `#${heading.id}`,
85
- dir: direction(book, heading.text)
86
- }, [text(heading.text)]), element("span", {
87
- class: "segnavia-contents-page",
88
- "data-target": heading.id,
89
- dir: direction(book, page)
90
- }, [text(page)])]);
91
- });
92
- return element("ol", { class: "segnavia-contents-list" }, entries);
93
- }
94
- function addContents(book, section, headings, options) {
95
- const title = section.info.title;
96
- const titleNode = section.node.children.find((child) => isTag(child) && child.name === "h1");
97
- if (title !== void 0 && titleNode && isTag(titleNode)) titleNode.attribs.dir = direction(book, title);
98
- section.node.children.push(contentsList(book, headings, options));
99
- linkChildren(section.node);
100
- }
101
- function targetLinks(sections) {
102
- const ids = sections.flatMap((section) => [section.info.id, ...elements(section.node.children).flatMap((node) => node.attribs.id ? [node.attribs.id] : [])]);
103
- return element("div", {
104
- class: "segnavia-targets",
105
- "aria-hidden": "true"
106
- }, [...new Set(ids)].map((id) => element("a", { href: `#${id}` }, [text("​")])));
107
- }
108
- function duplicateIdIssues(sections, headings) {
109
- const listed = new Set(headings.map((heading) => heading.id));
110
- const occurrences = /* @__PURE__ */ new Map();
111
- for (const section of sections) {
112
- const ids = [section.info.id, ...elements(section.node.children).flatMap((node) => node.attribs.id ? [node.attribs.id] : [])];
113
- for (const id of ids) occurrences.set(id, [...occurrences.get(id) ?? [], section.info]);
114
- }
115
- return [...occurrences].flatMap(([id, owners]) => {
116
- if (!listed.has(id) || owners.length < 2) return [];
117
- const named = owners.slice(0, 2).map((owner) => `${owner.title ?? owner.id} (${owner.id})`);
118
- return [`destination id ${id} is used in both ${named[0]} and ${named[1]}; listed heading and generated destination ids must be unique`];
119
- });
120
- }
121
- function reservedIdIssues(section) {
122
- return elements(section.node.children).flatMap((node) => {
123
- const id = node.attribs.id;
124
- if (!id?.startsWith("segnavia-")) return [];
125
- return [`authored id ${id} in ${section.info.title ?? section.info.id} (${section.info.id}) uses the reserved segnavia- prefix`];
126
- });
127
- }
128
- const BOOK_COLUMN_MIN = 2;
129
- const BOOK_COLUMN_MAX = 4;
130
73
  function columnsValueOk(value) {
131
74
  if (!/^[0-9]+$/.test(value)) return false;
132
75
  const count = Number(value);
@@ -140,117 +83,165 @@ function columnIssues(section) {
140
83
  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
84
  });
142
85
  }
143
- function endnotesHeading(book) {
144
- const titleValue = book.endnotes.title;
86
+ function notesHeading(book, notes) {
145
87
  return element("h1", {
146
- class: "segnavia-endnotes-title",
147
- dir: direction(book, titleValue)
148
- }, [text(titleValue)]);
88
+ class: notes.series.titleClass,
89
+ dir: resolveDirection(book.dir, book.lang, notes.series.title)
90
+ }, [text(notes.series.title)]);
149
91
  }
150
- function endnotesSection(book, paragraphs, galley, id, runningHead, opens) {
92
+ function notesSection(book, notes, build, id, runningHead, parts, opens) {
93
+ const scope = notes.scopeTitle !== void 0 && (notes.series.placement === "chapter" || notes.series.placement === "part") ? [element("p", {
94
+ class: "segnavia-notes-scope",
95
+ dir: resolveDirection(book.dir, book.lang, notes.scopeTitle)
96
+ }, [text(notes.scopeTitle)])] : [];
151
97
  return {
152
98
  info: {
153
99
  id,
154
100
  kind: "endnotes",
155
- title: book.endnotes.title,
101
+ division: notes.targetId === "book" ? "body" : notes.division,
102
+ title: notes.series.title,
156
103
  runningHead
157
104
  },
158
- node: sectionNode("endnotes", id, [endnotesHeading(book), ...paragraphs], galley, runningHead, opens ? flowAttribs(opens) : {})
105
+ node: sectionNode("endnotes", id, [
106
+ notesHeading(book, notes),
107
+ ...scope,
108
+ ...notes.paragraphs
109
+ ], build, runningHead, {
110
+ ...opens ? flowAttribs(opens) : {},
111
+ ...parts ? { "data-segnavia-parts": JSON.stringify(parts) } : {}
112
+ })
159
113
  };
160
114
  }
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
- }
168
- function withBlankPages(sections, blankBefore) {
169
- const counts = /* @__PURE__ */ new Map();
170
- for (const id of blankBefore ?? []) counts.set(id, (counts.get(id) ?? 0) + 1);
171
- return sections.flatMap((section) => [...Array.from({ length: counts.get(section.info.id) ?? 0 }, () => element("div", {
172
- class: "segnavia-blank",
173
- "aria-hidden": "true"
174
- })), section.node]);
115
+ function decorateSection(sectionEntry, build) {
116
+ const section = build.sections.at(-1);
117
+ if (!section) return;
118
+ if (sectionEntry.ancestry.length > 0) section.node.attribs["data-segnavia-parts"] = JSON.stringify(partNames(sectionEntry.ancestry));
119
+ attachPendingPartHeadings(build, sectionEntry, section.info.id);
175
120
  }
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"));
121
+ function noteContext(sectionEntry, targetId, chapter, title, build) {
122
+ const ancestry = sectionEntry.ancestry.at(-1);
123
+ for (const part of sectionEntry.ancestry) build.partTargets.set(part.id, targetId);
124
+ return {
125
+ chapter,
126
+ title,
127
+ division: sectionEntry.division,
128
+ targetId,
129
+ ...chapter === 0 && sectionEntry.division !== "front" ? { placementKey: targetId.replace(/^segnavia-/, "") } : {},
130
+ ...ancestry ? { part: {
131
+ id: ancestry.id,
132
+ title: ancestry.title
133
+ } } : {}
134
+ };
186
135
  }
187
136
  function addFrontMatter(book, build) {
188
137
  let contents;
189
138
  let frontNumber = 0;
190
- for (const entry of book.frontMatter) {
139
+ for (const sectionEntry of bookSections(book)) {
140
+ if (sectionEntry.division !== "front") continue;
141
+ const { entry } = sectionEntry;
142
+ if (isPart(entry)) {
143
+ addPart(sectionEntry, build);
144
+ continue;
145
+ }
191
146
  if (isBookContents(entry)) {
192
- contents = contentsSection(entry, build.galley);
147
+ contents = contentsSection(entry, build.build);
193
148
  build.sections.push(contents);
149
+ decorateSection(sectionEntry, build);
194
150
  continue;
195
151
  }
196
152
  if (isDesignedSection(entry)) {
197
153
  build.designedNumber += 1;
198
- appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "front", book, build.galley, build.notes, build.sections, build.headings, build.issues);
154
+ appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "front", book, build, sectionEntry);
199
155
  continue;
200
156
  }
201
157
  if (isTemplatedSection(entry)) {
202
158
  build.templatedNumber += 1;
203
- appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "front", book, build.galley, build.notes, build.templates, build.sections, build.headings, build.issues);
159
+ appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "front", book, build, sectionEntry);
204
160
  continue;
205
161
  }
206
162
  frontNumber += 1;
207
- const section = authoredSection(entry, `segnavia-front-${frontNumber}`, build.galley);
163
+ const section = authoredSection(entry, `segnavia-front-${frontNumber}`, build.build);
208
164
  build.sections.push(section);
165
+ decorateSection(sectionEntry, build);
209
166
  build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
210
- if (entry.contents && entry.title !== void 0) build.headings.push({
167
+ if ("contents" in entry && entry.contents && entry.title !== void 0) build.headings.push(...withSectionDepth([{
211
168
  id: section.info.id,
212
169
  level: 1,
213
170
  text: entry.title,
214
171
  sectionId: section.info.id
215
- });
216
- transformNotes(section.node, book, 0, build.notes, build.galley);
172
+ }], sectionEntry.depth));
173
+ transformNotes(section.node, book, noteContext(sectionEntry, section.info.id, 0, entry.title, build), build.notes, build.build === "galley");
217
174
  }
218
175
  return contents;
219
176
  }
220
- function addChapters(book, build) {
221
- let chapterNumber = 0;
222
- for (const entry of book.chapters) {
177
+ function addChapters(book, build, division = "body") {
178
+ for (const sectionEntry of bookSections(book)) {
179
+ if (sectionEntry.division !== division) continue;
180
+ const { entry } = sectionEntry;
181
+ if (isPart(entry)) {
182
+ addPart(sectionEntry, build);
183
+ continue;
184
+ }
185
+ if (isBookContents(entry)) continue;
223
186
  if (isDesignedSection(entry)) {
224
187
  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);
188
+ appendDesigned(entry, `segnavia-designed-${build.designedNumber}`, "designed", book, build, sectionEntry);
228
189
  continue;
229
190
  }
230
191
  if (isTemplatedSection(entry)) {
231
192
  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);
193
+ appendTemplated(entry, `segnavia-templated-${build.templatedNumber}`, "templated", book, build, sectionEntry);
235
194
  continue;
236
195
  }
237
- chapterNumber += 1;
238
- const section = chapterSection(entry, chapterNumber, build.galley);
196
+ if (!("html" in entry)) continue;
197
+ build.chapterNumber += 1;
198
+ const section = chapterSection(entry, build.chapterNumber, sectionEntry.division, build.build);
239
199
  build.sections.push(section);
200
+ decorateSection(sectionEntry, build);
240
201
  build.issues.push(...reservedIdIssues(section), ...columnIssues(section));
241
- build.headings.push({
202
+ if (entry.title !== void 0) build.headings.push(...withSectionDepth([{
242
203
  id: section.info.id,
243
204
  level: 1,
244
205
  text: entry.title,
245
206
  sectionId: section.info.id
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);
207
+ }], sectionEntry.depth));
208
+ build.headings.push(...withSectionDepth(chapterHeadings(section, build.chapterNumber, book.contents.depth), sectionEntry.depth));
209
+ transformNotes(section.node, book, noteContext(sectionEntry, section.info.id, build.chapterNumber, entry.title, build), build.notes, build.build === "galley");
210
+ }
211
+ }
212
+ function collectedSectionId(notes) {
213
+ const prefix = notes.series.sectionIdPrefix;
214
+ if (notes.targetId === "book") return prefix;
215
+ if (notes.series.placement === "part") return `${prefix}-${notes.placementKey.replace(/^segnavia-/, "")}`;
216
+ if (notes.series.placement === "chapter" && notes.targetId.startsWith("segnavia-chapter-")) return `${prefix}-${notes.targetId.slice(17)}`;
217
+ return `${prefix}-${notes.targetId.replace(/^segnavia-/, "")}`;
218
+ }
219
+ function insertCollectedSections(book, build) {
220
+ const order = new Map(book.notes.map((series, index) => [series.name, index]));
221
+ const collected = [...build.notes.collected.values()].sort((left, right) => (order.get(left.series.name) ?? 0) - (order.get(right.series.name) ?? 0));
222
+ for (const notes of [...collected].reverse()) {
223
+ if (notes.targetId === "book") continue;
224
+ const targetId = notes.series.placement === "part" ? build.partTargets.get(notes.placementKey) ?? notes.targetId : notes.targetId;
225
+ notes.targetId = targetId;
226
+ const index = build.sections.findIndex(({ info }) => info.id === targetId);
227
+ if (index < 0) {
228
+ build.issues.push(`note series ${notes.series.name} could not find placement target ${targetId}; keep the owning section in the book`);
229
+ continue;
230
+ }
231
+ const owner = build.sections[index];
232
+ const parts = owner?.node.attribs["data-segnavia-parts"];
233
+ build.sections.splice(index + 1, 0, notesSection(book, notes, build.build, collectedSectionId(notes), owner?.info.runningHead, parts ? JSON.parse(parts) : void 0, "any"));
234
+ }
235
+ for (const notes of collected) {
236
+ if (notes.targetId !== "book") continue;
237
+ const index = build.sections.findIndex(({ info }) => info.division === "end");
238
+ build.sections.splice(index < 0 ? build.sections.length : index, 0, notesSection(book, notes, build.build, collectedSectionId(notes), notes.series.title));
249
239
  }
250
240
  }
251
241
  function bookDocument(book, options = {}) {
242
+ const { build: requestedBuild, issue: buildIssue } = resolveBookBuild(book, options.build);
252
243
  const build = {
253
- galley: book.mode === "paginated",
244
+ build: requestedBuild,
254
245
  notes: noteState(),
255
246
  sections: [],
256
247
  headings: [],
@@ -258,24 +249,29 @@ function bookDocument(book, options = {}) {
258
249
  templates: new Map(book.templates.map((template) => [template.name, template])),
259
250
  designedNumber: 0,
260
251
  templatedNumber: 0,
261
- heldNotesOwner: 0
252
+ chapterNumber: 0,
253
+ partTargets: /* @__PURE__ */ new Map(),
254
+ pendingPartHeadings: []
262
255
  };
263
256
  const contents = addFrontMatter(book, build);
264
257
  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));
258
+ addChapters(book, build, "end");
259
+ const { notes, sections, headings, issues } = build;
260
+ insertCollectedSections(book, build);
270
261
  if (contents) addContents(book, contents, headings, options);
271
- const visible = sections.filter((section) => options.part === void 0 || bookPrintPart(section.info.kind) === options.part);
262
+ if (requestedBuild !== "print") markBlockPositions(sections);
263
+ const visible = sections.filter((section) => options.part === void 0 || bookPrintPart(section.info.division) === options.part);
272
264
  const document = parse("");
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
- ])] : [
278
- element("style", {}, [text(PRINT_CSS)]),
265
+ document.children = requestedBuild === "galley" ? [
266
+ element("style", {}, [text(bookDocumentCss(book, requestedBuild))]),
267
+ ...options.targets ? [targetLinks(visible)] : [],
268
+ element("div", { "data-segnavia-galleys": "" }, [
269
+ ...templateHolders(book.templates),
270
+ ...visible.map((section) => section.node),
271
+ ...notes.pageParagraphs.length > 0 ? [notesGalley(notes.pageParagraphs)] : []
272
+ ])
273
+ ] : [
274
+ element("style", {}, [text(bookDocumentCss(book, requestedBuild))]),
279
275
  ...options.targets ? [targetLinks(visible)] : [],
280
276
  ...withBlankPages(visible, options.blankBefore)
281
277
  ];
@@ -285,16 +281,15 @@ function bookDocument(book, options = {}) {
285
281
  sections: sections.map((section) => section.info),
286
282
  headings,
287
283
  issues: [
288
- ...noteMarkerIssues(book, {
289
- footnotes: notes.footnotes,
290
- endnotes: notes.endnotes
291
- }),
284
+ ...buildIssue ? [buildIssue] : [],
285
+ ...noteMarkerIssues(book.notes, notes.used),
286
+ ...notes.issues,
292
287
  ...markPlacementIssues(book.parentPages),
293
- ...templateRegionNameIssues(book.templates),
288
+ ...templateRegionIssues(book.templates),
294
289
  ...issues,
295
290
  ...duplicateIdIssues(sections, headings)
296
291
  ]
297
292
  };
298
293
  }
299
294
  //#endregion
300
- export { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, bookDocument };
295
+ export { bookDocument };
@@ -0,0 +1,13 @@
1
+ import { Element } from "domhandler";
2
+ import { Book } from "@segnavia/format";
3
+ //#region src/book/book-flow.d.ts
4
+ export declare const BOOK_FLOW_COLUMN_MIN_PX = 240;
5
+ export declare const BOOK_COLUMN_MIN = 2;
6
+ export declare const BOOK_COLUMN_MAX = 4;
7
+ export interface BookPositionKey {
8
+ flow: string;
9
+ index: number;
10
+ suffix?: string;
11
+ }
12
+ export declare function parseBookPositionKey(value: string): BookPositionKey | undefined;
13
+ //#endregion
@@ -0,0 +1,96 @@
1
+ import { isTag } from "domhandler";
2
+ import { MAX_PART_DEPTH, PX_PER_MM, bookSections, isPart, sheetMm } from "@segnavia/format";
3
+ //#region src/book/book-flow.ts
4
+ const BOOK_FLOW_COLUMN_MIN_PX = 240;
5
+ const BOOK_COLUMN_MIN = 2;
6
+ const BOOK_COLUMN_MAX = 4;
7
+ const CONTENTS_INDENTS = Array.from({ length: 5 }, (_, index) => index + 2).map((level) => `:where(.segnavia-contents-entry[data-level="${level}"]){padding-inline-start:${level - 1}em}`).join("");
8
+ const DEEP_CONTENTS_INDENTS = Array.from({ length: MAX_PART_DEPTH }, (_, index) => index + 7).map((level) => `:where(.segnavia-contents-entry[data-level="${level}"]){padding-inline-start:${level - 1}em}`).join("");
9
+ const CONTENTS_PREFIX = `: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}${CONTENTS_INDENTS}`;
10
+ const CONTENTS_SUFFIX = ":where(.segnavia-targets){position:absolute;inline-size:1px;block-size:1px;overflow:hidden;clip-path:inset(50%)}";
11
+ const GALLEY_SUFFIX = ":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}";
12
+ function hasParts(book) {
13
+ return bookSections(book).some(({ entry }) => isPart(entry));
14
+ }
15
+ function contentsCss(book) {
16
+ return `${CONTENTS_PREFIX}${hasParts(book) ? DEEP_CONTENTS_INDENTS : ""}${CONTENTS_SUFFIX}`;
17
+ }
18
+ function printCss(book) {
19
+ const breaks = hasParts(book) ? ".segnavia-part,.segnavia-chapter,.segnavia-endnotes,.segnavia-blank" : ".segnavia-chapter,.segnavia-endnotes,.segnavia-blank";
20
+ return `${contentsCss(book)}@media print{:where(.segnavia-front,.segnavia-contents)~:where(.segnavia-front,.segnavia-contents),:where(${breaks}){break-before:page}:where(.segnavia-blank){break-after:page}}`;
21
+ }
22
+ function bookFlowPageGeometry(book) {
23
+ if (!book.page) return void 0;
24
+ const paper = sheetMm(book.page);
25
+ return {
26
+ paperInline: paper.width * PX_PER_MM,
27
+ paperBlock: paper.height * PX_PER_MM,
28
+ textInline: (paper.width - book.page.margin.inner - book.page.margin.outer) * PX_PER_MM,
29
+ textBlock: (paper.height - book.page.margin.top - book.page.margin.bottom) * PX_PER_MM,
30
+ marginBlockStart: book.page.margin.top * PX_PER_MM,
31
+ marginInlineStartRecto: book.page.margin.inner * PX_PER_MM,
32
+ marginInlineStartVerso: book.page.margin.outer * PX_PER_MM
33
+ };
34
+ }
35
+ function columnFallbacks(gap) {
36
+ return Array.from({ length: 3 }, (_, index) => index + 2).map((count) => {
37
+ return `@container (max-width:${count * 240 + (count - 1) * gap}px){:where([data-columns="${count}"]){column-count:1}}`;
38
+ }).join("");
39
+ }
40
+ function flowCss(book) {
41
+ const gap = (book.page?.columnGap ?? 0) * PX_PER_MM;
42
+ const columns = Array.from({ length: 3 }, (_, index) => index + 2).map((count) => `:where([data-columns="${count}"]){column-count:${count}}`).join("");
43
+ return `${contentsCss(book)}:where([data-segnavia-flow]){container-type:inline-size}:where([data-columns]){column-gap:${gap}px}${columns}:where([data-segnavia-flow-page]){inline-size:100%;container-type:inline-size;margin-inline:0}:where([data-segnavia-flow-page-size]){position:relative;inline-size:100cqi;block-size:calc(100cqi * var(--segnavia-paper-ratio));overflow:hidden}:where([data-segnavia-flow-paper]){position:absolute;inset-block-start:0;inset-inline-start:0;inline-size:var(--segnavia-paper-inline);block-size:var(--segnavia-paper-block);overflow:hidden;zoom:var(--segnavia-flow-scale,calc(100cqi / var(--segnavia-paper-inline)))}:where([data-segnavia-flow-text]){position:absolute;inset-block-start:var(--segnavia-text-block-start);inset-inline-start:var(--segnavia-text-inline-start);inline-size:var(--segnavia-text-inline);block-size:var(--segnavia-text-block);container-type:inline-size}${columnFallbacks(gap)}`;
44
+ }
45
+ function resolveBookBuild(book, requested) {
46
+ const build = requested ?? (book.mode === "paginated" ? "galley" : "print");
47
+ if (book.mode === "paginated" || build === "print") return { build };
48
+ return {
49
+ build,
50
+ issue: `a continuous book cannot use the ${build} build; request the print build, or make the book paginated with a page to clear this`
51
+ };
52
+ }
53
+ function bookDocumentCss(book, build) {
54
+ if (build === "galley") return `${contentsCss(book)}${GALLEY_SUFFIX}`;
55
+ return build === "flow" ? flowCss(book) : printCss(book);
56
+ }
57
+ function flowPageStyle(geometry, recto) {
58
+ const marginInlineStart = recto ? geometry.marginInlineStartRecto : geometry.marginInlineStartVerso;
59
+ return `--segnavia-paper-inline:${geometry.paperInline}px;--segnavia-paper-block:${geometry.paperBlock}px;--segnavia-paper-ratio:${geometry.paperBlock / geometry.paperInline};--segnavia-text-inline:${geometry.textInline}px;--segnavia-text-block:${geometry.textBlock}px;--segnavia-text-block-start:${geometry.marginBlockStart}px;--segnavia-text-inline-start:${marginInlineStart}px`;
60
+ }
61
+ function bookBlockPosition(flowId, block, suffix) {
62
+ return `${flowId}:${block}${suffix ? `:${suffix}` : ""}`;
63
+ }
64
+ function parseBookPositionKey(value) {
65
+ const match = /^(.*):(\d+)(?::([^:]+))?$/.exec(value);
66
+ if (!match?.[1] || !match[2]) return void 0;
67
+ return {
68
+ flow: match[1],
69
+ index: Number(match[2]),
70
+ ...match[3] ? { suffix: match[3] } : {}
71
+ };
72
+ }
73
+ function isFootnoteBlock(node) {
74
+ return (node.attribs.class ?? "").split(/\s+/).includes("segnavia-footnotes");
75
+ }
76
+ function isNotesScope(node) {
77
+ return (node.attribs.class ?? "").split(/\s+/).includes("segnavia-notes-scope");
78
+ }
79
+ function markBlockPositions(sections) {
80
+ for (const section of sections) {
81
+ let position = 0;
82
+ for (const child of section.node.children) {
83
+ if (!isTag(child)) continue;
84
+ if (child.name === "style" || child.name === "script") continue;
85
+ if (isNotesScope(child)) continue;
86
+ if (isFootnoteBlock(child)) {
87
+ child.attribs["data-segnavia-position"] = bookBlockPosition(section.info.id, position, "footnotes");
88
+ continue;
89
+ }
90
+ position += 1;
91
+ child.attribs["data-segnavia-position"] = bookBlockPosition(section.info.id, position);
92
+ }
93
+ }
94
+ }
95
+ //#endregion
96
+ export { BOOK_COLUMN_MAX, BOOK_COLUMN_MIN, BOOK_FLOW_COLUMN_MIN_PX, bookBlockPosition, bookDocumentCss, bookFlowPageGeometry, flowPageStyle, markBlockPositions, parseBookPositionKey, resolveBookBuild };
@@ -0,0 +1,4 @@
1
+ import "./book-flow.js";
2
+ import "./dom.js";
3
+ import { Element } from "domhandler";
4
+ import { Book, BookSectionParents } from "@segnavia/format";