docrev 0.6.13 → 0.7.6
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/CHANGELOG.md +32 -0
- package/README.md +191 -133
- package/bin/rev.js +113 -5059
- package/completions/rev.ps1 +210 -0
- package/lib/annotations.js +41 -11
- package/lib/build.js +95 -8
- package/lib/commands/build.js +708 -0
- package/lib/commands/citations.js +497 -0
- package/lib/commands/comments.js +922 -0
- package/lib/commands/context.js +165 -0
- package/lib/commands/core.js +295 -0
- package/lib/commands/doi.js +419 -0
- package/lib/commands/history.js +307 -0
- package/lib/commands/index.js +56 -0
- package/lib/commands/init.js +247 -0
- package/lib/commands/response.js +374 -0
- package/lib/commands/sections.js +862 -0
- package/lib/commands/utilities.js +2272 -0
- package/lib/config.js +19 -0
- package/lib/crossref.js +17 -2
- package/lib/doi.js +279 -43
- package/lib/errors.js +338 -0
- package/lib/format.js +53 -6
- package/lib/git.js +92 -0
- package/lib/import.js +24 -3
- package/lib/journals.js +28 -4
- package/lib/orcid.js +149 -0
- package/lib/pdf-comments.js +217 -0
- package/lib/pdf-import.js +446 -0
- package/lib/plugins.js +285 -0
- package/lib/review.js +109 -0
- package/lib/schema.js +368 -0
- package/lib/sections.js +3 -8
- package/lib/templates.js +218 -0
- package/lib/tui.js +437 -0
- package/lib/undo.js +236 -0
- package/lib/wordcomments.js +15 -20
- package/package.json +5 -3
- package/skill/REFERENCE.md +76 -18
- package/skill/SKILL.md +122 -27
- package/.rev-dictionary +0 -4
package/lib/tui.js
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI (Text User Interface) components for enhanced visual display
|
|
3
|
+
* Uses box-drawing characters and colors for a richer terminal experience
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import * as readline from 'readline';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Clear the terminal screen
|
|
11
|
+
*/
|
|
12
|
+
export function clearScreen() {
|
|
13
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Move cursor to position
|
|
18
|
+
* @param {number} row
|
|
19
|
+
* @param {number} col
|
|
20
|
+
*/
|
|
21
|
+
export function moveCursor(row, col) {
|
|
22
|
+
process.stdout.write(`\x1B[${row};${col}H`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get terminal dimensions
|
|
27
|
+
* @returns {{rows: number, cols: number}}
|
|
28
|
+
*/
|
|
29
|
+
export function getTerminalSize() {
|
|
30
|
+
return {
|
|
31
|
+
rows: process.stdout.rows || 24,
|
|
32
|
+
cols: process.stdout.columns || 80,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Draw a box with content
|
|
38
|
+
* @param {object} options
|
|
39
|
+
* @param {string} options.title
|
|
40
|
+
* @param {string[]} options.content
|
|
41
|
+
* @param {number} options.width
|
|
42
|
+
* @param {string} options.borderColor
|
|
43
|
+
* @returns {string[]}
|
|
44
|
+
*/
|
|
45
|
+
export function drawBox({ title = '', content = [], width = 60, borderColor = 'dim' }) {
|
|
46
|
+
const border = {
|
|
47
|
+
tl: '╭', tr: '╮', bl: '╰', br: '╯',
|
|
48
|
+
h: '─', v: '│',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const colorFn = chalk[borderColor] || chalk.dim;
|
|
52
|
+
const lines = [];
|
|
53
|
+
|
|
54
|
+
// Top border with title
|
|
55
|
+
if (title) {
|
|
56
|
+
const titleDisplay = ` ${title} `;
|
|
57
|
+
const remaining = width - titleDisplay.length - 2;
|
|
58
|
+
lines.push(
|
|
59
|
+
colorFn(border.tl + border.h) +
|
|
60
|
+
chalk.bold(titleDisplay) +
|
|
61
|
+
colorFn(border.h.repeat(Math.max(0, remaining)) + border.tr)
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
lines.push(colorFn(border.tl + border.h.repeat(width - 2) + border.tr));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Content lines
|
|
68
|
+
for (const line of content) {
|
|
69
|
+
const plainLen = stripAnsi(line).length;
|
|
70
|
+
const padding = Math.max(0, width - 4 - plainLen);
|
|
71
|
+
lines.push(
|
|
72
|
+
colorFn(border.v) + ' ' + line + ' '.repeat(padding) + ' ' + colorFn(border.v)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Bottom border
|
|
77
|
+
lines.push(colorFn(border.bl + border.h.repeat(width - 2) + border.br));
|
|
78
|
+
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Draw a status bar at the bottom of the screen
|
|
84
|
+
* @param {string} left - Left-aligned text
|
|
85
|
+
* @param {string} right - Right-aligned text
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function statusBar(left, right = '') {
|
|
89
|
+
const { cols } = getTerminalSize();
|
|
90
|
+
const leftLen = stripAnsi(left).length;
|
|
91
|
+
const rightLen = stripAnsi(right).length;
|
|
92
|
+
const padding = Math.max(0, cols - leftLen - rightLen);
|
|
93
|
+
|
|
94
|
+
return chalk.inverse(left + ' '.repeat(padding) + right);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Draw a progress indicator
|
|
99
|
+
* @param {number} current
|
|
100
|
+
* @param {number} total
|
|
101
|
+
* @param {number} width
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
export function progressIndicator(current, total, width = 20) {
|
|
105
|
+
const ratio = current / total;
|
|
106
|
+
const filled = Math.round(ratio * width);
|
|
107
|
+
const empty = width - filled;
|
|
108
|
+
|
|
109
|
+
const bar = chalk.cyan('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
|
|
110
|
+
return `${bar} ${current}/${total}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Format a comment for TUI display
|
|
115
|
+
* @param {object} comment
|
|
116
|
+
* @param {number} index
|
|
117
|
+
* @param {number} total
|
|
118
|
+
* @param {number} width
|
|
119
|
+
* @returns {string[]}
|
|
120
|
+
*/
|
|
121
|
+
export function formatCommentCard(comment, index, total, width = 70) {
|
|
122
|
+
const statusIcon = comment.resolved ? chalk.green('✓') : chalk.yellow('○');
|
|
123
|
+
const author = comment.author || 'Anonymous';
|
|
124
|
+
|
|
125
|
+
const content = [];
|
|
126
|
+
|
|
127
|
+
// Author and status line
|
|
128
|
+
content.push(chalk.blue(author) + ' ' + statusIcon);
|
|
129
|
+
content.push('');
|
|
130
|
+
|
|
131
|
+
// Comment text (word-wrap)
|
|
132
|
+
const wrappedText = wordWrap(comment.content, width - 6);
|
|
133
|
+
for (const line of wrappedText) {
|
|
134
|
+
content.push(line);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Context
|
|
138
|
+
if (comment.before) {
|
|
139
|
+
content.push('');
|
|
140
|
+
const context = comment.before.trim().slice(-50);
|
|
141
|
+
content.push(chalk.dim(`Context: "...${context}"`));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Line number
|
|
145
|
+
content.push('');
|
|
146
|
+
content.push(chalk.dim(`Line ${comment.line}`));
|
|
147
|
+
|
|
148
|
+
return drawBox({
|
|
149
|
+
title: `Comment ${index + 1}/${total}`,
|
|
150
|
+
content,
|
|
151
|
+
width,
|
|
152
|
+
borderColor: comment.resolved ? 'green' : 'cyan',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Draw the action menu
|
|
158
|
+
* @param {string[]} options - Array of [key, description] tuples
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
export function actionMenu(options) {
|
|
162
|
+
return options
|
|
163
|
+
.map(([key, desc]) => chalk.bold(`[${key}]`) + chalk.dim(desc))
|
|
164
|
+
.join(' ');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Word wrap text to fit within width
|
|
169
|
+
* @param {string} text
|
|
170
|
+
* @param {number} width
|
|
171
|
+
* @returns {string[]}
|
|
172
|
+
*/
|
|
173
|
+
function wordWrap(text, width) {
|
|
174
|
+
const words = text.split(/\s+/);
|
|
175
|
+
const lines = [];
|
|
176
|
+
let currentLine = '';
|
|
177
|
+
|
|
178
|
+
for (const word of words) {
|
|
179
|
+
if (currentLine.length + word.length + 1 <= width) {
|
|
180
|
+
currentLine += (currentLine ? ' ' : '') + word;
|
|
181
|
+
} else {
|
|
182
|
+
if (currentLine) lines.push(currentLine);
|
|
183
|
+
currentLine = word;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (currentLine) lines.push(currentLine);
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Strip ANSI codes for length calculation
|
|
193
|
+
* @param {string} str
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
function stripAnsi(str) {
|
|
197
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Run TUI comment review session
|
|
202
|
+
* @param {string} text
|
|
203
|
+
* @param {object} options
|
|
204
|
+
* @returns {Promise<{text: string, resolved: number, replied: number, skipped: number}>}
|
|
205
|
+
*/
|
|
206
|
+
export async function tuiCommentReview(text, options = {}) {
|
|
207
|
+
const { getComments, setCommentStatus } = await import('./annotations.js');
|
|
208
|
+
const { createDocumentSession } = await import('./undo.js');
|
|
209
|
+
const { author = 'Author', addReply, setStatus } = options;
|
|
210
|
+
|
|
211
|
+
const comments = getComments(text, { pendingOnly: true });
|
|
212
|
+
|
|
213
|
+
if (comments.length === 0) {
|
|
214
|
+
console.log(chalk.green('No pending comments found.'));
|
|
215
|
+
return { text, resolved: 0, replied: 0, skipped: 0 };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Create session with undo support
|
|
219
|
+
const session = createDocumentSession(text);
|
|
220
|
+
|
|
221
|
+
let currentIndex = 0;
|
|
222
|
+
let resolved = 0;
|
|
223
|
+
let replied = 0;
|
|
224
|
+
let skipped = 0;
|
|
225
|
+
let message = ''; // Status message to display
|
|
226
|
+
|
|
227
|
+
// Helper to render current state
|
|
228
|
+
const render = () => {
|
|
229
|
+
clearScreen();
|
|
230
|
+
|
|
231
|
+
const { cols } = getTerminalSize();
|
|
232
|
+
const cardWidth = Math.min(cols - 4, 80);
|
|
233
|
+
|
|
234
|
+
// Header
|
|
235
|
+
console.log(chalk.cyan.bold(` Reviewing ${comments.length} comment(s) as ${author}`));
|
|
236
|
+
const undoInfo = session.info();
|
|
237
|
+
const undoStatus = session.canUndo()
|
|
238
|
+
? chalk.dim(` | ${undoInfo.undoSteps} undo`)
|
|
239
|
+
: '';
|
|
240
|
+
console.log(chalk.dim(` ${progressIndicator(currentIndex + 1, comments.length)}${undoStatus}`));
|
|
241
|
+
console.log();
|
|
242
|
+
|
|
243
|
+
// Current comment card
|
|
244
|
+
const comment = comments[currentIndex];
|
|
245
|
+
const card = formatCommentCard(comment, currentIndex, comments.length, cardWidth);
|
|
246
|
+
for (const line of card) {
|
|
247
|
+
console.log(' ' + line);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log();
|
|
251
|
+
|
|
252
|
+
// Status message
|
|
253
|
+
if (message) {
|
|
254
|
+
console.log(' ' + message);
|
|
255
|
+
console.log();
|
|
256
|
+
message = '';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Action menu with undo
|
|
260
|
+
const menuItems = [
|
|
261
|
+
['r', 'eply'],
|
|
262
|
+
['m', 'ark resolved'],
|
|
263
|
+
['s', 'kip'],
|
|
264
|
+
['n', 'ext'],
|
|
265
|
+
['p', 'rev'],
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
if (session.canUndo()) {
|
|
269
|
+
menuItems.push(['u', 'ndo']);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
menuItems.push(['A', 'll resolve'], ['q', 'uit']);
|
|
273
|
+
|
|
274
|
+
console.log(' ' + actionMenu(menuItems));
|
|
275
|
+
|
|
276
|
+
console.log();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Prompt for keypress
|
|
280
|
+
const promptKey = (validKeys) => {
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
const rl = readline.createInterface({
|
|
283
|
+
input: process.stdin,
|
|
284
|
+
output: process.stdout,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (process.stdin.isTTY) {
|
|
288
|
+
process.stdin.setRawMode(true);
|
|
289
|
+
}
|
|
290
|
+
process.stdin.resume();
|
|
291
|
+
|
|
292
|
+
process.stdin.once('data', (key) => {
|
|
293
|
+
const char = key.toString();
|
|
294
|
+
|
|
295
|
+
if (process.stdin.isTTY) {
|
|
296
|
+
process.stdin.setRawMode(false);
|
|
297
|
+
}
|
|
298
|
+
rl.close();
|
|
299
|
+
|
|
300
|
+
if (char === '\u0003') {
|
|
301
|
+
// Ctrl+C
|
|
302
|
+
clearScreen();
|
|
303
|
+
process.exit(0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (validKeys.includes(char.toLowerCase()) || validKeys.includes(char)) {
|
|
307
|
+
resolve(char);
|
|
308
|
+
} else {
|
|
309
|
+
resolve(promptKey(validKeys));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Prompt for text input
|
|
316
|
+
const promptText = (prompt) => {
|
|
317
|
+
return new Promise((resolve) => {
|
|
318
|
+
const rl = readline.createInterface({
|
|
319
|
+
input: process.stdin,
|
|
320
|
+
output: process.stdout,
|
|
321
|
+
});
|
|
322
|
+
rl.question(prompt, (answer) => {
|
|
323
|
+
rl.close();
|
|
324
|
+
resolve(answer);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Main loop
|
|
330
|
+
while (currentIndex < comments.length) {
|
|
331
|
+
render();
|
|
332
|
+
|
|
333
|
+
const validKeys = session.canUndo()
|
|
334
|
+
? ['r', 'm', 's', 'n', 'p', 'u', 'A', 'q']
|
|
335
|
+
: ['r', 'm', 's', 'n', 'p', 'A', 'q'];
|
|
336
|
+
|
|
337
|
+
const choice = await promptKey(validKeys);
|
|
338
|
+
const comment = comments[currentIndex];
|
|
339
|
+
|
|
340
|
+
switch (choice) {
|
|
341
|
+
case 'q':
|
|
342
|
+
clearScreen();
|
|
343
|
+
console.log(chalk.yellow('Aborted.'));
|
|
344
|
+
return { text: session.getText(), resolved, replied, skipped: comments.length - currentIndex };
|
|
345
|
+
|
|
346
|
+
case 'u':
|
|
347
|
+
// Undo last change
|
|
348
|
+
if (session.canUndo()) {
|
|
349
|
+
const undone = session.undo();
|
|
350
|
+
if (undone) {
|
|
351
|
+
message = chalk.yellow(`Undone: ${undone.description}`);
|
|
352
|
+
// Adjust counters (approximate)
|
|
353
|
+
if (undone.description.includes('Resolved')) resolved = Math.max(0, resolved - 1);
|
|
354
|
+
if (undone.description.includes('Reply')) replied = Math.max(0, replied - 1);
|
|
355
|
+
if (currentIndex > 0) currentIndex--;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
|
|
360
|
+
case 'A':
|
|
361
|
+
// Resolve all remaining
|
|
362
|
+
let newText = session.getText();
|
|
363
|
+
for (let j = currentIndex; j < comments.length; j++) {
|
|
364
|
+
if (setStatus) {
|
|
365
|
+
newText = setStatus(newText, comments[j], true);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
session.applyChange(newText, `Resolved ${comments.length - currentIndex} comments`);
|
|
369
|
+
resolved += comments.length - currentIndex;
|
|
370
|
+
currentIndex = comments.length;
|
|
371
|
+
break;
|
|
372
|
+
|
|
373
|
+
case 'm':
|
|
374
|
+
if (setStatus) {
|
|
375
|
+
const newText = setStatus(session.getText(), comment, true);
|
|
376
|
+
session.applyChange(newText, `Resolved comment #${currentIndex + 1}`);
|
|
377
|
+
}
|
|
378
|
+
resolved++;
|
|
379
|
+
currentIndex++;
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case 'r':
|
|
383
|
+
console.log();
|
|
384
|
+
const replyText = await promptText(chalk.cyan(' Reply: '));
|
|
385
|
+
if (replyText.trim() && addReply) {
|
|
386
|
+
const newText = addReply(session.getText(), comment, author, replyText.trim());
|
|
387
|
+
session.applyChange(newText, `Reply to comment #${currentIndex + 1}`);
|
|
388
|
+
replied++;
|
|
389
|
+
}
|
|
390
|
+
currentIndex++;
|
|
391
|
+
break;
|
|
392
|
+
|
|
393
|
+
case 's':
|
|
394
|
+
skipped++;
|
|
395
|
+
currentIndex++;
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case 'n':
|
|
399
|
+
if (currentIndex < comments.length - 1) {
|
|
400
|
+
currentIndex++;
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
|
|
404
|
+
case 'p':
|
|
405
|
+
if (currentIndex > 0) {
|
|
406
|
+
currentIndex--;
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Summary
|
|
413
|
+
clearScreen();
|
|
414
|
+
console.log(chalk.cyan.bold(' Review Complete'));
|
|
415
|
+
console.log();
|
|
416
|
+
|
|
417
|
+
const undoInfo = session.info();
|
|
418
|
+
const summaryBox = drawBox({
|
|
419
|
+
title: 'Summary',
|
|
420
|
+
content: [
|
|
421
|
+
chalk.green(`Resolved: ${resolved}`),
|
|
422
|
+
chalk.blue(`Replied: ${replied}`),
|
|
423
|
+
chalk.yellow(`Skipped: ${skipped}`),
|
|
424
|
+
undoInfo.undoSteps > 1 ? chalk.dim(`Changes: ${undoInfo.undoSteps}`) : '',
|
|
425
|
+
].filter(Boolean),
|
|
426
|
+
width: 30,
|
|
427
|
+
borderColor: 'cyan',
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
for (const line of summaryBox) {
|
|
431
|
+
console.log(' ' + line);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
console.log();
|
|
435
|
+
|
|
436
|
+
return { text: session.getText(), resolved, replied, skipped };
|
|
437
|
+
}
|
package/lib/undo.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-session undo stack for comment/annotation operations
|
|
3
|
+
*
|
|
4
|
+
* Provides undo/redo functionality during interactive sessions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create an undo stack
|
|
9
|
+
* @param {number} maxSize - Maximum number of states to store
|
|
10
|
+
* @returns {object} Undo stack controller
|
|
11
|
+
*/
|
|
12
|
+
export function createUndoStack(maxSize = 50) {
|
|
13
|
+
const stack = [];
|
|
14
|
+
let position = -1;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
/**
|
|
18
|
+
* Push a new state onto the stack
|
|
19
|
+
* @param {any} state - State to save
|
|
20
|
+
* @param {string} description - Description of the change
|
|
21
|
+
*/
|
|
22
|
+
push(state, description = '') {
|
|
23
|
+
// Remove any states after current position (for redo)
|
|
24
|
+
if (position < stack.length - 1) {
|
|
25
|
+
stack.splice(position + 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Add new state
|
|
29
|
+
stack.push({
|
|
30
|
+
state: typeof state === 'string' ? state : JSON.parse(JSON.stringify(state)),
|
|
31
|
+
description,
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Enforce max size
|
|
36
|
+
while (stack.length > maxSize) {
|
|
37
|
+
stack.shift();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
position = stack.length - 1;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Undo to previous state
|
|
45
|
+
* @returns {{state: any, description: string}|null}
|
|
46
|
+
*/
|
|
47
|
+
undo() {
|
|
48
|
+
if (position <= 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
position--;
|
|
53
|
+
return stack[position];
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Redo to next state
|
|
58
|
+
* @returns {{state: any, description: string}|null}
|
|
59
|
+
*/
|
|
60
|
+
redo() {
|
|
61
|
+
if (position >= stack.length - 1) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
position++;
|
|
66
|
+
return stack[position];
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get current state
|
|
71
|
+
* @returns {{state: any, description: string}|null}
|
|
72
|
+
*/
|
|
73
|
+
current() {
|
|
74
|
+
if (position < 0 || position >= stack.length) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return stack[position];
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if undo is available
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
canUndo() {
|
|
85
|
+
return position > 0;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if redo is available
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
canRedo() {
|
|
93
|
+
return position < stack.length - 1;
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get stack info
|
|
98
|
+
* @returns {{position: number, size: number, undoSteps: number, redoSteps: number}}
|
|
99
|
+
*/
|
|
100
|
+
info() {
|
|
101
|
+
return {
|
|
102
|
+
position,
|
|
103
|
+
size: stack.length,
|
|
104
|
+
undoSteps: position,
|
|
105
|
+
redoSteps: stack.length - position - 1,
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get history of changes
|
|
111
|
+
* @param {number} limit - Max items to return
|
|
112
|
+
* @returns {Array<{description: string, current: boolean, index: number}>}
|
|
113
|
+
*/
|
|
114
|
+
history(limit = 10) {
|
|
115
|
+
const start = Math.max(0, position - Math.floor(limit / 2));
|
|
116
|
+
const end = Math.min(stack.length, start + limit);
|
|
117
|
+
|
|
118
|
+
return stack.slice(start, end).map((item, i) => ({
|
|
119
|
+
description: item.description,
|
|
120
|
+
current: start + i === position,
|
|
121
|
+
index: start + i,
|
|
122
|
+
}));
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clear the stack
|
|
127
|
+
*/
|
|
128
|
+
clear() {
|
|
129
|
+
stack.length = 0;
|
|
130
|
+
position = -1;
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the full stack (for debugging)
|
|
135
|
+
* @returns {Array}
|
|
136
|
+
*/
|
|
137
|
+
getStack() {
|
|
138
|
+
return [...stack];
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a document session with undo support
|
|
145
|
+
* @param {string} initialText - Initial document text
|
|
146
|
+
* @returns {object} Session controller
|
|
147
|
+
*/
|
|
148
|
+
export function createDocumentSession(initialText) {
|
|
149
|
+
const undoStack = createUndoStack();
|
|
150
|
+
|
|
151
|
+
// Save initial state
|
|
152
|
+
undoStack.push(initialText, 'Initial state');
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
/**
|
|
156
|
+
* Get current text
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
getText() {
|
|
160
|
+
const current = undoStack.current();
|
|
161
|
+
return current ? current.state : initialText;
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Apply a change
|
|
166
|
+
* @param {string} newText - New document text
|
|
167
|
+
* @param {string} description - What changed
|
|
168
|
+
*/
|
|
169
|
+
applyChange(newText, description) {
|
|
170
|
+
undoStack.push(newText, description);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Undo last change
|
|
175
|
+
* @returns {{text: string, description: string}|null}
|
|
176
|
+
*/
|
|
177
|
+
undo() {
|
|
178
|
+
const result = undoStack.undo();
|
|
179
|
+
if (result) {
|
|
180
|
+
return {
|
|
181
|
+
text: result.state,
|
|
182
|
+
description: result.description,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Redo last undone change
|
|
190
|
+
* @returns {{text: string, description: string}|null}
|
|
191
|
+
*/
|
|
192
|
+
redo() {
|
|
193
|
+
const result = undoStack.redo();
|
|
194
|
+
if (result) {
|
|
195
|
+
return {
|
|
196
|
+
text: result.state,
|
|
197
|
+
description: result.description,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if undo is available
|
|
205
|
+
* @returns {boolean}
|
|
206
|
+
*/
|
|
207
|
+
canUndo() {
|
|
208
|
+
return undoStack.canUndo();
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if redo is available
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
canRedo() {
|
|
216
|
+
return undoStack.canRedo();
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get stack info
|
|
221
|
+
* @returns {object}
|
|
222
|
+
*/
|
|
223
|
+
info() {
|
|
224
|
+
return undoStack.info();
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get change history
|
|
229
|
+
* @param {number} limit
|
|
230
|
+
* @returns {Array}
|
|
231
|
+
*/
|
|
232
|
+
history(limit = 10) {
|
|
233
|
+
return undoStack.history(limit);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|