@ansonlai/docx-redline-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/AGENTS.md +176 -0
  2. package/ARCHITECTURE.md +121 -0
  3. package/LICENSE +21 -0
  4. package/README.md +177 -0
  5. package/adapters/config.js +43 -0
  6. package/adapters/logger.js +89 -0
  7. package/adapters/xml-adapter.js +74 -0
  8. package/core/list-targeting.js +398 -0
  9. package/core/ooxml-identifiers.js +15 -0
  10. package/core/paragraph-offset-policy.js +50 -0
  11. package/core/paragraph-targeting.js +501 -0
  12. package/core/table-targeting.js +233 -0
  13. package/core/types.js +204 -0
  14. package/core/xml-query.js +99 -0
  15. package/dist/docx-redline-js.esm.js +8801 -0
  16. package/dist/docx-redline-js.esm.js.map +7 -0
  17. package/dist/docx-redline-js.esm.min.js +195 -0
  18. package/dist/docx-redline-js.esm.min.js.map +7 -0
  19. package/engine/format-application.js +358 -0
  20. package/engine/format-extraction.js +232 -0
  21. package/engine/format-paragraph-targeting.js +208 -0
  22. package/engine/format-span-application.js +178 -0
  23. package/engine/formatting-removal.js +330 -0
  24. package/engine/oxml-engine.js +279 -0
  25. package/engine/reconstruction-mapper.js +270 -0
  26. package/engine/reconstruction-mode.js +38 -0
  27. package/engine/reconstruction-writer.js +276 -0
  28. package/engine/rpr-helpers.js +194 -0
  29. package/engine/run-builders.js +235 -0
  30. package/engine/surgical-mode.js +520 -0
  31. package/engine/table-cell-context.js +151 -0
  32. package/engine/table-mode.js +172 -0
  33. package/index.js +308 -0
  34. package/orchestration/list-markdown.js +141 -0
  35. package/orchestration/list-parsing.js +73 -0
  36. package/orchestration/list-structural-fallback.js +530 -0
  37. package/orchestration/redline-operation-converter.js +141 -0
  38. package/orchestration/route-plan.js +160 -0
  39. package/package.json +76 -0
  40. package/pipeline/content-analysis.js +107 -0
  41. package/pipeline/diff-engine.js +204 -0
  42. package/pipeline/ingestion-export.js +255 -0
  43. package/pipeline/ingestion-paragraph.js +351 -0
  44. package/pipeline/ingestion-table.js +169 -0
  45. package/pipeline/ingestion-xml.js +39 -0
  46. package/pipeline/ingestion.js +8 -0
  47. package/pipeline/list-generation.js +280 -0
  48. package/pipeline/list-markers.js +77 -0
  49. package/pipeline/markdown-processor.js +160 -0
  50. package/pipeline/patching.js +408 -0
  51. package/pipeline/pipeline.js +326 -0
  52. package/pipeline/serialization.js +395 -0
  53. package/services/browser-demo-prompt-context.js +345 -0
  54. package/services/comment-builders.js +60 -0
  55. package/services/comment-engine.js +248 -0
  56. package/services/comment-locator.js +197 -0
  57. package/services/comment-package.js +113 -0
  58. package/services/numbering-helpers.js +416 -0
  59. package/services/numbering-service.js +290 -0
  60. package/services/package-builder.js +147 -0
  61. package/services/standalone-docx-plumbing.js +443 -0
  62. package/services/standalone-operation-runner.js +1169 -0
  63. package/services/table-reconciliation.js +344 -0
  64. package/standalone.js +5 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Table-specific reconciliation and transformation flows.
3
+ */
4
+
5
+ import { NS_W, getNextRevisionId, getRevisionTimestamp } from '../core/types.js';
6
+ import {
7
+ getElementsByTag,
8
+ getElementsByTagNS,
9
+ getFirstElementByTag,
10
+ getFirstElementByTagNS,
11
+ getXmlParseError
12
+ } from '../core/xml-query.js';
13
+ import { createParser } from '../adapters/xml-adapter.js';
14
+ import { log, error } from '../adapters/logger.js';
15
+ import { diffTablesWithVirtualGrid, serializeVirtualGridToOoxml, generateTableOoxml } from '../services/table-reconciliation.js';
16
+ import { parseTable } from '../pipeline/pipeline.js';
17
+ import { ingestTableToVirtualGrid } from '../pipeline/ingestion.js';
18
+
19
+ function noChanges(serializer, xmlDoc) {
20
+ return { oxml: serializer.serializeToString(xmlDoc), hasChanges: false };
21
+ }
22
+
23
+ /**
24
+ * Applies structural reconciliation to tables using Virtual Grid.
25
+ *
26
+ * @param {Document} xmlDoc - XML document
27
+ * @param {string} modifiedText - Markdown/target table text
28
+ * @param {XMLSerializer} serializer - Serializer instance
29
+ * @param {DOMParser} parser - Parser instance
30
+ * @param {string} author - Author name
31
+ * @param {boolean} [generateRedlines=true] - Track change toggle
32
+ * @returns {{ oxml: string, hasChanges: boolean }}
33
+ */
34
+ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
35
+ const tableNodes = getElementsByTag(xmlDoc, 'w:tbl');
36
+ const newTableData = parseTable(modifiedText);
37
+ const hasNewContent = newTableData.rows.length > 0 || newTableData.headers.length > 0;
38
+
39
+ if (tableNodes.length === 0 || !hasNewContent) {
40
+ return noChanges(serializer, xmlDoc);
41
+ }
42
+
43
+ const targetTable = tableNodes[0];
44
+ const oldGrid = ingestTableToVirtualGrid(targetTable);
45
+ const operations = diffTablesWithVirtualGrid(oldGrid, newTableData);
46
+
47
+ if (operations.length === 0) {
48
+ return noChanges(serializer, xmlDoc);
49
+ }
50
+
51
+ const options = { generateRedlines, author };
52
+ const reconciledOxml = serializeVirtualGridToOoxml(oldGrid, operations, options);
53
+ const wrappedOxml = `<root xmlns:w="${NS_W}">${reconciledOxml}</root>`;
54
+ const reconcileParser = parser || createParser();
55
+ const reconciledDoc = reconcileParser.parseFromString(wrappedOxml, 'application/xml');
56
+
57
+ const parseError = getXmlParseError(reconciledDoc);
58
+ if (parseError) {
59
+ error('[OxmlEngine] Failed to parse reconciled table OOXML:', parseError.textContent);
60
+ return noChanges(serializer, xmlDoc);
61
+ }
62
+
63
+ const newTableNode = getFirstElementByTag(reconciledDoc, 'w:tbl');
64
+ if (!newTableNode) {
65
+ error('[OxmlEngine] No table found in reconciled OOXML');
66
+ return noChanges(serializer, xmlDoc);
67
+ }
68
+
69
+ const importedTable = xmlDoc.importNode(newTableNode, true);
70
+ targetTable.parentNode.replaceChild(importedTable, targetTable);
71
+
72
+ return { oxml: serializer.serializeToString(xmlDoc), hasChanges: true };
73
+ }
74
+
75
+ /**
76
+ * Transforms paragraph content into a new table from Markdown text.
77
+ *
78
+ * @param {Document} xmlDoc - XML document
79
+ * @param {string} modifiedText - Markdown table text
80
+ * @param {XMLSerializer} serializer - Serializer instance
81
+ * @param {DOMParser} parser - Parser instance
82
+ * @param {string} author - Author name
83
+ * @param {boolean} generateRedlines - Track change toggle
84
+ * @returns {{ oxml: string, hasChanges: boolean }}
85
+ */
86
+ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
87
+ const tableData = parseTable(modifiedText);
88
+ if (!tableData || (tableData.rows.length === 0 && tableData.headers.length === 0)) {
89
+ log('[OxmlEngine] Failed to parse table data from Markdown');
90
+ return noChanges(serializer, xmlDoc);
91
+ }
92
+
93
+ const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author });
94
+ const activeParser = parser || createParser();
95
+ const tableDoc = activeParser.parseFromString(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, 'application/xml');
96
+
97
+ const tableParseError = getXmlParseError(tableDoc);
98
+ if (tableParseError) {
99
+ error('[OxmlEngine] Failed to parse generated table OOXML:', tableParseError.textContent);
100
+ return noChanges(serializer, xmlDoc);
101
+ }
102
+
103
+ let newTableElement = getFirstElementByTagNS(tableDoc, NS_W, 'tbl');
104
+ if (!newTableElement) {
105
+ newTableElement = getFirstElementByTagNS(tableDoc, NS_W, 'ins');
106
+ }
107
+ if (!newTableElement) {
108
+ error('[OxmlEngine] No table element found in generated OOXML');
109
+ return noChanges(serializer, xmlDoc);
110
+ }
111
+
112
+ let workingDoc = xmlDoc;
113
+ let paragraphs = getElementsByTagNS(workingDoc, NS_W, 'p');
114
+ if (paragraphs.length === 0) {
115
+ log('[OxmlEngine] No paragraphs found to replace');
116
+ return noChanges(serializer, workingDoc);
117
+ }
118
+
119
+ let firstParagraph = paragraphs[0];
120
+ let parent = firstParagraph.parentNode;
121
+
122
+ if (parent && parent.nodeType === 9) {
123
+ const wrappedDoc = activeParser.parseFromString(
124
+ `<w:document xmlns:w="${NS_W}"><w:body/></w:document>`,
125
+ 'application/xml'
126
+ );
127
+ const wrappedBody = getFirstElementByTagNS(wrappedDoc, NS_W, 'body');
128
+ paragraphs.forEach(p => wrappedBody.appendChild(wrappedDoc.importNode(p, true)));
129
+
130
+ workingDoc = wrappedDoc;
131
+ paragraphs = getElementsByTagNS(workingDoc, NS_W, 'p');
132
+ firstParagraph = paragraphs[0];
133
+ parent = firstParagraph.parentNode;
134
+ }
135
+
136
+ const importedTable = workingDoc.importNode(newTableElement, true);
137
+
138
+ if (generateRedlines) {
139
+ const date = getRevisionTimestamp();
140
+ paragraphs.forEach(p => {
141
+ const runs = getElementsByTagNS(p, NS_W, 'r');
142
+ runs.forEach(run => {
143
+ const textNodes = getElementsByTagNS(run, NS_W, 't');
144
+ textNodes.forEach(t => {
145
+ const text = t.textContent || '';
146
+ if (text.trim()) {
147
+ const delText = workingDoc.createElementNS(NS_W, 'w:delText');
148
+ delText.textContent = text;
149
+ t.parentNode.replaceChild(delText, t);
150
+ }
151
+ });
152
+
153
+ const del = workingDoc.createElementNS(NS_W, 'w:del');
154
+ del.setAttribute('w:id', String(getNextRevisionId()));
155
+ del.setAttribute('w:author', author);
156
+ del.setAttribute('w:date', date);
157
+ run.parentNode.insertBefore(del, run);
158
+ del.appendChild(run);
159
+ });
160
+ });
161
+ } else {
162
+ paragraphs.slice(1).forEach(p => p.parentNode.removeChild(p));
163
+ }
164
+
165
+ parent.insertBefore(importedTable, firstParagraph);
166
+ if (!generateRedlines) {
167
+ parent.removeChild(firstParagraph);
168
+ }
169
+
170
+ log('[OxmlEngine] Text-to-table transformation complete');
171
+ return { oxml: serializer.serializeToString(workingDoc), hasChanges: true };
172
+ }
package/index.js ADDED
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Standalone reconciliation entrypoint (no Word JS API dependencies).
3
+ */
4
+
5
+ // Adapters
6
+ export { configureXmlProvider } from './adapters/xml-adapter.js';
7
+ export { configureLogger } from './adapters/logger.js';
8
+ export { setDefaultAuthor, getDefaultAuthor, setPlatform, getPlatform } from './adapters/config.js';
9
+
10
+ // Engine
11
+ import {
12
+ applyRedlineToOxml as applyRedlineToOxmlEngine,
13
+ sanitizeAiResponse,
14
+ parseOoxml,
15
+ serializeOoxml
16
+ } from './engine/oxml-engine.js';
17
+ import { parseTable as parseMarkdownTable } from './pipeline/pipeline.js';
18
+ import { wrapInDocumentFragment as wrapInDocumentFragmentShared } from './pipeline/serialization.js';
19
+ import {
20
+ buildSingleLineListStructuralFallbackPlan,
21
+ executeSingleLineListStructuralFallback,
22
+ resolveSingleLineListFallbackNumberingAction,
23
+ recordSingleLineListFallbackExplicitSequence,
24
+ clearSingleLineListFallbackExplicitSequence,
25
+ enforceListBindingOnParagraphNodes,
26
+ stripSingleLineListMarkerPrefix
27
+ } from './orchestration/list-structural-fallback.js';
28
+
29
+ /**
30
+ * Standalone-safe redline wrapper.
31
+ *
32
+ * In non-Word runtimes, the engine can return `{ useNativeApi: true, hasChanges: true }`
33
+ * without an OOXML payload for some format-only operations. Standalone callers cannot
34
+ * complete that native fallback path, so normalize to a no-op with warnings.
35
+ */
36
+ export async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}) {
37
+ const result = await applyRedlineToOxmlEngine(oxml, originalText, modifiedText, options);
38
+ if (result?.useNativeApi && typeof result?.oxml !== 'string') {
39
+ const existingWarnings = Array.isArray(result?.warnings) ? result.warnings : [];
40
+ return {
41
+ ...result,
42
+ oxml,
43
+ hasChanges: false,
44
+ warnings: [
45
+ ...existingWarnings,
46
+ 'Standalone mode cannot execute native Word API fallback for this operation.'
47
+ ]
48
+ };
49
+ }
50
+ return result;
51
+ }
52
+
53
+ /**
54
+ * Reconciles a Markdown table against an OOXML scope.
55
+ *
56
+ * This centralizes table-specific validation + reconciliation so Word add-in
57
+ * and browser modules can share the same entrypoint.
58
+ *
59
+ * @param {string} oxml - OOXML scope to reconcile (paragraph/range/table package)
60
+ * @param {string} originalText - Original visible text in that scope
61
+ * @param {string} markdownTable - Markdown table text
62
+ * @param {Object} [options={}] - Reconciliation options forwarded to applyRedlineToOxml
63
+ * @returns {Promise<{ oxml: string, hasChanges: boolean, warnings?: string[], isMarkdownTable: boolean, tableData?: Object }>}
64
+ */
65
+ export async function reconcileMarkdownTableOoxml(oxml, originalText, markdownTable, options = {}) {
66
+ const sourceOoxml = typeof oxml === 'string' ? oxml : '';
67
+ const tableText = typeof markdownTable === 'string' ? markdownTable : String(markdownTable || '');
68
+ let tableData;
69
+
70
+ try {
71
+ tableData = parseMarkdownTable(tableText);
72
+ } catch {
73
+ tableData = { headers: [], rows: [] };
74
+ }
75
+
76
+ const hasTableData = (tableData?.headers?.length || 0) > 0 || (tableData?.rows?.length || 0) > 0;
77
+ if (!hasTableData) {
78
+ return {
79
+ oxml: sourceOoxml,
80
+ hasChanges: false,
81
+ isMarkdownTable: false,
82
+ warnings: ['Could not parse Markdown table from input.']
83
+ };
84
+ }
85
+
86
+ const result = await applyRedlineToOxml(
87
+ sourceOoxml,
88
+ originalText || '',
89
+ tableText,
90
+ options
91
+ );
92
+
93
+ return {
94
+ ...result,
95
+ isMarkdownTable: true,
96
+ tableData
97
+ };
98
+ }
99
+
100
+ export { resolveParagraphRangeByRefs } from './core/paragraph-targeting.js';
101
+ export { inferTableReplacementParagraphBlock, isLikelyStructuredTableSourceParagraph } from './core/table-targeting.js';
102
+
103
+ /**
104
+ * Applies redline reconciliation, then forces single-line structural list
105
+ * conversion when the redline is a no-op on marker-prefixed list text.
106
+ *
107
+ * This is useful for inputs like `1. HEADER` where text diff is unchanged but
108
+ * OOXML should convert plain text markers into real Word list structure.
109
+ *
110
+ * @param {string} oxml - Original OOXML
111
+ * @param {string} originalText - Original visible text
112
+ * @param {string} modifiedText - Proposed modified text
113
+ * @param {Object} [options={}] - Reconciliation options
114
+ * @param {boolean} [options.listFallbackAllowExistingList=true] - Allow fallback even when paragraph is already list-bound
115
+ * @returns {Promise<{ oxml: string, hasChanges: boolean } & Record<string, any>>}
116
+ */
117
+ export async function applyRedlineToOxmlWithListFallback(oxml, originalText, modifiedText, options = {}) {
118
+ const allowExistingListForFallback = options.listFallbackAllowExistingList !== false;
119
+ const plan = buildSingleLineListStructuralFallbackPlan({
120
+ oxml,
121
+ originalText,
122
+ modifiedText,
123
+ allowExistingList: allowExistingListForFallback
124
+ });
125
+ const preferListFallback = options.preferListStructuralFallback !== false;
126
+ let preflightFallbackWarnings = [];
127
+
128
+ if (plan && preferListFallback) {
129
+ const fallbackResult = await executeSingleLineListStructuralFallback(plan, {
130
+ author: options.author,
131
+ generateRedlines: options.generateRedlines,
132
+ pipeline: options.listFallbackPipeline
133
+ });
134
+ if (fallbackResult?.hasChanges && fallbackResult?.oxml) {
135
+ const wrappedOxml = wrapInDocumentFragmentShared(fallbackResult.oxml, {
136
+ includeNumbering: fallbackResult.includeNumbering ?? true,
137
+ numberingXml: fallbackResult.numberingXml
138
+ });
139
+ const fallbackWarnings = Array.isArray(fallbackResult?.warnings) ? fallbackResult.warnings : [];
140
+ return {
141
+ oxml: wrappedOxml,
142
+ hasChanges: true,
143
+ warnings: fallbackWarnings,
144
+ listStructuralFallbackApplied: true,
145
+ listStructuralFallbackKey: fallbackResult.listStructuralFallbackKey || null,
146
+ listStructuralFallbackNumberingXml: fallbackResult.numberingXml || null
147
+ };
148
+ }
149
+ preflightFallbackWarnings = Array.isArray(fallbackResult?.warnings) ? fallbackResult.warnings : [];
150
+ }
151
+
152
+ const baseResult = await applyRedlineToOxml(oxml, originalText, modifiedText, options);
153
+
154
+ if (!plan) {
155
+ return {
156
+ ...baseResult,
157
+ warnings: [
158
+ ...(Array.isArray(baseResult?.warnings) ? baseResult.warnings : []),
159
+ ...preflightFallbackWarnings
160
+ ],
161
+ listStructuralFallbackApplied: false
162
+ };
163
+ }
164
+
165
+ if (preferListFallback) {
166
+ return {
167
+ ...baseResult,
168
+ warnings: [
169
+ ...(Array.isArray(baseResult?.warnings) ? baseResult.warnings : []),
170
+ ...preflightFallbackWarnings
171
+ ],
172
+ listStructuralFallbackApplied: false
173
+ };
174
+ }
175
+
176
+ if (baseResult?.hasChanges) {
177
+ return {
178
+ ...baseResult,
179
+ listStructuralFallbackApplied: false
180
+ };
181
+ }
182
+
183
+ const fallbackResult = await executeSingleLineListStructuralFallback(plan, {
184
+ author: options.author,
185
+ generateRedlines: options.generateRedlines,
186
+ pipeline: options.listFallbackPipeline
187
+ });
188
+ if (!fallbackResult?.hasChanges || !fallbackResult?.oxml) {
189
+ const existingWarnings = Array.isArray(baseResult?.warnings) ? baseResult.warnings : [];
190
+ const fallbackWarnings = Array.isArray(fallbackResult?.warnings) ? fallbackResult.warnings : [];
191
+ return {
192
+ ...baseResult,
193
+ warnings: [...existingWarnings, ...fallbackWarnings],
194
+ listStructuralFallbackApplied: false
195
+ };
196
+ }
197
+
198
+ const wrappedOxml = wrapInDocumentFragmentShared(fallbackResult.oxml, {
199
+ includeNumbering: fallbackResult.includeNumbering ?? true,
200
+ numberingXml: fallbackResult.numberingXml
201
+ });
202
+ const existingWarnings = Array.isArray(baseResult?.warnings) ? baseResult.warnings : [];
203
+ const fallbackWarnings = Array.isArray(fallbackResult?.warnings) ? fallbackResult.warnings : [];
204
+
205
+ return {
206
+ ...baseResult,
207
+ oxml: wrappedOxml,
208
+ hasChanges: true,
209
+ warnings: [...existingWarnings, ...preflightFallbackWarnings, ...fallbackWarnings],
210
+ listStructuralFallbackApplied: true,
211
+ listStructuralFallbackKey: fallbackResult.listStructuralFallbackKey || null,
212
+ listStructuralFallbackNumberingXml: fallbackResult.numberingXml || null
213
+ };
214
+ }
215
+
216
+ export { sanitizeAiResponse, parseOoxml, serializeOoxml };
217
+
218
+ export {
219
+ createDynamicNumberingIdState,
220
+ reserveNextNumberingId,
221
+ reserveNextNumberingIdPair,
222
+ overwriteParagraphNumIds,
223
+ extractFirstParagraphNumId,
224
+ buildExplicitDecimalMultilevelNumberingXml,
225
+ remapNumberingPayloadForDocument,
226
+ mergeNumberingXmlBySchemaOrder
227
+ } from './services/numbering-helpers.js';
228
+
229
+ // Pipeline components
230
+ export { ReconciliationPipeline } from './pipeline/pipeline.js';
231
+ export { ingestOoxml } from './pipeline/ingestion.js';
232
+ export { ingestWordOoxmlToPlainText, ingestWordOoxmlToMarkdown } from './pipeline/ingestion-export.js';
233
+ export { preprocessMarkdown } from './pipeline/markdown-processor.js';
234
+ export { serializeToOoxml, wrapInDocumentFragment } from './pipeline/serialization.js';
235
+
236
+ // Comment engine
237
+ export {
238
+ injectCommentsIntoOoxml,
239
+ injectCommentsIntoPackage,
240
+ buildCommentElement,
241
+ buildCommentsPartXml
242
+ } from './services/comment-engine.js';
243
+
244
+ // Formatting removal utilities
245
+ export {
246
+ removeFormattingFromRPr,
247
+ applyFormattingRemovalToOoxml,
248
+ applyHighlightToOoxml
249
+ } from './engine/formatting-removal.js';
250
+
251
+ // Table/list tools
252
+ export { generateTableOoxml } from './services/table-reconciliation.js';
253
+ export { NumberingService } from './services/numbering-service.js';
254
+ export {
255
+ parseXmlStrictStandalone,
256
+ getBodyElementFromDocument,
257
+ insertBodyElementBeforeSectPr,
258
+ normalizeBodySectionOrderStandalone,
259
+ sanitizeNestedParagraphsInTables,
260
+ getPackagePartName,
261
+ extractReplacementNodesFromOoxml,
262
+ ensureNumberingArtifactsInZip,
263
+ ensureCommentsArtifactsInZip,
264
+ validateDocxPackage
265
+ } from './services/standalone-docx-plumbing.js';
266
+ export { buildReconciliationPlan, RoutePlanKind, normalizeContentEscapesForRouting } from './orchestration/route-plan.js';
267
+ export { parseMarkdownListContent, hasListItems } from './orchestration/list-parsing.js';
268
+ export { buildListMarkdown, inferNumberingStyleFromMarker, normalizeListItemsWithLevels } from './orchestration/list-markdown.js';
269
+ export {
270
+ buildSingleLineListStructuralFallbackPlan,
271
+ executeSingleLineListStructuralFallback,
272
+ resolveSingleLineListFallbackNumberingAction,
273
+ recordSingleLineListFallbackExplicitSequence,
274
+ clearSingleLineListFallbackExplicitSequence,
275
+ enforceListBindingOnParagraphNodes,
276
+ stripSingleLineListMarkerPrefix
277
+ } from './orchestration/list-structural-fallback.js';
278
+
279
+ // Core types/constants
280
+ export { DiffOp, RunKind, ContainerKind, ContentType, NS_W, escapeXml } from './core/types.js';
281
+ export { extractParagraphIdFromOoxml } from './core/ooxml-identifiers.js';
282
+ export {
283
+ WORD_MAIN_NS,
284
+ getParagraphText,
285
+ getDocumentParagraphNodes,
286
+ normalizeWhitespaceForTargeting,
287
+ isMarkdownTableText,
288
+ parseParagraphReference,
289
+ stripLeadingParagraphMarker,
290
+ splitLeadingParagraphMarker,
291
+ findContainingWordElement,
292
+ findParagraphByReference,
293
+ findParagraphByStrictText,
294
+ findParagraphByBestTextMatch,
295
+ resolveTargetParagraph,
296
+ buildTargetReferenceSnapshot,
297
+ resolveTargetParagraphWithSnapshot
298
+ } from './core/paragraph-targeting.js';
299
+ export { synthesizeTableMarkdownFromMultilineCellEdit } from './core/table-targeting.js';
300
+ export {
301
+ getParagraphListInfo,
302
+ collectContiguousListParagraphBlock,
303
+ synthesizeExpandedListScopeEdit,
304
+ planListInsertionOnlyEdit,
305
+ stripRedundantLeadingListMarkers
306
+ } from './core/list-targeting.js';
307
+
308
+
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Shared list markdown construction/parsing helpers for command adapters.
3
+ */
4
+
5
+ /**
6
+ * Infers numbering style from a list marker.
7
+ *
8
+ * @param {string} marker - Marker text
9
+ * @returns {'decimal'|'lowerAlpha'|'upperAlpha'|'lowerRoman'|'upperRoman'}
10
+ */
11
+ export function inferNumberingStyleFromMarker(marker) {
12
+ const m = (marker || '').trim();
13
+ if (!m) return 'decimal';
14
+ if (/^\d+(?:\.\d+)*\.?$/.test(m) || /^\(\d+\)$/.test(m)) return 'decimal';
15
+ if (/^[ivxlcdm]+\.$/.test(m)) return 'lowerRoman';
16
+ if (/^[IVXLCDM]{2,}\.$/.test(m)) return 'upperRoman';
17
+ if (/^[a-z]\.$/.test(m)) return 'lowerAlpha';
18
+ if (/^[A-Z]\.$/.test(m)) return 'upperAlpha';
19
+ return 'decimal';
20
+ }
21
+
22
+ /**
23
+ * Builds list markdown from normalized item+level input.
24
+ *
25
+ * @param {Array<{ text: string, level: number }>} itemsWithLevels - Items with indentation levels
26
+ * @param {'bullet'|'numbered'} listType - List kind
27
+ * @param {'decimal'|'lowerAlpha'|'upperAlpha'|'lowerRoman'|'upperRoman'} numberingStyle - Number style for numbered lists
28
+ * @returns {string}
29
+ */
30
+ export function buildListMarkdown(itemsWithLevels, listType, numberingStyle) {
31
+ const levelCounters = new Map();
32
+ const lines = [];
33
+
34
+ for (const item of itemsWithLevels) {
35
+ const level = Math.max(0, Number(item?.level) || 0);
36
+ for (const key of Array.from(levelCounters.keys())) {
37
+ if (key > level) {
38
+ levelCounters.delete(key);
39
+ }
40
+ }
41
+
42
+ const nextCounter = (levelCounters.get(level) || 0) + 1;
43
+ levelCounters.set(level, nextCounter);
44
+
45
+ const marker = buildListMarker(nextCounter, listType, numberingStyle);
46
+ const indent = ' '.repeat(level * 4);
47
+ lines.push(`${indent}${marker} ${item.text || ''}`.trimEnd());
48
+ }
49
+
50
+ return lines.join('\n');
51
+ }
52
+
53
+ /**
54
+ * Normalizes list item text/indentation into markdown-ready model.
55
+ *
56
+ * @param {Array<string>} rawItems - Raw tool items
57
+ * @param {Object} [options={}] - Normalize options
58
+ * @param {number} [options.indentSpaces=4] - Spaces per indent level
59
+ * @returns {Array<{ text: string, level: number, removedMarker: string|null }>}
60
+ */
61
+ export function normalizeListItemsWithLevels(rawItems, options = {}) {
62
+ const indentSpaces = Math.max(1, Number(options.indentSpaces) || 4);
63
+ const markersRegex = /^((?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|\d+\.|[ivxlcIVXLC]+\.|[-*•])\s*)/;
64
+
65
+ return (rawItems || []).map((rawItem) => {
66
+ const item = String(rawItem ?? '');
67
+ const indentMatch = item.match(/^(\s*)/);
68
+ const indentSize = indentMatch ? indentMatch[1].length : 0;
69
+ const level = Math.floor(indentSize / indentSpaces);
70
+
71
+ let stripped = item.trim();
72
+ let removedMarker = null;
73
+ const markerMatch = stripped.match(markersRegex);
74
+ if (markerMatch) {
75
+ removedMarker = markerMatch[1].trim() || null;
76
+ stripped = stripped.replace(markersRegex, '');
77
+ }
78
+
79
+ return {
80
+ text: stripped.trim(),
81
+ level,
82
+ removedMarker
83
+ };
84
+ });
85
+ }
86
+
87
+ function buildListMarker(counter, listType, numberingStyle) {
88
+ if (listType === 'bullet') return '-';
89
+
90
+ switch (numberingStyle) {
91
+ case 'lowerAlpha':
92
+ return `${toAlphaSequence(counter, false)}.`;
93
+ case 'upperAlpha':
94
+ return `${toAlphaSequence(counter, true)}.`;
95
+ case 'lowerRoman':
96
+ return `${toRoman(counter, false)}.`;
97
+ case 'upperRoman':
98
+ return `${toRoman(counter, true)}.`;
99
+ case 'decimal':
100
+ default:
101
+ return `${counter}.`;
102
+ }
103
+ }
104
+
105
+ function toAlphaSequence(value, upper = false) {
106
+ let n = Math.max(1, Number(value) || 1);
107
+ let out = '';
108
+ while (n > 0) {
109
+ n -= 1;
110
+ out = String.fromCharCode(97 + (n % 26)) + out;
111
+ n = Math.floor(n / 26);
112
+ }
113
+ return upper ? out.toUpperCase() : out;
114
+ }
115
+
116
+ function toRoman(value, upper = false) {
117
+ let n = Math.max(1, Number(value) || 1);
118
+ const romanPairs = [
119
+ [1000, 'M'],
120
+ [900, 'CM'],
121
+ [500, 'D'],
122
+ [400, 'CD'],
123
+ [100, 'C'],
124
+ [90, 'XC'],
125
+ [50, 'L'],
126
+ [40, 'XL'],
127
+ [10, 'X'],
128
+ [9, 'IX'],
129
+ [5, 'V'],
130
+ [4, 'IV'],
131
+ [1, 'I']
132
+ ];
133
+ let out = '';
134
+ for (const [num, sym] of romanPairs) {
135
+ while (n >= num) {
136
+ out += sym;
137
+ n -= num;
138
+ }
139
+ }
140
+ return upper ? out : out.toLowerCase();
141
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared markdown list parsing for command adapters.
3
+ *
4
+ * Keeps command-layer list parsing aligned with reconciliation marker logic.
5
+ */
6
+
7
+ import { matchListMarker, stripListMarker } from '../pipeline/list-markers.js';
8
+
9
+ /**
10
+ * Parses markdown list-like content into structured items.
11
+ *
12
+ * Output shape is compatible with command-layer expectations from `parseMarkdownList`.
13
+ *
14
+ * @param {string} content - Raw markdown/text content
15
+ * @returns {{ type: 'numbered'|'bullet'|'text', items: Array<{ type: 'numbered'|'bullet'|'text', level: number, text: string, marker?: string }> }|null}
16
+ */
17
+ export function parseMarkdownListContent(content) {
18
+ if (!content) return null;
19
+
20
+ const normalized = String(content).trim();
21
+ if (!normalized) return null;
22
+
23
+ const lines = normalized.split('\n');
24
+ const items = [];
25
+
26
+ for (const line of lines) {
27
+ if (!line.trim()) continue;
28
+
29
+ const markerMatch = matchListMarker(line, { allowZeroSpaceAfterMarker: false });
30
+ if (markerMatch) {
31
+ const indent = markerMatch[1] || '';
32
+ const marker = markerMatch[2].trim();
33
+ const text = stripListMarker(line, { allowZeroSpaceAfterMarker: false }).trim();
34
+ const level = Math.floor(indent.length / 2);
35
+ const isBullet = /^[-*+\u2022]$/.test(marker);
36
+
37
+ items.push({
38
+ type: isBullet ? 'bullet' : 'numbered',
39
+ level,
40
+ text,
41
+ marker
42
+ });
43
+ continue;
44
+ }
45
+
46
+ items.push({
47
+ type: 'text',
48
+ level: 0,
49
+ text: line.trim()
50
+ });
51
+ }
52
+
53
+ if (items.length === 0) return null;
54
+
55
+ const hasNumbered = items.some(item => item.type === 'numbered');
56
+ const hasBullet = items.some(item => item.type === 'bullet');
57
+
58
+ return {
59
+ type: hasNumbered ? 'numbered' : (hasBullet ? 'bullet' : 'text'),
60
+ items
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Checks whether parsed list data includes at least one real list item.
66
+ *
67
+ * @param {{ items?: Array<{ type?: string }> }|null} parsedListData - Parsed list data
68
+ * @returns {boolean}
69
+ */
70
+ export function hasListItems(parsedListData) {
71
+ if (!parsedListData || !Array.isArray(parsedListData.items)) return false;
72
+ return parsedListData.items.some(item => item?.type === 'numbered' || item?.type === 'bullet');
73
+ }