@stll/folio-core 0.2.0 → 0.3.1
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/README.md +2 -2
- package/dist/ai-edits/clean-text.d.ts +21 -1
- package/dist/ai-edits/clean-text.js +69 -1
- package/dist/ai-edits/headless.d.ts +49 -22
- package/dist/ai-edits/headless.js +101 -77
- package/dist/ai-edits/index.d.ts +3 -1
- package/dist/ai-edits/index.js +3 -1
- package/dist/ai-edits/read.d.ts +43 -0
- package/dist/ai-edits/read.js +91 -0
- package/dist/compat/eigenpal.d.ts +29 -0
- package/dist/compat/eigenpal.js +24 -0
- package/dist/controller/layoutPipeline.d.ts +1 -1
- package/dist/controller/layoutSession.d.ts +1 -1
- package/dist/docx/parser.d.ts +1 -1
- package/dist/docx/parser.js +1 -1
- package/dist/i18n/messages/catalogs.gen.d.ts +7123 -0
- package/dist/i18n/messages/catalogs.gen.js +7089 -0
- package/dist/i18n/messages/messages.gen.d.ts +419 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +2 -1
- package/dist/layout-bridge/convert/templatePreviewFlow.d.ts +1 -1
- package/dist/server.d.ts +4 -3
- package/dist/utils/fontResolver.d.ts +3 -1
- package/dist/utils/fontResolver.js +11 -4
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ changes, and footnotes.
|
|
|
12
12
|
|
|
13
13
|
There is **no React** in the import graph, so the core runs on a server or under
|
|
14
14
|
any framework. The React editor lives in
|
|
15
|
-
[`@stll/folio-react`](https://www.npmjs.com/package/@stll/folio-react)
|
|
16
|
-
|
|
15
|
+
[`@stll/folio-react`](https://www.npmjs.com/package/@stll/folio-react), and the
|
|
16
|
+
Vue editor lives in [`@stll/folio-vue`](https://www.npmjs.com/package/@stll/folio-vue).
|
|
17
17
|
|
|
18
18
|
Part of [stella](https://github.com/stella/stella), an open-source legal workspace.
|
|
19
19
|
|
|
@@ -24,5 +24,25 @@ type CleanBlockText = {
|
|
|
24
24
|
offsets: number[];
|
|
25
25
|
};
|
|
26
26
|
declare const buildCleanBlockText: (blockNode: Node, blockFrom: number) => CleanBlockText;
|
|
27
|
+
/**
|
|
28
|
+
* A "redline-aware" view of a textblock: the same left-to-right traversal as
|
|
29
|
+
* {@link buildCleanBlockText}, but every tracked change and comment anchor is
|
|
30
|
+
* rendered inline with a simple tag rather than being flattened away:
|
|
31
|
+
*
|
|
32
|
+
* - `<ins author="…">text</ins>` for insertion-marked runs,
|
|
33
|
+
* - `<del author="…">text</del>` for deletion-marked runs (tracked moves
|
|
34
|
+
* surface as plain ins/del, since a move carries the same marks),
|
|
35
|
+
* - `<comment id="N">quoted text</comment>` for comment-anchored runs.
|
|
36
|
+
*
|
|
37
|
+
* Nested annotations (e.g. an inserted run that is also commented) nest their
|
|
38
|
+
* tags in a stable `comment > ins > del` order. Text content and attribute
|
|
39
|
+
* values are XML-escaped so the tags stay unambiguous when embedded in a
|
|
40
|
+
* prompt. Adjacent runs sharing the same annotation coalesce into one tag.
|
|
41
|
+
*
|
|
42
|
+
* This is the view a consumer embeds when it wants the model to reason about
|
|
43
|
+
* the redline itself, in contrast to {@link buildCleanBlockText}'s
|
|
44
|
+
* post-tracked-changes view.
|
|
45
|
+
*/
|
|
46
|
+
declare const buildAnnotatedBlockText: (blockNode: Node) => string;
|
|
27
47
|
//#endregion
|
|
28
|
-
export { CleanBlockText, buildCleanBlockText };
|
|
48
|
+
export { CleanBlockText, buildAnnotatedBlockText, buildCleanBlockText };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//#region src/ai-edits/clean-text.ts
|
|
2
2
|
const DELETION_MARK = "deletion";
|
|
3
|
+
const INSERTION_MARK = "insertion";
|
|
4
|
+
const COMMENT_MARK = "comment";
|
|
3
5
|
const buildCleanBlockText = (blockNode, blockFrom) => {
|
|
4
6
|
let text = "";
|
|
5
7
|
const offsets = [];
|
|
@@ -19,5 +21,71 @@ const buildCleanBlockText = (blockNode, blockFrom) => {
|
|
|
19
21
|
offsets
|
|
20
22
|
};
|
|
21
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* A "redline-aware" view of a textblock: the same left-to-right traversal as
|
|
26
|
+
* {@link buildCleanBlockText}, but every tracked change and comment anchor is
|
|
27
|
+
* rendered inline with a simple tag rather than being flattened away:
|
|
28
|
+
*
|
|
29
|
+
* - `<ins author="…">text</ins>` for insertion-marked runs,
|
|
30
|
+
* - `<del author="…">text</del>` for deletion-marked runs (tracked moves
|
|
31
|
+
* surface as plain ins/del, since a move carries the same marks),
|
|
32
|
+
* - `<comment id="N">quoted text</comment>` for comment-anchored runs.
|
|
33
|
+
*
|
|
34
|
+
* Nested annotations (e.g. an inserted run that is also commented) nest their
|
|
35
|
+
* tags in a stable `comment > ins > del` order. Text content and attribute
|
|
36
|
+
* values are XML-escaped so the tags stay unambiguous when embedded in a
|
|
37
|
+
* prompt. Adjacent runs sharing the same annotation coalesce into one tag.
|
|
38
|
+
*
|
|
39
|
+
* This is the view a consumer embeds when it wants the model to reason about
|
|
40
|
+
* the redline itself, in contrast to {@link buildCleanBlockText}'s
|
|
41
|
+
* post-tracked-changes view.
|
|
42
|
+
*/
|
|
43
|
+
const buildAnnotatedBlockText = (blockNode) => {
|
|
44
|
+
const segments = [];
|
|
45
|
+
blockNode.descendants((node) => {
|
|
46
|
+
if (!node.isText || node.text === void 0) return true;
|
|
47
|
+
const annotation = annotationOf(node.marks);
|
|
48
|
+
const previous = segments.at(-1);
|
|
49
|
+
if (previous && sameAnnotation(previous.annotation, annotation)) {
|
|
50
|
+
previous.text += node.text;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
segments.push({
|
|
54
|
+
annotation,
|
|
55
|
+
text: node.text
|
|
56
|
+
});
|
|
57
|
+
return false;
|
|
58
|
+
});
|
|
59
|
+
return segments.map(renderAnnotatedSegment).join("");
|
|
60
|
+
};
|
|
61
|
+
const authorOf = (attrs) => {
|
|
62
|
+
const author = attrs["author"];
|
|
63
|
+
return typeof author === "string" ? author : "";
|
|
64
|
+
};
|
|
65
|
+
const annotationOf = (marks) => {
|
|
66
|
+
let commentId = null;
|
|
67
|
+
let insertionAuthor = null;
|
|
68
|
+
let deletionAuthor = null;
|
|
69
|
+
for (const mark of marks) if (mark.type.name === COMMENT_MARK) {
|
|
70
|
+
const id = mark.attrs["commentId"];
|
|
71
|
+
if (typeof id === "number") commentId = id;
|
|
72
|
+
} else if (mark.type.name === INSERTION_MARK) insertionAuthor = authorOf(mark.attrs);
|
|
73
|
+
else if (mark.type.name === DELETION_MARK) deletionAuthor = authorOf(mark.attrs);
|
|
74
|
+
return {
|
|
75
|
+
commentId,
|
|
76
|
+
insertionAuthor,
|
|
77
|
+
deletionAuthor
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
const sameAnnotation = (a, b) => a.commentId === b.commentId && a.insertionAuthor === b.insertionAuthor && a.deletionAuthor === b.deletionAuthor;
|
|
81
|
+
const escapeText = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
82
|
+
const escapeAttr = (value) => escapeText(value).replaceAll("\"", """);
|
|
83
|
+
const renderAnnotatedSegment = ({ annotation, text }) => {
|
|
84
|
+
let inner = escapeText(text);
|
|
85
|
+
if (annotation.deletionAuthor !== null) inner = `<del author="${escapeAttr(annotation.deletionAuthor)}">${inner}</del>`;
|
|
86
|
+
if (annotation.insertionAuthor !== null) inner = `<ins author="${escapeAttr(annotation.insertionAuthor)}">${inner}</ins>`;
|
|
87
|
+
if (annotation.commentId !== null) inner = `<comment id="${annotation.commentId}">${inner}</comment>`;
|
|
88
|
+
return inner;
|
|
89
|
+
};
|
|
22
90
|
//#endregion
|
|
23
|
-
export { buildCleanBlockText };
|
|
91
|
+
export { buildAnnotatedBlockText, buildCleanBlockText };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FolioAIBlock, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./types.js";
|
|
2
2
|
import { document_d_exports } from "../types/document.js";
|
|
3
|
+
import { FolioReviewChange, FolioReviewChangeKind } from "./read.js";
|
|
3
4
|
|
|
4
5
|
//#region src/ai-edits/headless.d.ts
|
|
5
6
|
/** Options for {@link FolioDocxReviewer.fromBuffer}. */
|
|
@@ -16,26 +17,14 @@ type FolioApplyOperationsOptions = {
|
|
|
16
17
|
*/
|
|
17
18
|
snapshot?: FolioAIEditSnapshot;
|
|
18
19
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type FolioReviewChange = {
|
|
20
|
+
/** Options for {@link FolioDocxReviewer.getContentAsText}. */
|
|
21
|
+
type FolioGetContentAsTextOptions = {
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* two changes (a deletion side and an insertion side) with distinct ids.
|
|
23
|
+
* Render tracked changes and comment anchors inline as `<ins>` / `<del>` /
|
|
24
|
+
* `<comment>` tags instead of the default flattened, post-tracked-changes
|
|
25
|
+
* text. (default: `false`)
|
|
27
26
|
*/
|
|
28
|
-
|
|
29
|
-
type: FolioReviewChangeKind;
|
|
30
|
-
author: string; /** ISO date the change was authored, or `null` when the source omitted it. */
|
|
31
|
-
date: string | null; /** Inserted text for insertions, removed text for deletions. */
|
|
32
|
-
text: string;
|
|
33
|
-
/**
|
|
34
|
-
* Stable id of the containing body block (Word `w14:paraId` or `seq-NNNN`),
|
|
35
|
-
* matching {@link FolioDocxReviewer.getContent}. `null` when the change sits
|
|
36
|
-
* in a block with no surviving visible text, which has no snapshot block.
|
|
37
|
-
*/
|
|
38
|
-
blockId: string | null;
|
|
27
|
+
annotated?: boolean;
|
|
39
28
|
};
|
|
40
29
|
/** Filter for {@link FolioDocxReviewer.getChanges}. */
|
|
41
30
|
type FolioReviewChangeFilter = {
|
|
@@ -92,6 +81,15 @@ declare class FolioDocxReviewer {
|
|
|
92
81
|
private readonly originalBuffer;
|
|
93
82
|
private state;
|
|
94
83
|
private readonly createdComments;
|
|
84
|
+
/**
|
|
85
|
+
* Resolved-state overrides recorded by {@link resolveComment}, keyed by
|
|
86
|
+
* comment id. Applied on read ({@link getComments}) and on write
|
|
87
|
+
* ({@link toDocument}) rather than mutating the parsed `Comment` objects in
|
|
88
|
+
* place, matching how the parser/serializer treat `Comment` as immutable
|
|
89
|
+
* (`commentParser.ts` replaces array slots via spread; the serializer
|
|
90
|
+
* types its inputs `readonly Comment[]`).
|
|
91
|
+
*/
|
|
92
|
+
private readonly resolvedOverrides;
|
|
95
93
|
private constructor();
|
|
96
94
|
/** Parse a `.docx` buffer into a reviewer. */
|
|
97
95
|
static fromBuffer(buffer: ArrayBuffer, options?: FolioDocxReviewerOptions): Promise<FolioDocxReviewer>;
|
|
@@ -116,8 +114,22 @@ declare class FolioDocxReviewer {
|
|
|
116
114
|
* The body as LLM-ready plain text: one line per block, each prefixed with
|
|
117
115
|
* its stable block id (`[<blockId>] text`). Copyable verbatim into a prompt
|
|
118
116
|
* without JSON quote-escaping.
|
|
117
|
+
*
|
|
118
|
+
* With `{ annotated: true }` each block's text is rendered redline-aware:
|
|
119
|
+
* tracked insertions/deletions and comment anchors appear inline as
|
|
120
|
+
* `<ins>` / `<del>` / `<comment>` tags (see {@link buildAnnotatedBlockText})
|
|
121
|
+
* for prompt-embedding parity with a live editor's redline view. The default
|
|
122
|
+
* (clean) output flattens tracked changes and is unchanged.
|
|
119
123
|
*/
|
|
120
|
-
getContentAsText(): string;
|
|
124
|
+
getContentAsText(options?: FolioGetContentAsTextOptions): string;
|
|
125
|
+
/**
|
|
126
|
+
* Header / footer and footnote / endnote text as labeled, LLM-ready lines,
|
|
127
|
+
* one per non-empty part: `[header default] …`, `[footer default] …`,
|
|
128
|
+
* `[footnote #N] …`, `[endnote #N] …`. Read-only — note bodies are outside
|
|
129
|
+
* the reviewer's body-only apply scope, so this reflects the parsed source.
|
|
130
|
+
* Empty parts and separator notes are omitted.
|
|
131
|
+
*/
|
|
132
|
+
getNotesAsText(): string;
|
|
121
133
|
/**
|
|
122
134
|
* The tracked changes (insertions and deletions) present in the body, read
|
|
123
135
|
* from the same `insertion` / `deletion` marks the editor renders. Each
|
|
@@ -141,6 +153,21 @@ declare class FolioDocxReviewer {
|
|
|
141
153
|
* the created reply, or `null` when the target comment is absent.
|
|
142
154
|
*/
|
|
143
155
|
replyTo(target: FolioReviewComment | number, input: FolioReviewReplyInput): FolioReviewCommentReply | null;
|
|
156
|
+
/**
|
|
157
|
+
* Mark a comment thread resolved, or reopen a previously resolved one. Pass
|
|
158
|
+
* the id from {@link getComments} / {@link FolioReviewComment.id}. Applies
|
|
159
|
+
* only to the target comment id, not cascaded to its replies: Word keys the
|
|
160
|
+
* resolved marker off the comment's own `w15:commentEx` entry
|
|
161
|
+
* (`commentSerializer.ts`'s `buildCommentExtendedEntries` reads only
|
|
162
|
+
* `comment.done` for the id it is building an entry for), so resolving the
|
|
163
|
+
* thread root is sufficient and reply entries need no `done` flag of their
|
|
164
|
+
* own. On {@link toBuffer} the state is written to `commentsExtended.xml`
|
|
165
|
+
* (`w15:done`) through the same channel `replyTo`-created comments use.
|
|
166
|
+
* Returns `false` when no comment with that id exists.
|
|
167
|
+
*/
|
|
168
|
+
resolveComment(commentId: string, options?: {
|
|
169
|
+
resolved?: boolean;
|
|
170
|
+
}): boolean;
|
|
144
171
|
/**
|
|
145
172
|
* Accept an existing tracked change, keeping its text and dropping the
|
|
146
173
|
* redline. Pass a {@link FolioReviewChange} from {@link getChanges} or its
|
|
@@ -187,8 +214,8 @@ declare class FolioDocxReviewer {
|
|
|
187
214
|
* paragraph attrs rather than inline marks.
|
|
188
215
|
*/
|
|
189
216
|
private countTrackedChanges;
|
|
190
|
-
/**
|
|
191
|
-
private
|
|
217
|
+
/** Apply any {@link resolveComment} overrides recorded for these comments. */
|
|
218
|
+
private withResolvedOverrides;
|
|
192
219
|
/** Map each anchored comment id to its anchored text and containing block id. */
|
|
193
220
|
private commentAnchors;
|
|
194
221
|
/**
|
|
@@ -222,4 +249,4 @@ type ApplyFolioAIEditsToBufferResult = FolioAIEditApplyResult & {
|
|
|
222
249
|
*/
|
|
223
250
|
declare const applyFolioAIEditsToBuffer: (buffer: ArrayBuffer, operations: FolioAIEditOperation[], options?: ApplyFolioAIEditsToBufferOptions) => Promise<ApplyFolioAIEditsToBufferResult>;
|
|
224
251
|
//#endregion
|
|
225
|
-
export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyOperationsOptions, FolioDocxReviewer, FolioDocxReviewerOptions, FolioReviewChange, FolioReviewChangeFilter, FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer };
|
|
252
|
+
export { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyOperationsOptions, FolioDocxReviewer, FolioDocxReviewerOptions, FolioGetContentAsTextOptions, type FolioReviewChange, FolioReviewChangeFilter, type FolioReviewChangeKind, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer };
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { getEndnoteText, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote } from "../docx/footnoteParser.js";
|
|
1
2
|
import { deterministicHexId } from "../utils/hexId.js";
|
|
2
3
|
import { repackDocx } from "../docx/rezip.js";
|
|
3
|
-
import {
|
|
4
|
+
import { buildAnnotatedBlockText } from "./clean-text.js";
|
|
5
|
+
import { createFolioAIEditSnapshot, normalizeFolioAIBlockText } from "./snapshot.js";
|
|
4
6
|
import { applyFolioAIEditOperations } from "./apply.js";
|
|
7
|
+
import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
|
|
5
8
|
import { createReply } from "../docx/replyToComment.js";
|
|
6
9
|
import { attemptSelectiveSave } from "../docx/selectiveSave.js";
|
|
10
|
+
import { getHeaderFooterText } from "../docx/headerFooterParser.js";
|
|
7
11
|
import { parseDocx } from "../docx/parser.js";
|
|
8
12
|
import { acceptAIEditRevision, acceptAllChanges, rejectAIEditRevision, rejectAllChanges } from "../prosemirror/commands/comments.js";
|
|
9
13
|
import { updateDocumentContent } from "../prosemirror/conversion/fromProseDoc.js";
|
|
@@ -114,12 +118,13 @@ const ensureDeterministicParaIdsInState = (state) => {
|
|
|
114
118
|
* for headings and the list marker for list items, so a model can copy the
|
|
115
119
|
* block id straight back into an operation.
|
|
116
120
|
*/
|
|
117
|
-
const
|
|
121
|
+
const formatBlockLine = (block, text) => {
|
|
118
122
|
const label = `[${block.id}]`;
|
|
119
|
-
if (block.kind === "heading") return `${label} (h${headingLevel(block)}) ${
|
|
120
|
-
if (block.kind === "listItem") return `${label} ${block.displayLabel ?? "•"} ${
|
|
121
|
-
return `${label} ${
|
|
123
|
+
if (block.kind === "heading") return `${label} (h${headingLevel(block)}) ${text}`;
|
|
124
|
+
if (block.kind === "listItem") return `${label} ${block.displayLabel ?? "•"} ${text}`;
|
|
125
|
+
return `${label} ${text}`;
|
|
122
126
|
};
|
|
127
|
+
const formatBlockForLLM = (block) => formatBlockLine(block, block.text);
|
|
123
128
|
const headingLevel = (block) => {
|
|
124
129
|
const digits = /(\d+)/u.exec(block.styleId ?? block.displayLabel ?? "")?.[1];
|
|
125
130
|
const level = digits ? Number.parseInt(digits, 10) : 1;
|
|
@@ -157,6 +162,15 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
157
162
|
originalBuffer;
|
|
158
163
|
state;
|
|
159
164
|
createdComments = [];
|
|
165
|
+
/**
|
|
166
|
+
* Resolved-state overrides recorded by {@link resolveComment}, keyed by
|
|
167
|
+
* comment id. Applied on read ({@link getComments}) and on write
|
|
168
|
+
* ({@link toDocument}) rather than mutating the parsed `Comment` objects in
|
|
169
|
+
* place, matching how the parser/serializer treat `Comment` as immutable
|
|
170
|
+
* (`commentParser.ts` replaces array slots via spread; the serializer
|
|
171
|
+
* types its inputs `readonly Comment[]`).
|
|
172
|
+
*/
|
|
173
|
+
resolvedOverrides = /* @__PURE__ */ new Map();
|
|
160
174
|
constructor(args) {
|
|
161
175
|
this.baseDocument = args.baseDocument;
|
|
162
176
|
this.originalBuffer = args.originalBuffer;
|
|
@@ -228,9 +242,54 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
228
242
|
* The body as LLM-ready plain text: one line per block, each prefixed with
|
|
229
243
|
* its stable block id (`[<blockId>] text`). Copyable verbatim into a prompt
|
|
230
244
|
* without JSON quote-escaping.
|
|
245
|
+
*
|
|
246
|
+
* With `{ annotated: true }` each block's text is rendered redline-aware:
|
|
247
|
+
* tracked insertions/deletions and comment anchors appear inline as
|
|
248
|
+
* `<ins>` / `<del>` / `<comment>` tags (see {@link buildAnnotatedBlockText})
|
|
249
|
+
* for prompt-embedding parity with a live editor's redline view. The default
|
|
250
|
+
* (clean) output flattens tracked changes and is unchanged.
|
|
251
|
+
*/
|
|
252
|
+
getContentAsText(options = {}) {
|
|
253
|
+
if (!options.annotated) return this.getContent().map(formatBlockForLLM).join("\n");
|
|
254
|
+
const snapshot = this.snapshot();
|
|
255
|
+
const startById = /* @__PURE__ */ new Map();
|
|
256
|
+
for (const anchor of Object.values(snapshot.anchors)) startById.set(anchor.id, anchor.from);
|
|
257
|
+
return snapshot.blocks.map((block) => {
|
|
258
|
+
const from = startById.get(block.id);
|
|
259
|
+
const node = from === void 0 ? null : this.state.doc.nodeAt(from);
|
|
260
|
+
return formatBlockLine(block, node ? buildAnnotatedBlockText(node) : block.text);
|
|
261
|
+
}).join("\n");
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Header / footer and footnote / endnote text as labeled, LLM-ready lines,
|
|
265
|
+
* one per non-empty part: `[header default] …`, `[footer default] …`,
|
|
266
|
+
* `[footnote #N] …`, `[endnote #N] …`. Read-only — note bodies are outside
|
|
267
|
+
* the reviewer's body-only apply scope, so this reflects the parsed source.
|
|
268
|
+
* Empty parts and separator notes are omitted.
|
|
231
269
|
*/
|
|
232
|
-
|
|
233
|
-
|
|
270
|
+
getNotesAsText() {
|
|
271
|
+
const pkg = this.baseDocument.package;
|
|
272
|
+
const lines = [];
|
|
273
|
+
const pushHeaderFooter = (map, label) => {
|
|
274
|
+
if (!map) return;
|
|
275
|
+
for (const hf of map.values()) {
|
|
276
|
+
const text = normalizeFolioAIBlockText(getHeaderFooterText(hf));
|
|
277
|
+
if (text.length > 0) lines.push(`[${label} ${hf.hdrFtrType}] ${text}`);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
pushHeaderFooter(pkg.headers, "header");
|
|
281
|
+
pushHeaderFooter(pkg.footers, "footer");
|
|
282
|
+
for (const footnote of pkg.footnotes ?? []) {
|
|
283
|
+
if (isSeparatorFootnote(footnote)) continue;
|
|
284
|
+
const text = normalizeFolioAIBlockText(getFootnoteText(footnote));
|
|
285
|
+
if (text.length > 0) lines.push(`[footnote #${footnote.id}] ${text}`);
|
|
286
|
+
}
|
|
287
|
+
for (const endnote of pkg.endnotes ?? []) {
|
|
288
|
+
if (isSeparatorEndnote(endnote)) continue;
|
|
289
|
+
const text = normalizeFolioAIBlockText(getEndnoteText(endnote));
|
|
290
|
+
if (text.length > 0) lines.push(`[endnote #${endnote.id}] ${text}`);
|
|
291
|
+
}
|
|
292
|
+
return lines.join("\n");
|
|
234
293
|
}
|
|
235
294
|
/**
|
|
236
295
|
* The tracked changes (insertions and deletions) present in the body, read
|
|
@@ -239,44 +298,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
239
298
|
* against. Runs of one revision within a block fold into a single entry.
|
|
240
299
|
*/
|
|
241
300
|
getChanges(filter) {
|
|
242
|
-
const
|
|
243
|
-
const deletionType = this.state.schema.marks["deletion"];
|
|
244
|
-
const blockStarts = this.blockStartIds();
|
|
245
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
246
|
-
let currentBlockId = null;
|
|
247
|
-
this.state.doc.descendants((node, pos) => {
|
|
248
|
-
if (node.isTextblock) {
|
|
249
|
-
currentBlockId = blockStarts.get(pos) ?? null;
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
if (!node.isInline || node.text === void 0) return;
|
|
253
|
-
const text = node.text;
|
|
254
|
-
for (const mark of node.marks) {
|
|
255
|
-
if (typeof mark.attrs["revisionId"] !== "number") continue;
|
|
256
|
-
let kind;
|
|
257
|
-
if (mark.type === insertionType) kind = "insertion";
|
|
258
|
-
else if (mark.type === deletionType) kind = "deletion";
|
|
259
|
-
else continue;
|
|
260
|
-
const revisionId = mark.attrs["revisionId"];
|
|
261
|
-
const key = `${currentBlockId ?? ""}:${kind}:${revisionId}`;
|
|
262
|
-
const existing = grouped.get(key);
|
|
263
|
-
if (existing) {
|
|
264
|
-
existing.text += text;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const author = mark.attrs["author"];
|
|
268
|
-
const date = mark.attrs["date"];
|
|
269
|
-
grouped.set(key, {
|
|
270
|
-
id: revisionId,
|
|
271
|
-
type: kind,
|
|
272
|
-
author: typeof author === "string" ? author : "",
|
|
273
|
-
date: typeof date === "string" ? date : null,
|
|
274
|
-
text,
|
|
275
|
-
blockId: currentBlockId
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
const changes = [...grouped.values()];
|
|
301
|
+
const changes = getTrackedChangesFromDoc(this.state.doc);
|
|
280
302
|
if (!filter) return changes;
|
|
281
303
|
return changes.filter((change) => (filter.author === void 0 || change.author === filter.author) && (filter.type === void 0 || change.type === filter.type));
|
|
282
304
|
}
|
|
@@ -287,7 +309,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
287
309
|
* (nested tracked changes, hyperlinks) is out of scope.
|
|
288
310
|
*/
|
|
289
311
|
getComments(filter) {
|
|
290
|
-
const definitions = [...this.baseDocument.package.document.comments ?? [], ...this.createdComments];
|
|
312
|
+
const definitions = this.withResolvedOverrides([...this.baseDocument.package.document.comments ?? [], ...this.createdComments]);
|
|
291
313
|
if (definitions.length === 0) return [];
|
|
292
314
|
const anchors = this.commentAnchors();
|
|
293
315
|
const repliesByParent = /* @__PURE__ */ new Map();
|
|
@@ -347,6 +369,25 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
347
369
|
};
|
|
348
370
|
}
|
|
349
371
|
/**
|
|
372
|
+
* Mark a comment thread resolved, or reopen a previously resolved one. Pass
|
|
373
|
+
* the id from {@link getComments} / {@link FolioReviewComment.id}. Applies
|
|
374
|
+
* only to the target comment id, not cascaded to its replies: Word keys the
|
|
375
|
+
* resolved marker off the comment's own `w15:commentEx` entry
|
|
376
|
+
* (`commentSerializer.ts`'s `buildCommentExtendedEntries` reads only
|
|
377
|
+
* `comment.done` for the id it is building an entry for), so resolving the
|
|
378
|
+
* thread root is sufficient and reply entries need no `done` flag of their
|
|
379
|
+
* own. On {@link toBuffer} the state is written to `commentsExtended.xml`
|
|
380
|
+
* (`w15:done`) through the same channel `replyTo`-created comments use.
|
|
381
|
+
* Returns `false` when no comment with that id exists.
|
|
382
|
+
*/
|
|
383
|
+
resolveComment(commentId, options = {}) {
|
|
384
|
+
const resolved = options.resolved ?? true;
|
|
385
|
+
const target = [...this.baseDocument.package.document.comments ?? [], ...this.createdComments].find((comment) => String(comment.id) === commentId);
|
|
386
|
+
if (!target) return false;
|
|
387
|
+
this.resolvedOverrides.set(target.id, resolved);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
350
391
|
* Accept an existing tracked change, keeping its text and dropping the
|
|
351
392
|
* redline. Pass a {@link FolioReviewChange} from {@link getChanges} or its
|
|
352
393
|
* revision id. Reuses the editor's own accept command headlessly, so the
|
|
@@ -384,7 +425,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
384
425
|
/** The current document model with edits merged back in. */
|
|
385
426
|
toDocument() {
|
|
386
427
|
const document = updateDocumentContent(this.baseDocument, this.state.doc);
|
|
387
|
-
if (this.createdComments.length > 0) document.package.document.comments = [...document.package.document.comments ?? [], ...this.createdComments];
|
|
428
|
+
if (this.createdComments.length > 0 || this.resolvedOverrides.size > 0) document.package.document.comments = this.withResolvedOverrides([...document.package.document.comments ?? [], ...this.createdComments]);
|
|
388
429
|
return document;
|
|
389
430
|
}
|
|
390
431
|
/**
|
|
@@ -437,40 +478,23 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
437
478
|
});
|
|
438
479
|
return count;
|
|
439
480
|
}
|
|
440
|
-
/**
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
481
|
+
/** Apply any {@link resolveComment} overrides recorded for these comments. */
|
|
482
|
+
withResolvedOverrides(comments) {
|
|
483
|
+
if (this.resolvedOverrides.size === 0) return [...comments];
|
|
484
|
+
return comments.map((comment) => {
|
|
485
|
+
const override = this.resolvedOverrides.get(comment.id);
|
|
486
|
+
return override === void 0 ? comment : {
|
|
487
|
+
...comment,
|
|
488
|
+
done: override
|
|
489
|
+
};
|
|
490
|
+
});
|
|
445
491
|
}
|
|
446
492
|
/** Map each anchored comment id to its anchored text and containing block id. */
|
|
447
493
|
commentAnchors() {
|
|
448
|
-
const commentType = this.state.schema.marks["comment"];
|
|
449
494
|
const anchors = /* @__PURE__ */ new Map();
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
this.state.doc.descendants((node, pos) => {
|
|
454
|
-
if (node.isTextblock) {
|
|
455
|
-
currentBlockId = blockStarts.get(pos) ?? null;
|
|
456
|
-
return true;
|
|
457
|
-
}
|
|
458
|
-
if (!node.isInline || node.text === void 0) return;
|
|
459
|
-
const text = node.text;
|
|
460
|
-
for (const mark of node.marks) {
|
|
461
|
-
if (mark.type !== commentType || typeof mark.attrs["commentId"] !== "number") continue;
|
|
462
|
-
const commentId = mark.attrs["commentId"];
|
|
463
|
-
const existing = anchors.get(commentId);
|
|
464
|
-
if (!existing) {
|
|
465
|
-
anchors.set(commentId, {
|
|
466
|
-
text,
|
|
467
|
-
blockId: currentBlockId
|
|
468
|
-
});
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
existing.text += text;
|
|
472
|
-
existing.blockId ??= currentBlockId;
|
|
473
|
-
}
|
|
495
|
+
for (const anchor of getCommentAnchorsFromDoc(this.state.doc)) anchors.set(anchor.commentId, {
|
|
496
|
+
text: anchor.quote,
|
|
497
|
+
blockId: anchor.blockId
|
|
474
498
|
});
|
|
475
499
|
return anchors;
|
|
476
500
|
}
|
package/dist/ai-edits/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "./types.js";
|
|
2
2
|
import { FolioAIEditView, applyFolioAIEditOperations } from "./apply.js";
|
|
3
|
+
import { buildAnnotatedBlockText } from "./clean-text.js";
|
|
4
|
+
import { FolioCommentAnchor, FolioReviewChange, FolioReviewChangeKind, getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
|
|
3
5
|
import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js";
|
|
4
6
|
import { getFolioParaIdFromBlockId } from "../types/block-id.js";
|
|
5
7
|
import { WordDiffSegment, diffWordSegments } from "./word-diff.js";
|
|
6
|
-
export { type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAIEditView, type FolioAISignatureParty, type WordDiffSegment, applyFolioAIEditOperations, createFolioAIEditSnapshot, diffWordSegments, getFolioParaIdFromBlockId, hashFolioAIBlockText, normalizeFolioAIBlockText };
|
|
8
|
+
export { type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAIEditView, type FolioAISignatureParty, type FolioCommentAnchor, type FolioReviewChange, type FolioReviewChangeKind, type WordDiffSegment, applyFolioAIEditOperations, buildAnnotatedBlockText, createFolioAIEditSnapshot, diffWordSegments, getCommentAnchorsFromDoc, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, normalizeFolioAIBlockText };
|
package/dist/ai-edits/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getFolioParaIdFromBlockId } from "../types/block-id.js";
|
|
2
|
+
import { buildAnnotatedBlockText } from "./clean-text.js";
|
|
2
3
|
import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./snapshot.js";
|
|
3
4
|
import { diffWordSegments } from "./word-diff.js";
|
|
4
5
|
import { applyFolioAIEditOperations } from "./apply.js";
|
|
5
|
-
|
|
6
|
+
import { getCommentAnchorsFromDoc, getTrackedChangesFromDoc } from "./read.js";
|
|
7
|
+
export { applyFolioAIEditOperations, buildAnnotatedBlockText, createFolioAIEditSnapshot, diffWordSegments, getCommentAnchorsFromDoc, getFolioParaIdFromBlockId, getTrackedChangesFromDoc, hashFolioAIBlockText, normalizeFolioAIBlockText };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Node } from "prosemirror-model";
|
|
2
|
+
|
|
3
|
+
//#region src/ai-edits/read.d.ts
|
|
4
|
+
type FolioReviewChangeKind = "insertion" | "deletion";
|
|
5
|
+
/** A tracked change (insertion or deletion) discovered in the document body. */
|
|
6
|
+
type FolioReviewChange = {
|
|
7
|
+
/**
|
|
8
|
+
* The tracked-change revision id — the OOXML `w:id` carried on the
|
|
9
|
+
* insertion / deletion mark. A replace produces two changes (a deletion side
|
|
10
|
+
* and an insertion side) with distinct ids.
|
|
11
|
+
*/
|
|
12
|
+
id: number;
|
|
13
|
+
type: FolioReviewChangeKind;
|
|
14
|
+
author: string; /** ISO date the change was authored, or `null` when the source omitted it. */
|
|
15
|
+
date: string | null; /** Inserted text for insertions, removed text for deletions. */
|
|
16
|
+
text: string;
|
|
17
|
+
/**
|
|
18
|
+
* Stable id of the containing body block (Word `w14:paraId` or `seq-NNNN`).
|
|
19
|
+
* `null` when the change sits in a block with no surviving visible text,
|
|
20
|
+
* which has no snapshot block.
|
|
21
|
+
*/
|
|
22
|
+
blockId: string | null;
|
|
23
|
+
};
|
|
24
|
+
/** A comment anchor (the ranged text a comment marks) discovered in the body. */
|
|
25
|
+
type FolioCommentAnchor = {
|
|
26
|
+
commentId: number; /** Stable id of the anchored body block, or `null` when the anchor is absent. */
|
|
27
|
+
blockId: string | null; /** The document text the comment is anchored to. */
|
|
28
|
+
quote: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The tracked changes (insertions and deletions) present in the body, read from
|
|
32
|
+
* the `insertion` / `deletion` marks the editor renders. Runs of one revision
|
|
33
|
+
* within a block fold into a single entry.
|
|
34
|
+
*/
|
|
35
|
+
declare const getTrackedChangesFromDoc: (doc: Node) => FolioReviewChange[];
|
|
36
|
+
/**
|
|
37
|
+
* The comment anchors present in the body, read from the `comment` mark the
|
|
38
|
+
* editor renders. Each entry carries the anchored text and containing block id;
|
|
39
|
+
* runs of one comment id within the body fold into a single anchor.
|
|
40
|
+
*/
|
|
41
|
+
declare const getCommentAnchorsFromDoc: (doc: Node) => FolioCommentAnchor[];
|
|
42
|
+
//#endregion
|
|
43
|
+
export { FolioCommentAnchor, FolioReviewChange, FolioReviewChangeKind, getCommentAnchorsFromDoc, getTrackedChangesFromDoc };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createFolioAIEditSnapshot } from "./snapshot.js";
|
|
2
|
+
//#region src/ai-edits/read.ts
|
|
3
|
+
/** Map each snapshot block's start position to its stable id. */
|
|
4
|
+
const blockStartIdsFromDoc = (doc) => {
|
|
5
|
+
const starts = /* @__PURE__ */ new Map();
|
|
6
|
+
for (const anchor of Object.values(createFolioAIEditSnapshot(doc).anchors)) starts.set(anchor.from, anchor.id);
|
|
7
|
+
return starts;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* The tracked changes (insertions and deletions) present in the body, read from
|
|
11
|
+
* the `insertion` / `deletion` marks the editor renders. Runs of one revision
|
|
12
|
+
* within a block fold into a single entry.
|
|
13
|
+
*/
|
|
14
|
+
const getTrackedChangesFromDoc = (doc) => {
|
|
15
|
+
const insertionType = doc.type.schema.marks["insertion"];
|
|
16
|
+
const deletionType = doc.type.schema.marks["deletion"];
|
|
17
|
+
const blockStarts = blockStartIdsFromDoc(doc);
|
|
18
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
19
|
+
let currentBlockId = null;
|
|
20
|
+
doc.descendants((node, pos) => {
|
|
21
|
+
if (node.isTextblock) {
|
|
22
|
+
currentBlockId = blockStarts.get(pos) ?? null;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (!node.isInline || node.text === void 0) return;
|
|
26
|
+
const text = node.text;
|
|
27
|
+
for (const mark of node.marks) {
|
|
28
|
+
if (typeof mark.attrs["revisionId"] !== "number") continue;
|
|
29
|
+
let kind;
|
|
30
|
+
if (mark.type === insertionType) kind = "insertion";
|
|
31
|
+
else if (mark.type === deletionType) kind = "deletion";
|
|
32
|
+
else continue;
|
|
33
|
+
const revisionId = mark.attrs["revisionId"];
|
|
34
|
+
const key = `${currentBlockId ?? ""}:${kind}:${revisionId}`;
|
|
35
|
+
const existing = grouped.get(key);
|
|
36
|
+
if (existing) {
|
|
37
|
+
existing.text += text;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const author = mark.attrs["author"];
|
|
41
|
+
const date = mark.attrs["date"];
|
|
42
|
+
grouped.set(key, {
|
|
43
|
+
id: revisionId,
|
|
44
|
+
type: kind,
|
|
45
|
+
author: typeof author === "string" ? author : "",
|
|
46
|
+
date: typeof date === "string" ? date : null,
|
|
47
|
+
text,
|
|
48
|
+
blockId: currentBlockId
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return [...grouped.values()];
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The comment anchors present in the body, read from the `comment` mark the
|
|
56
|
+
* editor renders. Each entry carries the anchored text and containing block id;
|
|
57
|
+
* runs of one comment id within the body fold into a single anchor.
|
|
58
|
+
*/
|
|
59
|
+
const getCommentAnchorsFromDoc = (doc) => {
|
|
60
|
+
const commentType = doc.type.schema.marks["comment"];
|
|
61
|
+
if (!commentType) return [];
|
|
62
|
+
const blockStarts = blockStartIdsFromDoc(doc);
|
|
63
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
64
|
+
let currentBlockId = null;
|
|
65
|
+
doc.descendants((node, pos) => {
|
|
66
|
+
if (node.isTextblock) {
|
|
67
|
+
currentBlockId = blockStarts.get(pos) ?? null;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (!node.isInline || node.text === void 0) return;
|
|
71
|
+
const text = node.text;
|
|
72
|
+
for (const mark of node.marks) {
|
|
73
|
+
if (mark.type !== commentType || typeof mark.attrs["commentId"] !== "number") continue;
|
|
74
|
+
const commentId = mark.attrs["commentId"];
|
|
75
|
+
const existing = anchors.get(commentId);
|
|
76
|
+
if (!existing) {
|
|
77
|
+
anchors.set(commentId, {
|
|
78
|
+
commentId,
|
|
79
|
+
blockId: currentBlockId,
|
|
80
|
+
quote: text
|
|
81
|
+
});
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
existing.quote += text;
|
|
85
|
+
existing.blockId ??= currentBlockId;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return [...anchors.values()];
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
export { getCommentAnchorsFromDoc, getTrackedChangesFromDoc };
|