@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.
- package/AGENTS.md +176 -0
- package/ARCHITECTURE.md +121 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/adapters/config.js +43 -0
- package/adapters/logger.js +89 -0
- package/adapters/xml-adapter.js +74 -0
- package/core/list-targeting.js +398 -0
- package/core/ooxml-identifiers.js +15 -0
- package/core/paragraph-offset-policy.js +50 -0
- package/core/paragraph-targeting.js +501 -0
- package/core/table-targeting.js +233 -0
- package/core/types.js +204 -0
- package/core/xml-query.js +99 -0
- package/dist/docx-redline-js.esm.js +8801 -0
- package/dist/docx-redline-js.esm.js.map +7 -0
- package/dist/docx-redline-js.esm.min.js +195 -0
- package/dist/docx-redline-js.esm.min.js.map +7 -0
- package/engine/format-application.js +358 -0
- package/engine/format-extraction.js +232 -0
- package/engine/format-paragraph-targeting.js +208 -0
- package/engine/format-span-application.js +178 -0
- package/engine/formatting-removal.js +330 -0
- package/engine/oxml-engine.js +279 -0
- package/engine/reconstruction-mapper.js +270 -0
- package/engine/reconstruction-mode.js +38 -0
- package/engine/reconstruction-writer.js +276 -0
- package/engine/rpr-helpers.js +194 -0
- package/engine/run-builders.js +235 -0
- package/engine/surgical-mode.js +520 -0
- package/engine/table-cell-context.js +151 -0
- package/engine/table-mode.js +172 -0
- package/index.js +308 -0
- package/orchestration/list-markdown.js +141 -0
- package/orchestration/list-parsing.js +73 -0
- package/orchestration/list-structural-fallback.js +530 -0
- package/orchestration/redline-operation-converter.js +141 -0
- package/orchestration/route-plan.js +160 -0
- package/package.json +76 -0
- package/pipeline/content-analysis.js +107 -0
- package/pipeline/diff-engine.js +204 -0
- package/pipeline/ingestion-export.js +255 -0
- package/pipeline/ingestion-paragraph.js +351 -0
- package/pipeline/ingestion-table.js +169 -0
- package/pipeline/ingestion-xml.js +39 -0
- package/pipeline/ingestion.js +8 -0
- package/pipeline/list-generation.js +280 -0
- package/pipeline/list-markers.js +77 -0
- package/pipeline/markdown-processor.js +160 -0
- package/pipeline/patching.js +408 -0
- package/pipeline/pipeline.js +326 -0
- package/pipeline/serialization.js +395 -0
- package/services/browser-demo-prompt-context.js +345 -0
- package/services/comment-builders.js +60 -0
- package/services/comment-engine.js +248 -0
- package/services/comment-locator.js +197 -0
- package/services/comment-package.js +113 -0
- package/services/numbering-helpers.js +416 -0
- package/services/numbering-service.js +290 -0
- package/services/package-builder.js +147 -0
- package/services/standalone-docx-plumbing.js +443 -0
- package/services/standalone-operation-runner.js +1169 -0
- package/services/table-reconciliation.js +344 -0
- 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
|
+
}
|