@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
@@ -0,0 +1,267 @@
1
+ import { paginateColumns } from "./paginate-columns.js";
2
+ import { paginateMarks } from "./paginate-marks.js";
3
+ //#region src/book/paginate-sequence.ts
4
+ function paginateSequence(input, fillPage) {
5
+ function emptyMarks() {
6
+ return Object.create(null);
7
+ }
8
+ function columnRefusal(block) {
9
+ const ceiling = 12;
10
+ const count = block.columns;
11
+ if (count === void 0) return void 0;
12
+ if (Number.isInteger(count) && count >= 1 && count <= ceiling) return void 0;
13
+ const position = `chapter ${block.position.chapter} block ${block.position.block}`;
14
+ return {
15
+ tag: block.tag,
16
+ position: block.position,
17
+ message: `The ${block.tag} at ${position} could not establish a column count; use an integer from 1 to 12 to clear this refusal.`
18
+ };
19
+ }
20
+ function pagesRefusal(flow) {
21
+ if (flow.kind !== "fixed") return void 0;
22
+ if (Number.isSafeInteger(flow.pages) && flow.pages >= 0) return void 0;
23
+ return {
24
+ flow: flow.id,
25
+ message: `The ${flow.id} flow could not establish a page count; use a safe integer of 0 or more to clear this refusal.`
26
+ };
27
+ }
28
+ function boundsRefusal() {
29
+ for (const flow of input.flows) {
30
+ const pages = pagesRefusal(flow);
31
+ if (pages) return pages;
32
+ if (flow.kind === "fixed") continue;
33
+ for (const block of flow.blocks) {
34
+ const columns = columnRefusal(block);
35
+ if (columns) return columns;
36
+ }
37
+ }
38
+ }
39
+ function isFront(flow) {
40
+ if (flow.kind === "fixed") return flow.sequence === "front";
41
+ return flow.kind === "front" || flow.kind === "contents";
42
+ }
43
+ function unitsOf(flow) {
44
+ if (flow.kind === "fixed") return [];
45
+ const units = [];
46
+ for (const block of flow.blocks) for (const [index, line] of block.lines.entries()) {
47
+ const previous = units[units.length - 1];
48
+ const refs = (block.footnoteRefs ?? []).filter((ref) => ref.line === index).map((ref) => ref.note);
49
+ const unit = {
50
+ flowId: flow.id,
51
+ block,
52
+ index,
53
+ top: line.top,
54
+ bottom: line.bottom,
55
+ refs
56
+ };
57
+ if (previous) {
58
+ unit.prevBottom = previous.bottom;
59
+ previous.nextTop = line.top;
60
+ }
61
+ units.push(unit);
62
+ }
63
+ return units;
64
+ }
65
+ function flowParents(flow) {
66
+ return flow.parents === void 0 ? {} : { parents: flow.parents };
67
+ }
68
+ function openingOf(flow) {
69
+ if (flow.opens === "recto" || flow.opens === "verso" || flow.opens === "any") return flow.opens;
70
+ return input.settings.rectoOpenings ? "recto" : "any";
71
+ }
72
+ function needsOpeningBlank(opens, count) {
73
+ const nextIsRecto = count % 2 === 0;
74
+ if (opens === "recto") return !nextIsRecto;
75
+ if (opens === "verso") return nextIsRecto;
76
+ return false;
77
+ }
78
+ function makeBlank(number, sequence, flow) {
79
+ return {
80
+ kind: "blank",
81
+ number,
82
+ sequence,
83
+ flow: flow.id,
84
+ role: "blank",
85
+ windows: [],
86
+ notes: [],
87
+ blank: true,
88
+ sectionOpening: false,
89
+ ...flowParents(flow)
90
+ };
91
+ }
92
+ function makeContent(part, number, flow, opener, placed, fragments, marks) {
93
+ const layout = paginateColumns(placed);
94
+ return {
95
+ kind: part,
96
+ number,
97
+ sequence: part,
98
+ flow: flow.id,
99
+ role: opener ? "opener" : "body",
100
+ windows: layout.windows,
101
+ notes: fragments,
102
+ ...layout.headerRepeat ? { headerRepeat: layout.headerRepeat } : {},
103
+ blank: false,
104
+ sectionOpening: opener,
105
+ ...opener || flow.runningHead === void 0 ? {} : { runningHead: flow.runningHead },
106
+ marks,
107
+ ...flowParents(flow)
108
+ };
109
+ }
110
+ function makeFixed(part, number, flow, opener, marks) {
111
+ const templated = flow.kind === "fixed" && flow.role === "body";
112
+ return {
113
+ kind: part,
114
+ number,
115
+ sequence: part,
116
+ flow: flow.id,
117
+ role: templated ? "body" : "fixed",
118
+ windows: [],
119
+ notes: [],
120
+ blank: false,
121
+ sectionOpening: opener,
122
+ ...opener && !templated || flow.runningHead === void 0 ? {} : { runningHead: flow.runningHead },
123
+ marks,
124
+ ...flowParents(flow)
125
+ };
126
+ }
127
+ function emitMarked(part, flow, opener, placed, fragments, pages, carried) {
128
+ const marked = paginateMarks(placed, carried);
129
+ pages.push(makeContent(part, pages.length + 1, flow, opener, placed, fragments, marked.marks));
130
+ return marked.carried;
131
+ }
132
+ function appendFixed(flow, part, pages, continued, carried) {
133
+ const count = flow.kind === "fixed" ? flow.pages : 0;
134
+ let next = carried;
135
+ for (let index = 0; index < count; index += 1) {
136
+ const opener = index === 0;
137
+ const marked = paginateMarks([], next);
138
+ next = marked.carried;
139
+ pages.push(makeFixed(part, pages.length + 1, flow, opener, marked.marks));
140
+ }
141
+ return {
142
+ pending: continued,
143
+ carried: next
144
+ };
145
+ }
146
+ function paginateFlow(flow, part, pages, continued, carried) {
147
+ if (flow.kind === "fixed") return appendFixed(flow, part, pages, continued, carried);
148
+ const units = unitsOf(flow);
149
+ let index = 0;
150
+ let pending = continued;
151
+ let opener = true;
152
+ let nextCarried = carried;
153
+ while (index < units.length || pending.length > 0) {
154
+ const page = fillPage(units, index, pending);
155
+ if (page.refusal) return {
156
+ refusal: page.refusal,
157
+ pending,
158
+ carried: nextCarried
159
+ };
160
+ if (page.done || page.placed.length === 0 && page.fragments.length === 0) return {
161
+ pending,
162
+ carried: nextCarried
163
+ };
164
+ nextCarried = emitMarked(part, flow, opener && page.placed.length > 0, page.placed, page.fragments, pages, nextCarried);
165
+ opener = opener && page.placed.length === 0;
166
+ index = page.index;
167
+ pending = page.pending;
168
+ }
169
+ return {
170
+ pending,
171
+ carried: nextCarried
172
+ };
173
+ }
174
+ function flushNotes(continued, flow, part, pages, carried) {
175
+ let pending = continued;
176
+ let nextCarried = carried;
177
+ while (pending.length > 0) {
178
+ const page = fillPage([], 0, pending);
179
+ if (page.refusal) return {
180
+ refusal: page.refusal,
181
+ carried: nextCarried
182
+ };
183
+ if (page.done || page.placed.length === 0 && page.fragments.length === 0) return { carried: nextCarried };
184
+ nextCarried = emitMarked(part, flow, false, page.placed, page.fragments, pages, nextCarried);
185
+ pending = page.pending;
186
+ }
187
+ return { carried: nextCarried };
188
+ }
189
+ function flushBeforeFixed(flow, previous, continued, carried, part, pages) {
190
+ if (flow.kind !== "fixed" || continued.length === 0 || !previous) return {
191
+ continued,
192
+ carried
193
+ };
194
+ const flushed = flushNotes(continued, previous, part, pages, carried);
195
+ if ("refusal" in flushed && flushed.refusal) return { refusal: flushed.refusal };
196
+ return {
197
+ continued: [],
198
+ carried: flushed.carried
199
+ };
200
+ }
201
+ function emitFlow(flow, previous, continued, carried, part, pages, physicalOffset) {
202
+ const held = flushBeforeFixed(flow, previous, continued, carried, part, pages);
203
+ if ("refusal" in held && held.refusal) return { refusal: held.refusal };
204
+ if (needsOpeningBlank(openingOf(flow), physicalOffset + pages.length)) pages.push(makeBlank(pages.length + 1, part, flow));
205
+ const result = paginateFlow(flow, part, pages, held.continued, held.carried);
206
+ if ("refusal" in result && result.refusal) return { refusal: result.refusal };
207
+ return {
208
+ continued: result.pending,
209
+ carried: result.carried
210
+ };
211
+ }
212
+ function paginatePart(flows, part, physicalOffset = 0) {
213
+ const pages = [];
214
+ let continued = [];
215
+ let carried = emptyMarks();
216
+ let previous;
217
+ for (const flow of flows) {
218
+ const result = emitFlow(flow, previous, continued, carried, part, pages, physicalOffset);
219
+ if ("refusal" in result && result.refusal) return {
220
+ pages,
221
+ refusals: [result.refusal]
222
+ };
223
+ continued = result.continued;
224
+ carried = result.carried;
225
+ previous = flow;
226
+ }
227
+ const last = flows[flows.length - 1];
228
+ if (last) {
229
+ const flushed = flushNotes(continued, last, part, pages, carried);
230
+ if ("refusal" in flushed && flushed.refusal) return {
231
+ pages,
232
+ refusals: [flushed.refusal]
233
+ };
234
+ }
235
+ return {
236
+ pages,
237
+ refusals: []
238
+ };
239
+ }
240
+ const bounded = boundsRefusal();
241
+ if (bounded) return {
242
+ pages: [],
243
+ refusals: [bounded]
244
+ };
245
+ const frontFlows = input.flows.filter(isFront);
246
+ const bodyFlows = input.flows.filter((flow) => !isFront(flow));
247
+ const front = paginatePart(frontFlows, "front");
248
+ const firstBody = bodyFlows[0];
249
+ const lastFront = frontFlows[frontFlows.length - 1];
250
+ const padFront = input.settings.rectoOpenings ? front.pages.length % 2 === 1 : firstBody !== void 0 && needsOpeningBlank(openingOf(firstBody), front.pages.length);
251
+ if (front.pages.length > 0 && lastFront && padFront) front.pages.push(makeBlank(front.pages.length + 1, "front", lastFront));
252
+ const body = paginatePart(bodyFlows, "body", front.pages.length);
253
+ if (body.refusals.length > 0) return {
254
+ pages: [...front.pages, ...body.pages],
255
+ refusals: body.refusals
256
+ };
257
+ if (front.refusals.length > 0) return {
258
+ pages: [...front.pages, ...body.pages],
259
+ refusals: front.refusals
260
+ };
261
+ return {
262
+ pages: [...front.pages, ...body.pages],
263
+ refusals: []
264
+ };
265
+ }
266
+ //#endregion
267
+ export { paginateSequence };
@@ -0,0 +1,13 @@
1
+ import { PaginateBookPage } from "./paginate-types.js";
2
+ //#region src/book/paginate-target.d.ts
3
+ export type PaginateTargetPage = Pick<PaginateBookPage, 'blank' | 'flow' | 'role' | 'sectionOpening' | 'windows'>;
4
+ export interface PaginateTarget {
5
+ flow: string;
6
+ heldPage?: number;
7
+ top?: number;
8
+ }
9
+ export declare function heldPageNumber(pages: PaginateTargetPage[], index: number): number;
10
+ export declare function windowPageIndex(pages: PaginateTargetPage[], flow: string, top: number): number | undefined;
11
+ export declare function openerPageIndex(pages: PaginateTargetPage[], flow: string): number | undefined;
12
+ export declare function pageIndexForTarget(pages: PaginateTargetPage[], target: PaginateTarget): number | undefined;
13
+ //#endregion
@@ -0,0 +1,36 @@
1
+ //#region src/book/paginate-target.ts
2
+ function heldPageNumber(pages, index) {
3
+ const page = pages[index];
4
+ if (!page) return 0;
5
+ let seen = 0;
6
+ for (let i = 0; i <= index; i++) {
7
+ const held = pages[i];
8
+ if (!held || held.flow !== page.flow || held.blank) continue;
9
+ if (held.role !== "fixed" && !(held.role === "body" && held.windows.length === 0)) continue;
10
+ seen += 1;
11
+ }
12
+ return seen;
13
+ }
14
+ function windowPageIndex(pages, flow, top) {
15
+ for (const [index, page] of pages.entries()) {
16
+ if (page.blank) continue;
17
+ for (const win of page.windows) if (win.flow === flow && top >= win.start && top < win.end) return index;
18
+ }
19
+ }
20
+ function openerPageIndex(pages, flow) {
21
+ for (const [index, page] of pages.entries()) if (!page.blank && page.flow === flow && page.sectionOpening) return index;
22
+ }
23
+ function pageIndexForTarget(pages, target) {
24
+ const held = target.heldPage;
25
+ if (held !== void 0 && Number.isSafeInteger(held) && held > 0) {
26
+ for (let index = 0; index < pages.length; index++) if (pages[index]?.flow === target.flow && heldPageNumber(pages, index) === held) return index;
27
+ return;
28
+ }
29
+ if (target.top !== void 0) {
30
+ const windowed = windowPageIndex(pages, target.flow, target.top);
31
+ if (windowed !== void 0) return windowed;
32
+ }
33
+ return openerPageIndex(pages, target.flow);
34
+ }
35
+ //#endregion
36
+ export { heldPageNumber, openerPageIndex, pageIndexForTarget, windowPageIndex };
@@ -0,0 +1,170 @@
1
+ import { BookOpens, BookSectionParents } from "@segnavia/format";
2
+ //#region src/book/paginate-types.d.ts
3
+ export type PaginateSequence = 'front' | 'body';
4
+ export type PaginatePageRole = 'opener' | 'body' | 'blank' | 'fixed';
5
+ export interface PaginateLine {
6
+ top: number;
7
+ bottom: number;
8
+ }
9
+ export interface PaginateNoteRef {
10
+ note: string;
11
+ line: number;
12
+ }
13
+ export interface PaginateMark {
14
+ name: string;
15
+ value: string;
16
+ line: number;
17
+ }
18
+ export interface PaginateBlock {
19
+ kind: 'heading' | 'figure' | 'table' | 'text';
20
+ tag: string;
21
+ position: {
22
+ chapter: number;
23
+ block: number;
24
+ };
25
+ lines: PaginateLine[];
26
+ columns?: number;
27
+ orphans?: number;
28
+ widows?: number;
29
+ keepWithNext?: boolean;
30
+ keepTogether?: boolean;
31
+ headerHeight?: number;
32
+ footnoteRefs?: PaginateNoteRef[];
33
+ marks?: PaginateMark[];
34
+ }
35
+ export type PaginateFlowParents = BookSectionParents;
36
+ export type PaginateOpens = BookOpens;
37
+ interface PaginateFlowBase {
38
+ id: string;
39
+ runningHead?: string;
40
+ opens?: PaginateOpens;
41
+ parents?: PaginateFlowParents;
42
+ }
43
+ export type PaginateFlow = (PaginateFlowBase & {
44
+ kind: 'front' | 'contents' | 'chapter' | 'endnotes';
45
+ blocks: PaginateBlock[];
46
+ }) | (PaginateFlowBase & {
47
+ kind: 'fixed';
48
+ pages: number;
49
+ sequence?: PaginateSequence;
50
+ role?: 'fixed' | 'body';
51
+ });
52
+ export interface PaginateNote {
53
+ id: string;
54
+ lines: PaginateLine[];
55
+ }
56
+ export interface PaginateBookInput {
57
+ flows: PaginateFlow[];
58
+ notes: PaginateNote[];
59
+ geometry: {
60
+ textHeight: number;
61
+ notesGap: number;
62
+ };
63
+ settings: {
64
+ orphans?: number;
65
+ widows?: number;
66
+ rectoOpenings: boolean;
67
+ direction: 'ltr' | 'rtl';
68
+ endnotePlacement: 'chapter' | 'book';
69
+ };
70
+ }
71
+ export interface PaginateWindow {
72
+ flow: string;
73
+ start: number;
74
+ end: number;
75
+ top: number;
76
+ column: number;
77
+ columns: number;
78
+ }
79
+ export interface PaginatePageNote {
80
+ id: string;
81
+ whole: boolean;
82
+ start: number;
83
+ end: number;
84
+ }
85
+ export type PaginatePageMarks = Record<string, {
86
+ first: string;
87
+ last: string;
88
+ }>;
89
+ export interface PaginateBookPage {
90
+ kind: 'front' | 'body' | 'blank';
91
+ number: number;
92
+ sequence: PaginateSequence;
93
+ flow: string;
94
+ role: PaginatePageRole;
95
+ windows: PaginateWindow[];
96
+ notes: PaginatePageNote[];
97
+ headerRepeat?: number;
98
+ blank: boolean;
99
+ sectionOpening: boolean;
100
+ runningHead?: string;
101
+ marks?: PaginatePageMarks;
102
+ parents?: PaginateFlowParents;
103
+ }
104
+ export type PaginateBookRefusal = {
105
+ tag: string;
106
+ position: {
107
+ chapter: number;
108
+ block: number;
109
+ };
110
+ message: string;
111
+ } | {
112
+ flow: string;
113
+ message: string;
114
+ } | {
115
+ note: string;
116
+ message: string;
117
+ };
118
+ export interface PaginateBookResult {
119
+ pages: PaginateBookPage[];
120
+ refusals: PaginateBookRefusal[];
121
+ }
122
+ export interface PaginateUnit {
123
+ flowId: string;
124
+ block: PaginateBlock;
125
+ index: number;
126
+ top: number;
127
+ bottom: number;
128
+ refs: string[];
129
+ prevBottom?: number;
130
+ nextTop?: number;
131
+ }
132
+ export interface PaginatePageFill {
133
+ placed: PaginateUnit[];
134
+ fragments: PaginatePageNote[];
135
+ index: number;
136
+ pending: PaginatePageNote[];
137
+ refusal?: PaginateBookRefusal;
138
+ done?: boolean;
139
+ }
140
+ export type PaginateNotesUsed = (placed: PaginateUnit[], fragments: PaginatePageNote[]) => number;
141
+ interface PaginateNotesBase {
142
+ notes: PaginateNote[];
143
+ geometry: {
144
+ textHeight: number;
145
+ notesGap: number;
146
+ };
147
+ }
148
+ export type PaginateNotesOp = (PaginateNotesBase & {
149
+ op: 'space';
150
+ fragments: PaginatePageNote[];
151
+ }) | (PaginateNotesBase & {
152
+ op: 'continue';
153
+ pending: PaginatePageNote[];
154
+ used: PaginateNotesUsed;
155
+ }) | (PaginateNotesBase & {
156
+ op: 'allocate';
157
+ placed: PaginateUnit[];
158
+ fragments: PaginatePageNote[];
159
+ unit: PaginateUnit;
160
+ used: PaginateNotesUsed;
161
+ }) | (PaginateNotesBase & {
162
+ op: 'recompute';
163
+ placed: PaginateUnit[];
164
+ seed: PaginatePageNote[];
165
+ used: PaginateNotesUsed;
166
+ }) | (PaginateNotesBase & {
167
+ op: 'leftover';
168
+ fragments: PaginatePageNote[];
169
+ });
170
+ //#endregion
@@ -8,7 +8,7 @@ export interface BookDocumentOptions {
8
8
  }
9
9
  export interface BookSectionInfo {
10
10
  id: string;
11
- kind: 'front' | 'contents' | 'chapter' | 'endnotes';
11
+ kind: 'front' | 'contents' | 'chapter' | 'endnotes' | 'designed' | 'templated';
12
12
  title?: string;
13
13
  runningHead?: string;
14
14
  }
@@ -0,0 +1,5 @@
1
+ import { CssSanitizeResult } from "./sanitize-css.js";
2
+ //#region src/book-css.d.ts
3
+ export declare function sanitizeBookCss(css: string | undefined): CssSanitizeResult;
4
+ export declare function bookStylesheet(fontCss: string | undefined, bookCss: string | undefined, hostCss: string | undefined): string;
5
+ //#endregion
@@ -0,0 +1,14 @@
1
+ import { sanitizeCss } from "./sanitize-css.js";
2
+ //#region src/book-css.ts
3
+ function sanitizeBookCss(css) {
4
+ const sanitized = sanitizeCss(css ?? "", false, false);
5
+ return {
6
+ css: sanitized.css.replaceAll("<", "\\3c "),
7
+ removed: sanitized.removed
8
+ };
9
+ }
10
+ function bookStylesheet(fontCss, bookCss, hostCss) {
11
+ return `${fontCss ?? ""}${sanitizeBookCss(bookCss).css}${hostCss ?? ""}`;
12
+ }
13
+ //#endregion
14
+ export { bookStylesheet, sanitizeBookCss };
@@ -0,0 +1,189 @@
1
+ //#region src/css-tokens.ts
2
+ function newlineEnd(source, at) {
3
+ return source[at] === "\r" && source[at + 1] === "\n" ? at + 2 : at + 1;
4
+ }
5
+ function cssEscape(source, at) {
6
+ let end = at + 1;
7
+ if (end >= source.length) return ["", end];
8
+ if (/[0-9a-f]/i.test(source[end] ?? "")) {
9
+ const start = end;
10
+ while (end < source.length && end - start < 6 && /[0-9a-f]/i.test(source[end] ?? "")) end++;
11
+ const point = Number.parseInt(source.slice(start, end), 16);
12
+ if (source[end] === "\r" && source[end + 1] === "\n") end += 2;
13
+ else if (/\s/.test(source[end] ?? "")) end++;
14
+ return [point === 0 || point > 1114111 ? "�" : String.fromCodePoint(point), end];
15
+ }
16
+ if (source[end] === "\n" || source[end] === "\r" || source[end] === "\f") return ["", newlineEnd(source, end)];
17
+ return [source[end] ?? "", end + 1];
18
+ }
19
+ function name(source, at) {
20
+ let value = "";
21
+ let end = at;
22
+ while (end < source.length) {
23
+ const character = source[end] ?? "";
24
+ if (/[\w-]/.test(character) || character.charCodeAt(0) >= 128) {
25
+ value += character;
26
+ end++;
27
+ continue;
28
+ }
29
+ if (character !== "\\") break;
30
+ const escaped = cssEscape(source, end);
31
+ value += escaped[0];
32
+ end = escaped[1];
33
+ }
34
+ return [value.toLowerCase(), end];
35
+ }
36
+ function skipTrivia(source, at) {
37
+ if (/\s/.test(source[at] ?? "")) return at + 1;
38
+ if (!source.startsWith("/*", at)) return at;
39
+ const close = source.indexOf("*/", at + 2);
40
+ return close < 0 ? source.length : close + 2;
41
+ }
42
+ function stringToken(source, start) {
43
+ const quote = source[start] ?? "";
44
+ let at = start + 1;
45
+ let value = "";
46
+ while (at < source.length && source[at] !== quote) {
47
+ if (source[at] === "\n" || source[at] === "\r" || source[at] === "\f") break;
48
+ if (source[at] === "\\") {
49
+ const escaped = cssEscape(source, at);
50
+ value += escaped[0];
51
+ at = escaped[1];
52
+ } else value += source[at++] ?? "";
53
+ }
54
+ if (source[at] === quote) at++;
55
+ return {
56
+ end: at,
57
+ kind: "string",
58
+ start,
59
+ value
60
+ };
61
+ }
62
+ function badUrlEnd(source, at) {
63
+ while (at < source.length) {
64
+ if (source[at] === ")") return at + 1;
65
+ if (source[at] === "\\") at = cssEscape(source, at)[1];
66
+ else at++;
67
+ }
68
+ return at;
69
+ }
70
+ function invalidUrlCharacter(character) {
71
+ const code = character.charCodeAt(0);
72
+ return character === "\"" || character === "'" || character === "(" || code <= 8 || code === 11 || code >= 14 && code <= 31 || code === 127;
73
+ }
74
+ function urlAfterWhitespace(source, start, at, value) {
75
+ let end = at;
76
+ while (/\s/.test(source[end] ?? "")) end++;
77
+ if (source[end] === ")") return {
78
+ end: end + 1,
79
+ kind: "url",
80
+ start,
81
+ value: value.trim().toLowerCase()
82
+ };
83
+ return {
84
+ end: badUrlEnd(source, end),
85
+ kind: "bad-url",
86
+ start,
87
+ value
88
+ };
89
+ }
90
+ function escapedUrlPart(source, start, at, value) {
91
+ const escaped = cssEscape(source, at);
92
+ if (escaped[1] === at + 1 || escaped[0] === "") return {
93
+ end: badUrlEnd(source, escaped[1]),
94
+ kind: "bad-url",
95
+ start,
96
+ value
97
+ };
98
+ return [escaped[0], escaped[1]];
99
+ }
100
+ function urlToken(source, start, content) {
101
+ let at = content;
102
+ let value = "";
103
+ while (/\s/.test(source[at] ?? "")) at++;
104
+ if (source[at] === "\"" || source[at] === "'") return {
105
+ end: content,
106
+ kind: "function",
107
+ start,
108
+ value: "url"
109
+ };
110
+ while (at < source.length) {
111
+ const character = source[at] ?? "";
112
+ if (character === ")") return {
113
+ end: at + 1,
114
+ kind: "url",
115
+ start,
116
+ value: value.trim().toLowerCase()
117
+ };
118
+ if (/\s/.test(character)) return urlAfterWhitespace(source, start, at, value);
119
+ if (invalidUrlCharacter(character)) return {
120
+ end: badUrlEnd(source, at + 1),
121
+ kind: "bad-url",
122
+ start,
123
+ value
124
+ };
125
+ if (character === "\\") {
126
+ const escaped = escapedUrlPart(source, start, at, value);
127
+ if (!Array.isArray(escaped)) return escaped;
128
+ value += escaped[0];
129
+ at = escaped[1];
130
+ } else {
131
+ value += character;
132
+ at++;
133
+ }
134
+ }
135
+ return {
136
+ end: at,
137
+ kind: "url",
138
+ start,
139
+ value: value.trim().toLowerCase()
140
+ };
141
+ }
142
+ function tokenAt(source, at) {
143
+ const character = source[at] ?? "";
144
+ if (character === "\"" || character === "'") return stringToken(source, at);
145
+ const offset = character === "@" ? 1 : 0;
146
+ const parsed = name(source, at + offset);
147
+ if (parsed[0]) {
148
+ const isFunction = offset === 0 && source[parsed[1]] === "(";
149
+ if (isFunction && parsed[0] === "url") return urlToken(source, at, parsed[1] + 1);
150
+ const kind = offset === 1 ? "at" : isFunction ? "function" : "word";
151
+ return {
152
+ end: parsed[1] + (isFunction ? 1 : 0),
153
+ kind,
154
+ start: at,
155
+ value: parsed[0]
156
+ };
157
+ }
158
+ return {
159
+ end: at + 1,
160
+ kind: {
161
+ "{": "open",
162
+ "}": "close",
163
+ ";": "semicolon",
164
+ "(": "open",
165
+ "[": "open",
166
+ ")": "close",
167
+ "]": "close"
168
+ }[character] ?? "word",
169
+ start: at,
170
+ value: character
171
+ };
172
+ }
173
+ function cssTokens(source) {
174
+ const found = [];
175
+ let at = 0;
176
+ while (at < source.length) {
177
+ const significant = skipTrivia(source, at);
178
+ if (significant !== at) {
179
+ at = significant;
180
+ continue;
181
+ }
182
+ const token = tokenAt(source, at);
183
+ found.push(token);
184
+ at = token.end;
185
+ }
186
+ return found;
187
+ }
188
+ //#endregion
189
+ export { cssEscape, cssTokens };