@adeu/core 1.6.2 → 1.6.5
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.
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +38 -38
- package/src/comments.test.ts +37 -37
- package/src/comments.ts +450 -450
- package/src/diff.test.ts +61 -61
- package/src/diff.ts +250 -250
- package/src/docx/bridge.ts +188 -188
- package/src/docx/dom.ts +53 -53
- package/src/docx/primitives.ts +64 -64
- package/src/domain.ts +10 -10
- package/src/engine.atomic.test.ts +57 -57
- package/src/engine.batch.test.ts +92 -92
- package/src/engine.safety.test.ts +41 -41
- package/src/engine.tables.test.ts +165 -165
- package/src/engine.ts +734 -734
- package/src/index.test.ts +7 -7
- package/src/index.ts +13 -13
- package/src/ingest.test.ts +43 -43
- package/src/ingest.ts +399 -399
- package/src/mapper.test.ts +65 -65
- package/src/mapper.ts +834 -834
- package/src/markup.test.ts +149 -149
- package/src/markup.ts +322 -322
- package/src/models.ts +50 -50
- package/src/outline.ts +376 -376
- package/src/pagination.ts +238 -238
- package/src/test-utils.ts +141 -141
- package/src/utils/docx.ts +477 -477
- package/tsconfig.json +21 -21
- package/tsup.config.ts +9 -9
- package/vitest.config.ts +11 -11
package/src/pagination.ts
CHANGED
|
@@ -1,239 +1,239 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stateless paginator for projected DOCX Markdown.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const PAGE_TARGET_CHARS = 19_000;
|
|
6
|
-
const APPENDIX_MARKER = '<!-- READONLY_BOUNDARY_START -->';
|
|
7
|
-
|
|
8
|
-
const _CRITIC_TOKENS: Record<string, string> = {
|
|
9
|
-
'{++': '++}',
|
|
10
|
-
'{--': '--}',
|
|
11
|
-
'{==': '==}',
|
|
12
|
-
'{>>': '<<}',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const _CHG_ID_PATTERN = /\bChg:(\d+)\b/g;
|
|
16
|
-
|
|
17
|
-
export interface PageInfo {
|
|
18
|
-
page: number;
|
|
19
|
-
total_pages: number;
|
|
20
|
-
has_next: boolean;
|
|
21
|
-
has_prev: boolean;
|
|
22
|
-
tracked_change_count: number;
|
|
23
|
-
page_content: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface PaginationResult {
|
|
27
|
-
pages: PageInfo[];
|
|
28
|
-
total_pages: number;
|
|
29
|
-
body_pages: string[];
|
|
30
|
-
body_page_offsets: number[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function split_structural_appendix(markdown: string): [string, string] {
|
|
34
|
-
if (!markdown) return ['', ''];
|
|
35
|
-
|
|
36
|
-
const idx = markdown.indexOf(APPENDIX_MARKER);
|
|
37
|
-
if (idx === -1) return [markdown, ''];
|
|
38
|
-
|
|
39
|
-
const line_start = markdown.lastIndexOf('\n', idx) + 1;
|
|
40
|
-
const body = markdown.substring(0, line_start).trimEnd();
|
|
41
|
-
const appendix = markdown.substring(line_start);
|
|
42
|
-
|
|
43
|
-
return [body, appendix];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function paginate(markdown_body: string, structural_appendix: string = ''): PaginationResult {
|
|
47
|
-
if (!markdown_body) {
|
|
48
|
-
const appendix_clean = structural_appendix ? structural_appendix.trim() : '';
|
|
49
|
-
const content = appendix_clean;
|
|
50
|
-
return {
|
|
51
|
-
pages: [{
|
|
52
|
-
page: 1,
|
|
53
|
-
total_pages: 1,
|
|
54
|
-
has_next: false,
|
|
55
|
-
has_prev: false,
|
|
56
|
-
tracked_change_count: _count_tracked_changes(content),
|
|
57
|
-
page_content: content,
|
|
58
|
-
}],
|
|
59
|
-
total_pages: 1,
|
|
60
|
-
body_pages: [''],
|
|
61
|
-
body_page_offsets: [0],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const block_records = _tokenize_into_atomic_blocks(markdown_body);
|
|
66
|
-
const [body_pages, body_page_offsets] = _assemble_pages(block_records);
|
|
67
|
-
|
|
68
|
-
let final_pages: string[];
|
|
69
|
-
if (structural_appendix && structural_appendix.trim()) {
|
|
70
|
-
const appendix = structural_appendix.trim();
|
|
71
|
-
final_pages = body_pages.map(bp => bp ? `${bp}\n\n${appendix}` : appendix);
|
|
72
|
-
} else {
|
|
73
|
-
final_pages = [...body_pages];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const total = final_pages.length;
|
|
77
|
-
const page_infos: PageInfo[] = final_pages.map((content, i) => ({
|
|
78
|
-
page: i + 1,
|
|
79
|
-
total_pages: total,
|
|
80
|
-
has_next: (i + 1 < total),
|
|
81
|
-
has_prev: (i + 1 > 1),
|
|
82
|
-
tracked_change_count: _count_tracked_changes(content),
|
|
83
|
-
page_content: content,
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
pages: page_infos,
|
|
88
|
-
total_pages: total,
|
|
89
|
-
body_pages,
|
|
90
|
-
body_page_offsets,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function _tokenize_into_atomic_blocks(markdown_body: string): [string, number][] {
|
|
95
|
-
const raw_blocks = _split_on_safe_paragraph_breaks(markdown_body);
|
|
96
|
-
return _merge_footnote_sections(raw_blocks);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function _split_on_safe_paragraph_breaks(text: string): [string, number][] {
|
|
100
|
-
const counters: Record<string, number> = { '++}': 0, '--}': 0, '==}': 0, '<<}': 0 };
|
|
101
|
-
const blocks: [string, number][] = [];
|
|
102
|
-
let block_start = 0;
|
|
103
|
-
let i = 0;
|
|
104
|
-
const n = text.length;
|
|
105
|
-
|
|
106
|
-
while (i < n) {
|
|
107
|
-
let matched_open = false;
|
|
108
|
-
for (const [open_tok, close_tok] of Object.entries(_CRITIC_TOKENS)) {
|
|
109
|
-
if (text.startsWith(open_tok, i)) {
|
|
110
|
-
counters[close_tok]++;
|
|
111
|
-
i += open_tok.length;
|
|
112
|
-
matched_open = true;
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (matched_open) continue;
|
|
117
|
-
|
|
118
|
-
let matched_close = false;
|
|
119
|
-
for (const close_tok of Object.values(_CRITIC_TOKENS)) {
|
|
120
|
-
if (text.startsWith(close_tok, i)) {
|
|
121
|
-
if (counters[close_tok] > 0) counters[close_tok]--;
|
|
122
|
-
i += close_tok.length;
|
|
123
|
-
matched_close = true;
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (matched_close) continue;
|
|
128
|
-
|
|
129
|
-
if (text[i] === '\n' && i + 1 < n && text[i + 1] === '\n') {
|
|
130
|
-
if (Object.values(counters).every(c => c === 0)) {
|
|
131
|
-
const block_text = text.substring(block_start, i);
|
|
132
|
-
if (block_text) blocks.push([block_text, block_start]);
|
|
133
|
-
|
|
134
|
-
let j = i;
|
|
135
|
-
while (j < n && text[j] === '\n') j++;
|
|
136
|
-
i = j;
|
|
137
|
-
block_start = i;
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
i++;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (block_start < n) {
|
|
146
|
-
const block_text = text.substring(block_start, n);
|
|
147
|
-
if (block_text) blocks.push([block_text, block_start]);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return blocks;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function _merge_footnote_sections(blocks: [string, number][]): [string, number][] {
|
|
154
|
-
if (!blocks.length) return blocks;
|
|
155
|
-
|
|
156
|
-
const merged: [string, number][] = [];
|
|
157
|
-
let i = 0;
|
|
158
|
-
|
|
159
|
-
while (i < blocks.length) {
|
|
160
|
-
const [block_text, block_offset] = blocks[i];
|
|
161
|
-
const stripped = block_text.trimStart();
|
|
162
|
-
const is_section_header = stripped.startsWith('## Footnotes') || stripped.startsWith('## Endnotes');
|
|
163
|
-
|
|
164
|
-
if (!is_section_header) {
|
|
165
|
-
merged.push([block_text, block_offset]);
|
|
166
|
-
i++;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
let accumulated_text = block_text;
|
|
171
|
-
let j = i + 1;
|
|
172
|
-
while (j < blocks.length) {
|
|
173
|
-
const [next_text] = blocks[j];
|
|
174
|
-
const next_stripped = next_text.trimStart();
|
|
175
|
-
if (next_stripped.startsWith('[^fn-') || next_stripped.startsWith('[^en-')) {
|
|
176
|
-
accumulated_text = `${accumulated_text}\n\n${next_text}`;
|
|
177
|
-
j++;
|
|
178
|
-
} else {
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
merged.push([accumulated_text, block_offset]);
|
|
184
|
-
i = j;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return merged;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function _assemble_pages(block_records: [string, number][]): [string[], number[]] {
|
|
191
|
-
if (!block_records.length) return [[''], [0]];
|
|
192
|
-
|
|
193
|
-
const pages: string[] = [];
|
|
194
|
-
const page_starts: number[] = [];
|
|
195
|
-
|
|
196
|
-
let current_blocks: string[] = [];
|
|
197
|
-
let current_size = 0;
|
|
198
|
-
let current_start = -1;
|
|
199
|
-
|
|
200
|
-
const flush_current = () => {
|
|
201
|
-
if (current_blocks.length > 0) {
|
|
202
|
-
pages.push(current_blocks.join('\n\n'));
|
|
203
|
-
page_starts.push(current_start);
|
|
204
|
-
}
|
|
205
|
-
current_blocks = [];
|
|
206
|
-
current_size = 0;
|
|
207
|
-
current_start = -1;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
for (const [block_text, block_offset] of block_records) {
|
|
211
|
-
const block_size = block_text.length;
|
|
212
|
-
const added_size = block_size + (current_blocks.length > 0 ? 2 : 0);
|
|
213
|
-
|
|
214
|
-
if (current_blocks.length > 0 && current_size + added_size > PAGE_TARGET_CHARS) {
|
|
215
|
-
flush_current();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (current_blocks.length === 0 && block_size > PAGE_TARGET_CHARS) {
|
|
219
|
-
pages.push(block_text);
|
|
220
|
-
page_starts.push(block_offset);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (current_blocks.length === 0) current_start = block_offset;
|
|
225
|
-
current_blocks.push(block_text);
|
|
226
|
-
current_size += current_size > 0 ? added_size : block_size;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
flush_current();
|
|
230
|
-
|
|
231
|
-
if (!pages.length) return [[''], [0]];
|
|
232
|
-
return [pages, page_starts];
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function _count_tracked_changes(page_content: string): number {
|
|
236
|
-
const matches = [...page_content.matchAll(_CHG_ID_PATTERN)];
|
|
237
|
-
const distinct = new Set(matches.map(m => m[1]));
|
|
238
|
-
return distinct.size;
|
|
1
|
+
/**
|
|
2
|
+
* Stateless paginator for projected DOCX Markdown.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const PAGE_TARGET_CHARS = 19_000;
|
|
6
|
+
const APPENDIX_MARKER = '<!-- READONLY_BOUNDARY_START -->';
|
|
7
|
+
|
|
8
|
+
const _CRITIC_TOKENS: Record<string, string> = {
|
|
9
|
+
'{++': '++}',
|
|
10
|
+
'{--': '--}',
|
|
11
|
+
'{==': '==}',
|
|
12
|
+
'{>>': '<<}',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const _CHG_ID_PATTERN = /\bChg:(\d+)\b/g;
|
|
16
|
+
|
|
17
|
+
export interface PageInfo {
|
|
18
|
+
page: number;
|
|
19
|
+
total_pages: number;
|
|
20
|
+
has_next: boolean;
|
|
21
|
+
has_prev: boolean;
|
|
22
|
+
tracked_change_count: number;
|
|
23
|
+
page_content: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PaginationResult {
|
|
27
|
+
pages: PageInfo[];
|
|
28
|
+
total_pages: number;
|
|
29
|
+
body_pages: string[];
|
|
30
|
+
body_page_offsets: number[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function split_structural_appendix(markdown: string): [string, string] {
|
|
34
|
+
if (!markdown) return ['', ''];
|
|
35
|
+
|
|
36
|
+
const idx = markdown.indexOf(APPENDIX_MARKER);
|
|
37
|
+
if (idx === -1) return [markdown, ''];
|
|
38
|
+
|
|
39
|
+
const line_start = markdown.lastIndexOf('\n', idx) + 1;
|
|
40
|
+
const body = markdown.substring(0, line_start).trimEnd();
|
|
41
|
+
const appendix = markdown.substring(line_start);
|
|
42
|
+
|
|
43
|
+
return [body, appendix];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function paginate(markdown_body: string, structural_appendix: string = ''): PaginationResult {
|
|
47
|
+
if (!markdown_body) {
|
|
48
|
+
const appendix_clean = structural_appendix ? structural_appendix.trim() : '';
|
|
49
|
+
const content = appendix_clean;
|
|
50
|
+
return {
|
|
51
|
+
pages: [{
|
|
52
|
+
page: 1,
|
|
53
|
+
total_pages: 1,
|
|
54
|
+
has_next: false,
|
|
55
|
+
has_prev: false,
|
|
56
|
+
tracked_change_count: _count_tracked_changes(content),
|
|
57
|
+
page_content: content,
|
|
58
|
+
}],
|
|
59
|
+
total_pages: 1,
|
|
60
|
+
body_pages: [''],
|
|
61
|
+
body_page_offsets: [0],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const block_records = _tokenize_into_atomic_blocks(markdown_body);
|
|
66
|
+
const [body_pages, body_page_offsets] = _assemble_pages(block_records);
|
|
67
|
+
|
|
68
|
+
let final_pages: string[];
|
|
69
|
+
if (structural_appendix && structural_appendix.trim()) {
|
|
70
|
+
const appendix = structural_appendix.trim();
|
|
71
|
+
final_pages = body_pages.map(bp => bp ? `${bp}\n\n${appendix}` : appendix);
|
|
72
|
+
} else {
|
|
73
|
+
final_pages = [...body_pages];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const total = final_pages.length;
|
|
77
|
+
const page_infos: PageInfo[] = final_pages.map((content, i) => ({
|
|
78
|
+
page: i + 1,
|
|
79
|
+
total_pages: total,
|
|
80
|
+
has_next: (i + 1 < total),
|
|
81
|
+
has_prev: (i + 1 > 1),
|
|
82
|
+
tracked_change_count: _count_tracked_changes(content),
|
|
83
|
+
page_content: content,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
pages: page_infos,
|
|
88
|
+
total_pages: total,
|
|
89
|
+
body_pages,
|
|
90
|
+
body_page_offsets,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _tokenize_into_atomic_blocks(markdown_body: string): [string, number][] {
|
|
95
|
+
const raw_blocks = _split_on_safe_paragraph_breaks(markdown_body);
|
|
96
|
+
return _merge_footnote_sections(raw_blocks);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _split_on_safe_paragraph_breaks(text: string): [string, number][] {
|
|
100
|
+
const counters: Record<string, number> = { '++}': 0, '--}': 0, '==}': 0, '<<}': 0 };
|
|
101
|
+
const blocks: [string, number][] = [];
|
|
102
|
+
let block_start = 0;
|
|
103
|
+
let i = 0;
|
|
104
|
+
const n = text.length;
|
|
105
|
+
|
|
106
|
+
while (i < n) {
|
|
107
|
+
let matched_open = false;
|
|
108
|
+
for (const [open_tok, close_tok] of Object.entries(_CRITIC_TOKENS)) {
|
|
109
|
+
if (text.startsWith(open_tok, i)) {
|
|
110
|
+
counters[close_tok]++;
|
|
111
|
+
i += open_tok.length;
|
|
112
|
+
matched_open = true;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (matched_open) continue;
|
|
117
|
+
|
|
118
|
+
let matched_close = false;
|
|
119
|
+
for (const close_tok of Object.values(_CRITIC_TOKENS)) {
|
|
120
|
+
if (text.startsWith(close_tok, i)) {
|
|
121
|
+
if (counters[close_tok] > 0) counters[close_tok]--;
|
|
122
|
+
i += close_tok.length;
|
|
123
|
+
matched_close = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (matched_close) continue;
|
|
128
|
+
|
|
129
|
+
if (text[i] === '\n' && i + 1 < n && text[i + 1] === '\n') {
|
|
130
|
+
if (Object.values(counters).every(c => c === 0)) {
|
|
131
|
+
const block_text = text.substring(block_start, i);
|
|
132
|
+
if (block_text) blocks.push([block_text, block_start]);
|
|
133
|
+
|
|
134
|
+
let j = i;
|
|
135
|
+
while (j < n && text[j] === '\n') j++;
|
|
136
|
+
i = j;
|
|
137
|
+
block_start = i;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (block_start < n) {
|
|
146
|
+
const block_text = text.substring(block_start, n);
|
|
147
|
+
if (block_text) blocks.push([block_text, block_start]);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return blocks;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function _merge_footnote_sections(blocks: [string, number][]): [string, number][] {
|
|
154
|
+
if (!blocks.length) return blocks;
|
|
155
|
+
|
|
156
|
+
const merged: [string, number][] = [];
|
|
157
|
+
let i = 0;
|
|
158
|
+
|
|
159
|
+
while (i < blocks.length) {
|
|
160
|
+
const [block_text, block_offset] = blocks[i];
|
|
161
|
+
const stripped = block_text.trimStart();
|
|
162
|
+
const is_section_header = stripped.startsWith('## Footnotes') || stripped.startsWith('## Endnotes');
|
|
163
|
+
|
|
164
|
+
if (!is_section_header) {
|
|
165
|
+
merged.push([block_text, block_offset]);
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let accumulated_text = block_text;
|
|
171
|
+
let j = i + 1;
|
|
172
|
+
while (j < blocks.length) {
|
|
173
|
+
const [next_text] = blocks[j];
|
|
174
|
+
const next_stripped = next_text.trimStart();
|
|
175
|
+
if (next_stripped.startsWith('[^fn-') || next_stripped.startsWith('[^en-')) {
|
|
176
|
+
accumulated_text = `${accumulated_text}\n\n${next_text}`;
|
|
177
|
+
j++;
|
|
178
|
+
} else {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
merged.push([accumulated_text, block_offset]);
|
|
184
|
+
i = j;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return merged;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function _assemble_pages(block_records: [string, number][]): [string[], number[]] {
|
|
191
|
+
if (!block_records.length) return [[''], [0]];
|
|
192
|
+
|
|
193
|
+
const pages: string[] = [];
|
|
194
|
+
const page_starts: number[] = [];
|
|
195
|
+
|
|
196
|
+
let current_blocks: string[] = [];
|
|
197
|
+
let current_size = 0;
|
|
198
|
+
let current_start = -1;
|
|
199
|
+
|
|
200
|
+
const flush_current = () => {
|
|
201
|
+
if (current_blocks.length > 0) {
|
|
202
|
+
pages.push(current_blocks.join('\n\n'));
|
|
203
|
+
page_starts.push(current_start);
|
|
204
|
+
}
|
|
205
|
+
current_blocks = [];
|
|
206
|
+
current_size = 0;
|
|
207
|
+
current_start = -1;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
for (const [block_text, block_offset] of block_records) {
|
|
211
|
+
const block_size = block_text.length;
|
|
212
|
+
const added_size = block_size + (current_blocks.length > 0 ? 2 : 0);
|
|
213
|
+
|
|
214
|
+
if (current_blocks.length > 0 && current_size + added_size > PAGE_TARGET_CHARS) {
|
|
215
|
+
flush_current();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (current_blocks.length === 0 && block_size > PAGE_TARGET_CHARS) {
|
|
219
|
+
pages.push(block_text);
|
|
220
|
+
page_starts.push(block_offset);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (current_blocks.length === 0) current_start = block_offset;
|
|
225
|
+
current_blocks.push(block_text);
|
|
226
|
+
current_size += current_size > 0 ? added_size : block_size;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
flush_current();
|
|
230
|
+
|
|
231
|
+
if (!pages.length) return [[''], [0]];
|
|
232
|
+
return [pages, page_starts];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function _count_tracked_changes(page_content: string): number {
|
|
236
|
+
const matches = [...page_content.matchAll(_CHG_ID_PATTERN)];
|
|
237
|
+
const distinct = new Set(matches.map(m => m[1]));
|
|
238
|
+
return distinct.size;
|
|
239
239
|
}
|