@adeu/core 1.6.2
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 +3627 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +247 -0
- package/dist/index.d.ts +247 -0
- package/dist/index.js +3579 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/src/comments.test.ts +38 -0
- package/src/comments.ts +451 -0
- package/src/diff.test.ts +62 -0
- package/src/diff.ts +251 -0
- package/src/docx/bridge.ts +189 -0
- package/src/docx/dom.ts +54 -0
- package/src/docx/primitives.ts +65 -0
- package/src/domain.ts +11 -0
- package/src/engine.atomic.test.ts +58 -0
- package/src/engine.batch.test.ts +93 -0
- package/src/engine.safety.test.ts +42 -0
- package/src/engine.tables.test.ts +166 -0
- package/src/engine.ts +735 -0
- package/src/index.test.ts +8 -0
- package/src/index.ts +14 -0
- package/src/ingest.test.ts +44 -0
- package/src/ingest.ts +400 -0
- package/src/mapper.test.ts +66 -0
- package/src/mapper.ts +835 -0
- package/src/markup.test.ts +150 -0
- package/src/markup.ts +323 -0
- package/src/models.ts +51 -0
- package/src/outline.ts +377 -0
- package/src/pagination.ts +239 -0
- package/src/test-utils.ts +142 -0
- package/src/utils/docx.ts +478 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
_replace_smart_quotes,
|
|
4
|
+
_make_fuzzy_regex,
|
|
5
|
+
_find_match_in_text,
|
|
6
|
+
_build_critic_markup,
|
|
7
|
+
apply_edits_to_markdown
|
|
8
|
+
} from './markup.js';
|
|
9
|
+
import { ModifyText } from './models.js';
|
|
10
|
+
|
|
11
|
+
describe('Markup Helpers', () => {
|
|
12
|
+
it.each([
|
|
13
|
+
['"Hello" and \'World\'', '"Hello" and \'World\''],
|
|
14
|
+
['Smart “quotes” and ‘apostrophes’', '"Hello" and \'World\''.replace('Hello', 'quotes').replace('World', 'apostrophes')], // Workaround for JS literal parsing in table
|
|
15
|
+
])('replace_smart_quotes(%s)', (input, expected) => {
|
|
16
|
+
// Quick override for the manual table definition above
|
|
17
|
+
if (input.includes('Smart')) expected = 'Smart "quotes" and \'apostrophes\'';
|
|
18
|
+
expect(_replace_smart_quotes(input)).toBe(expected);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
['hello world', ['hello world', 'hello world', 'hello world']],
|
|
23
|
+
['[___]', ['[___]', '[_____]', '[__________]']],
|
|
24
|
+
])('make_fuzzy_regex(%s)', (inputStr, matches) => {
|
|
25
|
+
const pattern = new RegExp(_make_fuzzy_regex(inputStr));
|
|
26
|
+
for (const m of matches) {
|
|
27
|
+
expect(m).toMatch(pattern);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each([
|
|
32
|
+
['The quick brown fox', 'quick', 4, 9],
|
|
33
|
+
['"Hello" said the fox', '"Hello"', 0, 7],
|
|
34
|
+
['hello world', 'hello world', 0, 13],
|
|
35
|
+
['The quick brown fox', 'elephant', -1, -1],
|
|
36
|
+
['Some text', '', -1, -1],
|
|
37
|
+
])('find_match_in_text: %s targets %s', (text, target, expectedStart, expectedEnd) => {
|
|
38
|
+
const [start, end] = _find_match_in_text(text, target);
|
|
39
|
+
expect(start).toBe(expectedStart);
|
|
40
|
+
expect(end).toBe(expectedEnd);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('build_critic_markup', () => {
|
|
45
|
+
it.each([
|
|
46
|
+
{ t: 'old', n: '', expected: '{--old--}' },
|
|
47
|
+
{ t: '', n: 'new', expected: '{++new++}' },
|
|
48
|
+
{ t: 'old', n: 'new', expected: '{--old--}{++new++}' },
|
|
49
|
+
{ t: 'old', n: 'new', c: 'Changed this', expected: '{--old--}{++new++}{>>Changed this<<}' },
|
|
50
|
+
{ t: 'old', n: 'new', idx: 3, incIdx: true, expected: '{--old--}{++new++}{>>[Edit:3]<<}' },
|
|
51
|
+
{ t: 'old', n: 'new', c: 'Reason', idx: 5, incIdx: true, expected: '{--old--}{++new++}{>>Reason [Edit:5]<<}' },
|
|
52
|
+
{ t: 'target', n: 'ignored', highlight: true, expected: '{==target==}' },
|
|
53
|
+
{ t: 'target', n: 'ignored', c: 'Note', idx: 2, incIdx: true, highlight: true, expected: '{==target==}{>>Note [Edit:2]<<}' },
|
|
54
|
+
|
|
55
|
+
// Formatting
|
|
56
|
+
{ t: '**Important**', n: '**Critical**', expected: '**{--Important--}{++Critical++}**' },
|
|
57
|
+
{ t: '_emphasis_', n: '_strong emphasis_', expected: '_{--emphasis--}{++strong emphasis++}_' },
|
|
58
|
+
{ t: '**_nested_**', n: '**_deeply nested_**', expected: '**{--_nested_--}{++_deeply nested_++}**' },
|
|
59
|
+
{ t: '**unbalanced', n: '**still unbalanced', expected: '{--**unbalanced--}{++**still unbalanced++}' },
|
|
60
|
+
{ t: '__0__', n: '__1__', expected: '{--__0__--}{++__1__++}' },
|
|
61
|
+
|
|
62
|
+
// Edge Cases
|
|
63
|
+
{ t: '', n: '', expected: '' },
|
|
64
|
+
{ t: ' ', n: 'text', expected: '{-- --}{++text++}' },
|
|
65
|
+
{ t: 'Line1\nLine2', n: 'SingleLine', expected: '{--Line1\nLine2--}{++SingleLine++}' },
|
|
66
|
+
{ t: 'C++', n: 'Python', expected: '{--C++--}{++Python++}' },
|
|
67
|
+
{ t: 'old', n: 'new', c: ' ', expected: '{--old--}{++new++}{>> <<}' },
|
|
68
|
+
{ t: 'old', n: 'new', c: '', expected: '{--old--}{++new++}' },
|
|
69
|
+
{ t: '', n: 'ignored', highlight: true, expected: '' }
|
|
70
|
+
])('builds correct markup for $t -> $n', ({ t, n, c, idx = 0, incIdx = false, highlight = false, expected }) => {
|
|
71
|
+
const result = _build_critic_markup(t, n, c, idx, incIdx, highlight);
|
|
72
|
+
if (highlight && !t) {
|
|
73
|
+
expect(['', '{====}']).toContain(result);
|
|
74
|
+
} else {
|
|
75
|
+
expect(result).toBe(expected);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('apply_edits_to_markdown', () => {
|
|
81
|
+
it.each([
|
|
82
|
+
['Notice of Termination', 'Notice of Termination', 'Notice of Immediate Termination', 'Notice of {++Immediate ++}Termination'],
|
|
83
|
+
['Hello World', 'Hello World', 'Hello Universe', 'Hello {--World--}{++Universe++}'],
|
|
84
|
+
['Old Item', 'Old Item', 'New Item', '{--Old--}{++New++} Item'],
|
|
85
|
+
['Original text', 'none', 'none', 'Original text'],
|
|
86
|
+
['Remove this word please.', 'this ', '', 'Remove {--this --}word please.'],
|
|
87
|
+
['', 'x', 'y', ''],
|
|
88
|
+
['Price is $100.00 (USD).', '$100.00', '$200.00', '{--$100.00--}{++$200.00++}'],
|
|
89
|
+
['Use {curly} and [square] brackets.', '{curly}', '{braces}', '{--{curly}--}{++{braces}++}']
|
|
90
|
+
])('basic edge cases: %s', (text, target, newText, expected) => {
|
|
91
|
+
const result = target === 'none' ? apply_edits_to_markdown(text, []) : apply_edits_to_markdown(text, [{ type: 'modify', target_text: target, new_text: newText }]);
|
|
92
|
+
expect(result).toContain(expected);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles modification with comment', () => {
|
|
96
|
+
const text = 'The quick brown fox.';
|
|
97
|
+
const edits: ModifyText[] = [{ type: 'modify', target_text: 'quick', new_text: 'slow', comment: 'Speed change' }];
|
|
98
|
+
expect(apply_edits_to_markdown(text, edits)).toContain('{--quick--}{++slow++}{>>Speed change<<}');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('handles highlight_only mode', () => {
|
|
102
|
+
const text = 'Highlight this section please.';
|
|
103
|
+
const edits: ModifyText[] = [{ type: 'modify', target_text: 'this section', new_text: 'ignored' }];
|
|
104
|
+
const result = apply_edits_to_markdown(text, edits, false, true);
|
|
105
|
+
expect(result).toContain('{==this section==}');
|
|
106
|
+
expect(result).not.toContain('{--');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('preserves order of multiple edits', () => {
|
|
110
|
+
const text = 'A B C';
|
|
111
|
+
const edits: ModifyText[] = [
|
|
112
|
+
{ type: 'modify', target_text: 'A', new_text: 'X' },
|
|
113
|
+
{ type: 'modify', target_text: 'B', new_text: 'Y' },
|
|
114
|
+
{ type: 'modify', target_text: 'C', new_text: 'Z' }
|
|
115
|
+
];
|
|
116
|
+
const result = apply_edits_to_markdown(text, edits, true);
|
|
117
|
+
expect(result).toContain('[Edit:0]');
|
|
118
|
+
expect(result.indexOf('{++X++}')).toBeLessThan(result.indexOf('{++Y++}'));
|
|
119
|
+
expect(result.indexOf('{++Y++}')).toBeLessThan(result.indexOf('{++Z++}'));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('skips overlapping edits (first wins)', () => {
|
|
123
|
+
const text = 'The quick brown fox';
|
|
124
|
+
const edits: ModifyText[] = [
|
|
125
|
+
{ type: 'modify', target_text: 'quick brown', new_text: 'slow red' },
|
|
126
|
+
{ type: 'modify', target_text: 'brown fox', new_text: 'green dog' }
|
|
127
|
+
];
|
|
128
|
+
const result = apply_edits_to_markdown(text, edits);
|
|
129
|
+
expect(result).toContain('{--quick brown--}{++slow red++}');
|
|
130
|
+
expect(result).not.toContain('green dog');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it.each([
|
|
134
|
+
['hello world', 'hello world', '{--hello world--}'],
|
|
135
|
+
['Sign here: [__________]', '[___]', '{--[__________]--}'],
|
|
136
|
+
['"Hello" said the fox.', '"Hello"', '{--"Hello"--}']
|
|
137
|
+
])('fuzzy and smart quotes: %s', (text, target, expectedSubstring) => {
|
|
138
|
+
const result = apply_edits_to_markdown(text, [{ type: 'modify', target_text: target, new_text: 'replacement' }]);
|
|
139
|
+
expect(result).toContain(expectedSubstring);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it.each([
|
|
143
|
+
['The **quick brown fox** jumped.', 'quick brown fox', 'slow red dog', 'The **{--quick brown fox--}{++slow red dog++}** jumped.'],
|
|
144
|
+
['This is _emphasized_ text.', 'emphasized', 'highlighted', '_{--emphasized--}{++highlighted++}_'],
|
|
145
|
+
['Variable __init__ is special.', '__init__', '__setup__', '__{--init--}{++setup++}__'],
|
|
146
|
+
])('formatting noise and preservation: %s', (text, target, newText, expectedSubstring) => {
|
|
147
|
+
const result = apply_edits_to_markdown(text, [{ type: 'modify', target_text: target, new_text: newText }]);
|
|
148
|
+
expect(result).toContain(expectedSubstring);
|
|
149
|
+
});
|
|
150
|
+
});
|
package/src/markup.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { trim_common_context } from './diff.js';
|
|
2
|
+
import { ModifyText } from './models.js';
|
|
3
|
+
|
|
4
|
+
function _should_strip_markers(text: string, marker: string): boolean {
|
|
5
|
+
if (!text.startsWith(marker) || !text.endsWith(marker)) return false;
|
|
6
|
+
if (text.length < marker.length * 2) return false;
|
|
7
|
+
|
|
8
|
+
const inner = text.substring(marker.length, text.length - marker.length);
|
|
9
|
+
if (!inner) return false;
|
|
10
|
+
|
|
11
|
+
if (inner.includes(marker)) return false;
|
|
12
|
+
if (!/[a-zA-Z]/.test(inner)) return false;
|
|
13
|
+
|
|
14
|
+
if (marker === '__' && /^\w+$/.test(inner)) return false;
|
|
15
|
+
if (marker === '_') {
|
|
16
|
+
if (inner.includes('_')) return false;
|
|
17
|
+
if (/^[0-9_]+$/.test(inner)) return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _strip_balanced_markers(text: string): [string, string, string] {
|
|
24
|
+
let prefix_markup = '';
|
|
25
|
+
let suffix_markup = '';
|
|
26
|
+
let clean_text = text;
|
|
27
|
+
|
|
28
|
+
const markers = ['**', '__', '_', '*'];
|
|
29
|
+
|
|
30
|
+
for (const marker of markers) {
|
|
31
|
+
if (_should_strip_markers(clean_text, marker)) {
|
|
32
|
+
prefix_markup += marker;
|
|
33
|
+
suffix_markup = marker + suffix_markup;
|
|
34
|
+
clean_text = clean_text.substring(marker.length, clean_text.length - marker.length);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [prefix_markup, clean_text, suffix_markup];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function _replace_smart_quotes(text: string): string {
|
|
43
|
+
return text.replace(/“/g, '"').replace(/”/g, '"').replace(/‘/g, "'").replace(/’/g, "'");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _find_safe_boundaries(text: string, start: number, end: number): [number, number] {
|
|
47
|
+
let new_start = start;
|
|
48
|
+
let new_end = end;
|
|
49
|
+
|
|
50
|
+
const expand_if_unbalanced = (marker: string) => {
|
|
51
|
+
const current_match = text.substring(new_start, new_end);
|
|
52
|
+
const count = (current_match.match(new RegExp(marker.replace(/\*/g, '\\*'), 'g')) || []).length;
|
|
53
|
+
|
|
54
|
+
if (count % 2 !== 0) {
|
|
55
|
+
const suffix = text.substring(new_end);
|
|
56
|
+
if (suffix.startsWith(marker)) {
|
|
57
|
+
new_end += marker.length;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const prefix = text.substring(0, new_start);
|
|
61
|
+
if (prefix.endsWith(marker)) {
|
|
62
|
+
new_start -= marker.length;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < 2; i++) {
|
|
69
|
+
expand_if_unbalanced('**');
|
|
70
|
+
expand_if_unbalanced('__');
|
|
71
|
+
expand_if_unbalanced('_');
|
|
72
|
+
expand_if_unbalanced('*');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [new_start, new_end];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _refine_match_boundaries(text: string, start: number, end: number): [number, number] {
|
|
79
|
+
const markers = ['**', '__', '*', '_'];
|
|
80
|
+
let current_text = text.substring(start, end);
|
|
81
|
+
let best_start = start;
|
|
82
|
+
let best_end = end;
|
|
83
|
+
|
|
84
|
+
const countMarker = (str: string, mk: string) => (str.match(new RegExp(mk.replace(/\*/g, '\\*'), 'g')) || []).length;
|
|
85
|
+
|
|
86
|
+
for (const marker of markers) {
|
|
87
|
+
if (current_text.startsWith(marker)) {
|
|
88
|
+
const current_score = countMarker(current_text, marker) % 2;
|
|
89
|
+
const trimmed_text = current_text.substring(marker.length);
|
|
90
|
+
const trimmed_score = countMarker(trimmed_text, marker) % 2;
|
|
91
|
+
|
|
92
|
+
if (current_score === 1 && trimmed_score === 0) {
|
|
93
|
+
best_start += marker.length;
|
|
94
|
+
current_text = trimmed_text;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const marker of markers) {
|
|
100
|
+
if (current_text.endsWith(marker)) {
|
|
101
|
+
const current_score = countMarker(current_text, marker) % 2;
|
|
102
|
+
const trimmed_text = current_text.substring(0, current_text.length - marker.length);
|
|
103
|
+
const trimmed_score = countMarker(trimmed_text, marker) % 2;
|
|
104
|
+
|
|
105
|
+
if (current_score === 1 && trimmed_score === 0) {
|
|
106
|
+
best_end -= marker.length;
|
|
107
|
+
current_text = trimmed_text;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return [best_start, best_end];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function _make_fuzzy_regex(target_text: string): string {
|
|
116
|
+
target_text = _replace_smart_quotes(target_text);
|
|
117
|
+
|
|
118
|
+
const parts: string[] = [];
|
|
119
|
+
const token_pattern = /(_+)|(\s+)|(['"])|([.,;:\/])/g;
|
|
120
|
+
|
|
121
|
+
// Note: JS does not support atomic groups (?>...).
|
|
122
|
+
// However, because we only match markdown characters * and _,
|
|
123
|
+
// we can use a character class `[*_]*` which is mathematically equivalent
|
|
124
|
+
// to `(?:\*\*|__|\*|_)*` but fundamentally immune to catastrophic backtracking!
|
|
125
|
+
const md_noise = "[*_]*";
|
|
126
|
+
const structural_noise = "(?:\\s*(?:[*+\\->]|\\d+\\.)\\s+|\\s*\\n\\s*)";
|
|
127
|
+
|
|
128
|
+
const start_list_marker = "(?:[ \\t]*(?:[*+\\->]|\\d+\\.)\\s+)?";
|
|
129
|
+
parts.push(start_list_marker);
|
|
130
|
+
parts.push(md_noise);
|
|
131
|
+
|
|
132
|
+
let last_idx = 0;
|
|
133
|
+
let match;
|
|
134
|
+
|
|
135
|
+
const escapeRegExp = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
136
|
+
|
|
137
|
+
while ((match = token_pattern.exec(target_text)) !== null) {
|
|
138
|
+
const literal = target_text.substring(last_idx, match.index);
|
|
139
|
+
if (literal) {
|
|
140
|
+
parts.push(escapeRegExp(literal));
|
|
141
|
+
parts.push(md_noise);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const g_underscore = match[1];
|
|
145
|
+
const g_space = match[2];
|
|
146
|
+
const g_quote = match[3];
|
|
147
|
+
const g_punct = match[4];
|
|
148
|
+
|
|
149
|
+
if (g_underscore) {
|
|
150
|
+
parts.push('_+');
|
|
151
|
+
} else if (g_space) {
|
|
152
|
+
if (g_space.includes('\n')) {
|
|
153
|
+
parts.push(`(?:${structural_noise}|\\s+)+`);
|
|
154
|
+
} else {
|
|
155
|
+
parts.push('\\s+');
|
|
156
|
+
}
|
|
157
|
+
} else if (g_quote) {
|
|
158
|
+
if (g_quote === "'") parts.push('[\u2018\u2019\']');
|
|
159
|
+
else parts.push('["\u201c\u201d]');
|
|
160
|
+
} else if (g_punct) {
|
|
161
|
+
parts.push(escapeRegExp(g_punct));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
parts.push(md_noise);
|
|
165
|
+
last_idx = token_pattern.lastIndex;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const remaining = target_text.substring(last_idx);
|
|
169
|
+
if (remaining) parts.push(escapeRegExp(remaining));
|
|
170
|
+
|
|
171
|
+
return parts.join('');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function _find_match_in_text(text: string, target: string): [number, number] {
|
|
175
|
+
if (!target) return [-1, -1];
|
|
176
|
+
|
|
177
|
+
let idx = text.indexOf(target);
|
|
178
|
+
if (idx !== -1) return _find_safe_boundaries(text, idx, idx + target.length);
|
|
179
|
+
|
|
180
|
+
const norm_text = _replace_smart_quotes(text);
|
|
181
|
+
const norm_target = _replace_smart_quotes(target);
|
|
182
|
+
idx = norm_text.indexOf(norm_target);
|
|
183
|
+
if (idx !== -1) return _find_safe_boundaries(text, idx, idx + norm_target.length);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const pattern = new RegExp(_make_fuzzy_regex(target));
|
|
187
|
+
const match = pattern.exec(text);
|
|
188
|
+
if (match) {
|
|
189
|
+
const raw_start = match.index;
|
|
190
|
+
const raw_end = match.index + match[0].length;
|
|
191
|
+
const [refined_start, refined_end] = _refine_match_boundaries(text, raw_start, raw_end);
|
|
192
|
+
return _find_safe_boundaries(text, refined_start, refined_end);
|
|
193
|
+
}
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// Ignore regex compilation errors from edge cases
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return [-1, -1];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function _build_critic_markup(
|
|
202
|
+
target_text: string,
|
|
203
|
+
new_text: string,
|
|
204
|
+
comment: string | null | undefined,
|
|
205
|
+
edit_index: number,
|
|
206
|
+
include_index: boolean,
|
|
207
|
+
highlight_only: boolean
|
|
208
|
+
): string {
|
|
209
|
+
const parts: string[] = [];
|
|
210
|
+
|
|
211
|
+
let [prefix_markup, clean_target, suffix_markup] = _strip_balanced_markers(target_text);
|
|
212
|
+
|
|
213
|
+
let clean_new = new_text;
|
|
214
|
+
if (prefix_markup && new_text) {
|
|
215
|
+
if (new_text.startsWith(prefix_markup) && new_text.endsWith(suffix_markup)) {
|
|
216
|
+
const inner_len = prefix_markup.length;
|
|
217
|
+
clean_new = new_text.length > inner_len * 2 ? new_text.substring(inner_len, new_text.length - inner_len) : new_text;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
parts.push(prefix_markup);
|
|
222
|
+
|
|
223
|
+
if (highlight_only) {
|
|
224
|
+
parts.push(`{==${clean_target}==}`);
|
|
225
|
+
} else {
|
|
226
|
+
const has_target = Boolean(clean_target);
|
|
227
|
+
const has_new = Boolean(clean_new);
|
|
228
|
+
|
|
229
|
+
if (has_target && !has_new) parts.push(`{--${clean_target}--}`);
|
|
230
|
+
else if (!has_target && has_new) parts.push(`{++${clean_new}++}`);
|
|
231
|
+
else if (has_target && has_new) parts.push(`{--${clean_target}--}{++${clean_new}++}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
parts.push(suffix_markup);
|
|
235
|
+
|
|
236
|
+
const meta_parts: string[] = [];
|
|
237
|
+
if (comment) meta_parts.push(comment);
|
|
238
|
+
if (include_index) meta_parts.push(`[Edit:${edit_index}]`);
|
|
239
|
+
|
|
240
|
+
if (meta_parts.length > 0) {
|
|
241
|
+
parts.push(`{>>${meta_parts.join(' ')}<<}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return parts.join('');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function apply_edits_to_markdown(
|
|
248
|
+
markdown_text: string,
|
|
249
|
+
edits: ModifyText[],
|
|
250
|
+
include_index = false,
|
|
251
|
+
highlight_only = false
|
|
252
|
+
): string {
|
|
253
|
+
if (!edits || edits.length === 0) return markdown_text;
|
|
254
|
+
|
|
255
|
+
const matched_edits: [number, number, string, ModifyText, number][] = [];
|
|
256
|
+
|
|
257
|
+
for (let idx = 0; idx < edits.length; idx++) {
|
|
258
|
+
const edit = edits[idx];
|
|
259
|
+
const target = edit.target_text || '';
|
|
260
|
+
|
|
261
|
+
if (!target) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const [start, end] = _find_match_in_text(markdown_text, target);
|
|
266
|
+
if (start === -1) continue;
|
|
267
|
+
|
|
268
|
+
const actual_matched_text = markdown_text.substring(start, end);
|
|
269
|
+
matched_edits.push([start, end, actual_matched_text, edit, idx]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const matched_edits_filtered: [number, number, string, ModifyText, number][] = [];
|
|
273
|
+
const occupied_ranges: [number, number][] = [];
|
|
274
|
+
|
|
275
|
+
matched_edits.sort((a, b) => a[4] - b[4]);
|
|
276
|
+
|
|
277
|
+
for (const [start, end, actual_text, edit, orig_idx] of matched_edits) {
|
|
278
|
+
let overlaps = false;
|
|
279
|
+
for (const [occ_start, occ_end] of occupied_ranges) {
|
|
280
|
+
if (start < occ_end && end > occ_start) {
|
|
281
|
+
overlaps = true;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!overlaps) {
|
|
287
|
+
matched_edits_filtered.push([start, end, actual_text, edit, orig_idx]);
|
|
288
|
+
occupied_ranges.push([start, end]);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
matched_edits_filtered.sort((a, b) => b[0] - a[0]);
|
|
293
|
+
|
|
294
|
+
let result = markdown_text;
|
|
295
|
+
|
|
296
|
+
for (const [start, end, actual_text, edit, orig_idx] of matched_edits_filtered) {
|
|
297
|
+
const new_txt = edit.new_text || '';
|
|
298
|
+
const [prefix_len, suffix_len] = trim_common_context(actual_text, new_txt);
|
|
299
|
+
|
|
300
|
+
const unmodified_prefix = prefix_len > 0 ? actual_text.substring(0, prefix_len) : '';
|
|
301
|
+
const unmodified_suffix = suffix_len > 0 ? actual_text.substring(actual_text.length - suffix_len) : '';
|
|
302
|
+
|
|
303
|
+
const t_end = actual_text.length - suffix_len;
|
|
304
|
+
const n_end = new_txt.length - suffix_len;
|
|
305
|
+
|
|
306
|
+
const isolated_target = actual_text.substring(prefix_len, t_end);
|
|
307
|
+
const isolated_new = new_txt.substring(prefix_len, n_end);
|
|
308
|
+
|
|
309
|
+
const markup = _build_critic_markup(
|
|
310
|
+
isolated_target,
|
|
311
|
+
isolated_new,
|
|
312
|
+
edit.comment,
|
|
313
|
+
orig_idx,
|
|
314
|
+
include_index,
|
|
315
|
+
highlight_only
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const full_replacement = unmodified_prefix + markup + unmodified_suffix;
|
|
319
|
+
result = result.substring(0, start) + full_replacement + result.substring(end);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return result;
|
|
323
|
+
}
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ModifyText {
|
|
2
|
+
type: 'modify';
|
|
3
|
+
target_text: string;
|
|
4
|
+
new_text: string;
|
|
5
|
+
comment?: string | null;
|
|
6
|
+
_match_start_index?: number | null;
|
|
7
|
+
_internal_op?: string | null;
|
|
8
|
+
_active_mapper_ref?: any | null; // Typed as DocumentMapper later
|
|
9
|
+
_original_target_text?: string;
|
|
10
|
+
_is_table_edit?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AcceptChange {
|
|
14
|
+
type: 'accept';
|
|
15
|
+
target_id: string;
|
|
16
|
+
comment?: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RejectChange {
|
|
20
|
+
type: 'reject';
|
|
21
|
+
target_id: string;
|
|
22
|
+
comment?: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ReplyComment {
|
|
26
|
+
type: 'reply';
|
|
27
|
+
target_id: string;
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface InsertTableRow {
|
|
32
|
+
type: 'insert_row';
|
|
33
|
+
target_text: string;
|
|
34
|
+
position: 'above' | 'below';
|
|
35
|
+
cells: string[];
|
|
36
|
+
_match_start_index?: number | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DeleteTableRow {
|
|
40
|
+
type: 'delete_row';
|
|
41
|
+
target_text: string;
|
|
42
|
+
_match_start_index?: number | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type DocumentChange =
|
|
46
|
+
| ModifyText
|
|
47
|
+
| AcceptChange
|
|
48
|
+
| RejectChange
|
|
49
|
+
| ReplyComment
|
|
50
|
+
| InsertTableRow
|
|
51
|
+
| DeleteTableRow;
|