@richscripts/richtexteditor 2.4.0 → 2.4.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.
Files changed (58) hide show
  1. package/migration/README.md +100 -0
  2. package/migration/cli.js +294 -0
  3. package/migration/index.d.ts +18 -0
  4. package/migration/index.js +242 -0
  5. package/migration/report-schema-v2.json +127 -0
  6. package/package.json +11 -1
  7. package/richtexteditor/CHANGELOG.md +326 -1
  8. package/richtexteditor/all_plugins.js +14959 -4832
  9. package/richtexteditor/all_plugins.min.js +62 -42
  10. package/richtexteditor/plugins/accessibilitychecker.js +8 -7
  11. package/richtexteditor/plugins/aitoolkit.css +17 -8
  12. package/richtexteditor/plugins/aitoolkit.js +8 -8
  13. package/richtexteditor/plugins/all_plugins.js +14959 -4832
  14. package/richtexteditor/plugins/all_plugins.min.js +62 -42
  15. package/richtexteditor/plugins/autocorrect.js +353 -0
  16. package/richtexteditor/plugins/bookmarkcard.js +12 -1
  17. package/richtexteditor/plugins/changecase.js +238 -0
  18. package/richtexteditor/plugins/comments.js +15 -28
  19. package/richtexteditor/plugins/contentminimap.js +6 -5
  20. package/richtexteditor/plugins/crossreference.js +408 -0
  21. package/richtexteditor/plugins/documentimport.js +915 -21
  22. package/richtexteditor/plugins/documentoutline.js +5 -4
  23. package/richtexteditor/plugins/docxexport.js +960 -0
  24. package/richtexteditor/plugins/draghandle.js +309 -0
  25. package/richtexteditor/plugins/footnotes.js +398 -0
  26. package/richtexteditor/plugins/formatpainter.js +298 -0
  27. package/richtexteditor/plugins/formattingmarks.js +246 -0
  28. package/richtexteditor/plugins/html2pdf.js +108 -4
  29. package/richtexteditor/plugins/imageeditor.js +37 -7
  30. package/richtexteditor/plugins/insertgallery.js +553 -170
  31. package/richtexteditor/plugins/keyboarda11y.js +316 -0
  32. package/richtexteditor/plugins/linenumbers.js +352 -0
  33. package/richtexteditor/plugins/linkchecker.js +396 -0
  34. package/richtexteditor/plugins/mentionplugin.js +4 -3
  35. package/richtexteditor/plugins/mergefields.js +328 -0
  36. package/richtexteditor/plugins/multilevellist.js +137 -0
  37. package/richtexteditor/plugins/pagination.js +505 -0
  38. package/richtexteditor/plugins/pdfexport.js +1137 -0
  39. package/richtexteditor/plugins/permanentpen.js +211 -0
  40. package/richtexteditor/plugins/revisionhistory.js +47 -34
  41. package/richtexteditor/plugins/rtlui.js +150 -0
  42. package/richtexteditor/plugins/sanitizer.js +486 -0
  43. package/richtexteditor/plugins/slashcommand.js +142 -17
  44. package/richtexteditor/plugins/spellcheck.js +6 -3
  45. package/richtexteditor/plugins/tableofcontents.js +353 -0
  46. package/richtexteditor/plugins/tabletools.js +342 -0
  47. package/richtexteditor/plugins/textdirection.js +313 -0
  48. package/richtexteditor/plugins/trackedchanges.js +37 -6
  49. package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
  50. package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
  51. package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
  52. package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
  53. package/richtexteditor/plugins/watermark.js +214 -0
  54. package/richtexteditor/plugins/wordexport.js +0 -0
  55. package/richtexteditor/plugins/yjscollab.js +49 -11
  56. package/richtexteditor/rte-config.js +44 -44
  57. package/richtexteditor/rte.js +91 -63
  58. package/richtexteditor/rte_theme_default.css +462 -384
@@ -0,0 +1,100 @@
1
+ # Migration helpers
2
+
3
+ `@richscripts/richtexteditor/migration` is a server-safe CommonJS entry point for moving common configuration and content from TinyMCE, CKEditor 5, Froala, Quill, Tiptap, or Lexical.
4
+
5
+ ```js
6
+ const { createMigrationReport, migrateConfig, migrateEditor } = require("@richscripts/richtexteditor/migration");
7
+
8
+ const plan = migrateConfig("tinymce", legacyConfig);
9
+ console.table(plan.unmapped);
10
+
11
+ const result = migrateEditor("tinymce", legacyEditor, richTextEditor);
12
+ console.table(result.warnings);
13
+
14
+ const report = createMigrationReport("tinymce", { config: legacyConfig, content: legacyHtml });
15
+ console.table(report.blockers);
16
+ if (!report.readyForCutover) process.exitCode = 1;
17
+ ```
18
+
19
+ - HTML-native editors transfer their HTML for RichTextEditor's normal loading pipeline.
20
+ - Quill Delta, Tiptap JSON, and Lexical JSON receive semantic HTML conversion for common blocks and marks, including mixed multi-level ordered/bullet lists and source checklist state.
21
+ - TinyMCE, CKEditor, and Froala HTML passes through without schema rewriting, while private runtime bookkeeping attributes (`data-mce-*`, `data-ck-*`, CKEditor list IDs, and `data-fr-*`) are removed before handoff.
22
+ - Unknown toolbar items, embeds, nodes, and marks are reported. They are not silently presented as migrated.
23
+ - `createMigrationReport` combines configuration and content findings into blockers, warnings, and a stable manual-review checklist for a pre-cutover inventory.
24
+ - Custom plugins, schemas, collaboration state, comments, tracked changes, uploads, permissions, and application data remain explicit integration work.
25
+
26
+ Always run representative production documents through both editors and compare saved output before cutover.
27
+
28
+ ## Custom structured-model converters
29
+
30
+ Use explicit programmatic converters to resolve inventoried Quill embeds or Tiptap/Lexical nodes and marks without forking the adapter:
31
+
32
+ ```js
33
+ const report = createMigrationReport("quill", {
34
+ content: legacyDelta,
35
+ converters: {
36
+ quill: {
37
+ embeds: {
38
+ mention(value) {
39
+ const id = escapeAttribute(value.id);
40
+ const label = escapeHtml(value.label);
41
+ return `<span data-mention="${id}">@${label}</span>`;
42
+ }
43
+ }
44
+ }
45
+ }
46
+ });
47
+
48
+ console.table(report.appliedConverters);
49
+ ```
50
+
51
+ Registries support `quill.embeds`, `tiptap.nodes`, `tiptap.marks`, and `lexical.nodes`. A converter returns an HTML string or `{ html, warnings }`. Returning `null` deliberately falls through to the normal unsupported-content blocker. Successful applications are counted in `appliedConverters`; each entry also includes stable source-model `paths` and the converter receives the current `context.path`.
52
+
53
+ Converter output is trusted application code and is not sanitized by the migration helper. Escape external values, sanitize generated HTML under your own policy, and keep representative-output review in the cutover gate.
54
+
55
+ ## File and batch inventory CLI
56
+
57
+ The package also installs `rte-migration-report`. A single-document inventory accepts an HTML file for TinyMCE, CKEditor, or Froala, and a JSON export for Quill, Tiptap, or Lexical:
58
+
59
+ ```sh
60
+ rte-migration-report --source quill --config quill-config.json --content article.delta.json --output report.json
61
+ ```
62
+
63
+ For a batch, create a manifest. File references are resolved relative to the manifest, so the inventory is portable in CI:
64
+
65
+ ```json
66
+ {
67
+ "entries": [
68
+ { "id": "terms", "source": "ckeditor", "configFile": "config.json", "contentFile": "terms.html" },
69
+ { "id": "guide", "source": "tiptap", "contentFile": "guide.json" }
70
+ ]
71
+ }
72
+ ```
73
+
74
+ ```sh
75
+ rte-migration-report --input migration-inventory.json --output migration-report.json
76
+ ```
77
+
78
+ To reuse the programmatic converter registry in single-file or batch CI runs, opt into a local CommonJS module:
79
+
80
+ ```sh
81
+ rte-migration-report --input migration-inventory.json --converters ./migration-converters.cjs --converter-sha256 "$REVIEWED_SHA256" --output migration-report.json
82
+ ```
83
+
84
+ The module must export the same `quill`/`tiptap`/`lexical` registry object documented above. The CLI resolves this path from its invoking working directory and validates every registry group and converter function before reading the inventory. This flag executes local code with the permissions of the CLI process; use only a reviewed, trusted module from your repository. Pin reviewed bytes with `--converter-sha256 <64-character digest>`: a malformed value or content mismatch fails before the module can execute. Without `--converters`, the CLI remains data-only and preserves every unsupported-content blocker.
85
+
86
+ CLI artifacts use report schema version `2` and include a stable `generator` object with the producing `@richscripts/richtexteditor` package version. Both single-document and batch output include `converterModule`: `null` for the default data-only path, or a non-sensitive file label, SHA-256 digest, and `hashVerifiedBeforeExecution` flag. Store the generator version and digest with CI artifacts and review schema-version changes before consuming new report fields. The label intentionally omits the local absolute path; the digest proves byte identity, not that the module is safe or correct.
87
+
88
+ Reports intentionally omit a current-time field, so identical inputs, package code, and converter bytes serialize identically. Let the CI artifact store provide creation/retention timestamps; this keeps readiness diffs meaningful and reproducible.
89
+
90
+ Print the packaged Draft 2020-12 JSON Schema with `rte-migration-report --print-schema`. The schema covers both single-document and batch artifacts, including generator identity, converter provenance, structured findings, aggregate totals, and nested document reports. The command is standalone so schema discovery cannot accidentally execute converters or read migration inputs.
91
+
92
+ Validate a stored artifact with `rte-migration-report --validate-report ./migration-report.json`. This dependency-free mode checks schema/generator compatibility, document field shapes, readiness-versus-blocker consistency, and batch totals/unsupported inventories against the nested reports. It does not load converter modules or source migration files. A valid artifact exits `0` and emits compact JSON containing its exact-byte SHA-256; malformed, incompatible, or internally inconsistent artifacts exit `1` with the failed invariant.
93
+
94
+ For promotion gates, add `--report-sha256 <64-character digest>`. The CLI compares the expected digest with the stored bytes before JSON parsing or validation and rejects mismatches. The output records `hashVerifiedBeforeValidation` so downstream jobs can distinguish a computed digest from an enforced pin. SHA-256 establishes byte identity and detects unintended artifact changes; it does not identify or authenticate a signer, and validation still does not replace content-fidelity acceptance.
95
+
96
+ To gate only newly introduced migration debt, add `--baseline-report ./accepted-baseline.json` to `--validate-report`. Both artifacts are validated first. Comparison output lists new blocker messages and new/increased unsupported findings by document ID, source, kind, name, added count, and exact new source paths. Exit `0` means no new detected debt; exit `2` means regressions were found; exit `1` means an artifact or option was invalid. Existing accepted blockers remain visible in the reports, and a regression-free comparison is not production acceptance.
97
+
98
+ Each document report includes a structured `unsupportedContent` inventory whose entries identify the unsupported `kind` (`node`, `mark`, or `embed`), name, occurrence count, and stable source-model paths such as `ops[4].insert.mention` or `$.content[2].marks[0]`. Batch output also provides document totals, `summary.unsupportedByDocument`, and source/kind/name rollups in `summary.unsupportedTotals`, including affected-document counts. CI and migration owners can locate, size, and assign custom conversion work without parsing warning text.
99
+
100
+ The command exits `0` when every report has no automated blocker, `2` when blockers are present, and `1` for invalid input or I/O errors. Use `--allow-blockers` when generating an informational artifact should not fail CI. A zero exit code is not production acceptance: every report retains the manual-review checklist.
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ var fs = require("node:fs");
5
+ var path = require("node:path");
6
+ var crypto = require("node:crypto");
7
+ var migration = require("./index.js");
8
+ var packageMetadata = require("../package.json");
9
+
10
+ var REPORT_SCHEMA_VERSION = 2;
11
+ var REPORT_GENERATOR = { name: "@richscripts/richtexteditor", version: packageMetadata.version };
12
+
13
+ function usage() {
14
+ return [
15
+ "Usage:",
16
+ " rte-migration-report --print-schema",
17
+ " rte-migration-report --validate-report <report.json> [--report-sha256 <digest>] [--baseline-report <baseline.json>]",
18
+ " rte-migration-report --source <editor> [--config <file>] [--content <file>] [--converters <module.cjs> --converter-sha256 <digest>] [--output <file>]",
19
+ " rte-migration-report --input <manifest.json> [--converters <module.cjs> --converter-sha256 <digest>] [--output <file>] [--allow-blockers]",
20
+ "",
21
+ "Sources: " + migration.SOURCES.join(", "),
22
+ "Warning: --converters executes the specified local CommonJS module as trusted code.",
23
+ "Pin reviewed converter bytes with --converter-sha256; a mismatch is rejected before code execution.",
24
+ "Exit codes: 0 ready, 2 blockers found, 1 invalid input or I/O failure."
25
+ ].join("\n");
26
+ }
27
+
28
+ function parseArgs(argv) {
29
+ var options = { allowBlockers: false };
30
+ for (var index = 0; index < argv.length; index += 1) {
31
+ var arg = argv[index];
32
+ if (arg === "--help" || arg === "-h") options.help = true;
33
+ else if (arg === "--print-schema") options.printSchema = true;
34
+ else if (arg === "--allow-blockers") options.allowBlockers = true;
35
+ else if (["--source", "--config", "--content", "--input", "--output", "--converters", "--converter-sha256", "--validate-report", "--report-sha256", "--baseline-report"].indexOf(arg) >= 0) {
36
+ if (!argv[index + 1] || /^--/.test(argv[index + 1])) throw new TypeError(arg + " requires a value");
37
+ options[arg === "--converter-sha256" ? "converterSha256" : arg === "--validate-report" ? "validateReport" : arg === "--report-sha256" ? "reportSha256" : arg === "--baseline-report" ? "baselineReport" : arg.slice(2)] = argv[index += 1];
38
+ } else throw new TypeError("Unknown option: " + arg);
39
+ }
40
+ return options;
41
+ }
42
+
43
+ function readJson(filePath, label) {
44
+ try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
45
+ catch (error) { throw new Error("Unable to read " + label + " JSON at " + filePath + ": " + error.message); }
46
+ }
47
+
48
+ function readContent(filePath, source) {
49
+ var text;
50
+ try { text = fs.readFileSync(filePath, "utf8"); }
51
+ catch (error) { throw new Error("Unable to read content at " + filePath + ": " + error.message); }
52
+ return ["tinymce", "ckeditor", "ckeditor5", "froala"].indexOf(String(source).toLowerCase()) >= 0 ? text : (function () {
53
+ try { return JSON.parse(text); }
54
+ catch (error) { throw new Error("Structured content must be valid JSON at " + filePath + ": " + error.message); }
55
+ }());
56
+ }
57
+
58
+ function loadConverters(filePath, expectedSha256) {
59
+ var loaded, bytes, loadedBytes, digest, allowed = { quill: ["embeds"], tiptap: ["nodes", "marks"], lexical: ["nodes"] };
60
+ try { bytes = fs.readFileSync(filePath); }
61
+ catch (error) { throw new Error("Unable to read trusted converter module at " + filePath + ": " + error.message); }
62
+ digest = crypto.createHash("sha256").update(bytes).digest("hex");
63
+ if (expectedSha256 != null) {
64
+ expectedSha256 = String(expectedSha256).toLowerCase();
65
+ if (!/^[a-f0-9]{64}$/.test(expectedSha256)) throw new TypeError("--converter-sha256 must be a 64-character hexadecimal SHA-256 digest");
66
+ if (digest !== expectedSha256) throw new Error("Trusted converter module SHA-256 mismatch; refusing to execute " + path.basename(filePath));
67
+ }
68
+ try { loaded = require(filePath); }
69
+ catch (error) { throw new Error("Unable to load trusted converter module at " + filePath + ": " + error.message); }
70
+ try { loadedBytes = fs.readFileSync(filePath); }
71
+ catch (error) { throw new Error("Unable to verify trusted converter module at " + filePath + ": " + error.message); }
72
+ if (!bytes.equals(loadedBytes)) throw new Error("Trusted converter module changed while it was loading; rerun with a stable file: " + filePath);
73
+ if (loaded && loaded.__esModule && loaded.default) loaded = loaded.default;
74
+ if (!loaded || typeof loaded !== "object" || Array.isArray(loaded)) throw new TypeError("Trusted converter module must export a converter registry object");
75
+ Object.keys(loaded).forEach(function (source) {
76
+ if (!allowed[source]) throw new TypeError("Trusted converter module has unsupported source registry: " + source);
77
+ if (!loaded[source] || typeof loaded[source] !== "object" || Array.isArray(loaded[source])) throw new TypeError("Trusted converter source registry must be an object: " + source);
78
+ Object.keys(loaded[source]).forEach(function (group) {
79
+ if (allowed[source].indexOf(group) < 0) throw new TypeError("Trusted converter module has unsupported registry group: " + source + "." + group);
80
+ var registry = loaded[source][group];
81
+ if (!registry || typeof registry !== "object" || Array.isArray(registry)) throw new TypeError("Trusted converter registry must be an object: " + source + "." + group);
82
+ Object.keys(registry).forEach(function (name) { if (typeof registry[name] !== "function") throw new TypeError("Trusted converter must be a function: " + source + "." + group + "." + name); });
83
+ });
84
+ });
85
+ return {
86
+ converters: loaded,
87
+ provenance: {
88
+ label: path.basename(filePath),
89
+ sha256: digest,
90
+ hashVerifiedBeforeExecution: expectedSha256 != null
91
+ }
92
+ };
93
+ }
94
+
95
+ function resolveEntry(entry, baseDir, index, converters) {
96
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new TypeError("Inventory entry " + index + " must be an object");
97
+ if (!entry.source) throw new TypeError("Inventory entry " + index + " requires source");
98
+ var config = entry.config || {};
99
+ var content = entry.content;
100
+ if (entry.configFile) config = readJson(path.resolve(baseDir, entry.configFile), "config");
101
+ if (entry.contentFile) content = readContent(path.resolve(baseDir, entry.contentFile), entry.source);
102
+ var report = migration.createMigrationReport(entry.source, { config: config, content: content, converters: converters });
103
+ if (entry.id != null) report.id = String(entry.id);
104
+ return report;
105
+ }
106
+
107
+ function assertReport(condition, message) {
108
+ if (!condition) throw new TypeError("Invalid migration report: " + message);
109
+ }
110
+
111
+ function validateFindings(items, label) {
112
+ assertReport(Array.isArray(items), label + " must be an array");
113
+ items.forEach(function (item, index) {
114
+ var prefix = label + "[" + index + "]";
115
+ assertReport(item && typeof item === "object" && !Array.isArray(item), prefix + " must be an object");
116
+ assertReport(["embed", "node", "mark"].indexOf(item.kind) >= 0, prefix + ".kind is invalid");
117
+ assertReport(typeof item.name === "string" && item.name.length > 0, prefix + ".name is required");
118
+ assertReport(Number.isInteger(item.count) && item.count > 0, prefix + ".count must be a positive integer");
119
+ assertReport(Array.isArray(item.paths) && item.paths.every(function (value) { return typeof value === "string"; }), prefix + ".paths must be a string array");
120
+ });
121
+ }
122
+
123
+ function validateDocumentReport(report, label) {
124
+ assertReport(report && typeof report === "object" && !Array.isArray(report), label + " must be an object");
125
+ assertReport(typeof report.source === "string" && report.source.length > 0, label + ".source is required");
126
+ assertReport(typeof report.readyForCutover === "boolean", label + ".readyForCutover must be boolean");
127
+ assertReport(report.config && typeof report.config === "object" && !Array.isArray(report.config), label + ".config must be an object");
128
+ assertReport(typeof report.html === "string", label + ".html must be a string");
129
+ ["mappedCommands", "unmapped", "blockers", "warnings", "manualReview"].forEach(function (key) {
130
+ assertReport(Array.isArray(report[key]) && report[key].every(function (value) { return typeof value === "string"; }), label + "." + key + " must be a string array");
131
+ });
132
+ validateFindings(report.unsupportedContent, label + ".unsupportedContent");
133
+ validateFindings(report.appliedConverters, label + ".appliedConverters");
134
+ assertReport(report.readyForCutover === (report.blockers.length === 0), label + ".readyForCutover must agree with blockers");
135
+ }
136
+
137
+ function validateStoredReport(report) {
138
+ assertReport(report && typeof report === "object" && !Array.isArray(report), "root must be an object");
139
+ assertReport(report.schemaVersion === REPORT_SCHEMA_VERSION, "schemaVersion must be " + REPORT_SCHEMA_VERSION);
140
+ assertReport(report.generator && report.generator.name === REPORT_GENERATOR.name && typeof report.generator.version === "string" && report.generator.version.length > 0, "generator identity is invalid");
141
+ assertReport(report.converterModule === null || (report.converterModule && typeof report.converterModule.label === "string" && /^[a-f0-9]{64}$/.test(report.converterModule.sha256) && typeof report.converterModule.hashVerifiedBeforeExecution === "boolean"), "converterModule provenance is invalid");
142
+ if (!Array.isArray(report.reports)) {
143
+ validateDocumentReport(report, "report");
144
+ return report;
145
+ }
146
+ assertReport(report.reports.length > 0, "reports must not be empty");
147
+ report.reports.forEach(function (item, index) { validateDocumentReport(item, "reports[" + index + "]"); });
148
+ assertReport(report.summary && typeof report.summary === "object", "summary is required for batch reports");
149
+ var ready = report.reports.filter(function (item) { return item.readyForCutover; }).length;
150
+ assertReport(report.summary.documents === report.reports.length, "summary.documents does not match reports.length");
151
+ assertReport(report.summary.readyDocuments === ready, "summary.readyDocuments does not match reports");
152
+ assertReport(report.summary.blockedDocuments === report.reports.length - ready, "summary.blockedDocuments does not match reports");
153
+ assertReport(report.readyForCutover === (ready === report.reports.length), "readyForCutover does not match batch reports");
154
+ assertReport(Array.isArray(report.summary.unsupportedTotals), "summary.unsupportedTotals must be an array");
155
+ assertReport(Array.isArray(report.summary.unsupportedByDocument), "summary.unsupportedByDocument must be an array");
156
+ var expectedByDocument = report.reports.filter(function (item) { return item.unsupportedContent.length; }).map(function (item, index) {
157
+ return { id: item.id == null ? String(index) : item.id, source: item.source, items: item.unsupportedContent };
158
+ });
159
+ assertReport(JSON.stringify(report.summary.unsupportedByDocument) === JSON.stringify(expectedByDocument), "summary.unsupportedByDocument does not match reports");
160
+ var expectedTotals = [];
161
+ report.reports.forEach(function (item) {
162
+ item.unsupportedContent.forEach(function (finding) {
163
+ var total = expectedTotals.filter(function (candidate) { return candidate.source === item.source && candidate.kind === finding.kind && candidate.name === finding.name; })[0];
164
+ if (total) { total.count += finding.count; total.documents += 1; }
165
+ else expectedTotals.push({ source: item.source, kind: finding.kind, name: finding.name, count: finding.count, documents: 1 });
166
+ });
167
+ });
168
+ expectedTotals.sort(function (left, right) { return (left.source + "\0" + left.kind + "\0" + left.name).localeCompare(right.source + "\0" + right.kind + "\0" + right.name); });
169
+ assertReport(JSON.stringify(report.summary.unsupportedTotals) === JSON.stringify(expectedTotals), "summary.unsupportedTotals does not match reports");
170
+ return report;
171
+ }
172
+
173
+ function reportDocuments(report) { return Array.isArray(report.reports) ? report.reports : [report]; }
174
+
175
+ function compareStoredReports(baseline, current) {
176
+ validateStoredReport(baseline);
177
+ validateStoredReport(current);
178
+ var baselineDocs = reportDocuments(baseline), currentDocs = reportDocuments(current), baselineBlockers = {}, baselineFindings = {};
179
+ baselineDocs.forEach(function (report, index) {
180
+ var id = report.id == null ? String(index) : String(report.id);
181
+ report.blockers.forEach(function (message) { baselineBlockers[id + "\0" + report.source + "\0" + message] = true; });
182
+ report.unsupportedContent.forEach(function (item) { baselineFindings[id + "\0" + report.source + "\0" + item.kind + "\0" + item.name] = item; });
183
+ });
184
+ var newBlockers = [], newUnsupported = [];
185
+ currentDocs.forEach(function (report, index) {
186
+ var id = report.id == null ? String(index) : String(report.id);
187
+ report.blockers.forEach(function (message) {
188
+ if (!baselineBlockers[id + "\0" + report.source + "\0" + message]) newBlockers.push({ id: id, source: report.source, message: message });
189
+ });
190
+ report.unsupportedContent.forEach(function (item) {
191
+ var old = baselineFindings[id + "\0" + report.source + "\0" + item.kind + "\0" + item.name], oldPaths = old ? old.paths : [];
192
+ var paths = item.paths.filter(function (value) { return oldPaths.indexOf(value) < 0; });
193
+ var addedCount = Math.max(0, item.count - (old ? old.count : 0));
194
+ if (addedCount || paths.length) newUnsupported.push({ id: id, source: report.source, kind: item.kind, name: item.name, addedCount: addedCount, paths: paths });
195
+ });
196
+ });
197
+ return { regressionFree: newBlockers.length === 0 && newUnsupported.length === 0, newBlockers: newBlockers, newUnsupported: newUnsupported };
198
+ }
199
+
200
+ function createOutput(options, cwd) {
201
+ if (options.input && options.source) throw new TypeError("Use either --input or --source, not both");
202
+ if (options.converterSha256 && !options.converters) throw new TypeError("--converter-sha256 requires --converters");
203
+ var loadedConverters = options.converters ? loadConverters(path.resolve(cwd, options.converters), options.converterSha256) : undefined;
204
+ var converters = loadedConverters && loadedConverters.converters;
205
+ if (options.input) {
206
+ var manifestPath = path.resolve(cwd, options.input);
207
+ var manifest = readJson(manifestPath, "manifest");
208
+ var entries = Array.isArray(manifest) ? manifest : manifest.entries;
209
+ if (!Array.isArray(entries) || !entries.length) throw new TypeError("Manifest must be a non-empty array or contain a non-empty entries array");
210
+ var reports = entries.map(function (entry, index) { return resolveEntry(entry, path.dirname(manifestPath), index, converters); });
211
+ var unsupportedTotals = [];
212
+ reports.forEach(function (report) {
213
+ report.unsupportedContent.forEach(function (item) {
214
+ var total = unsupportedTotals.filter(function (candidate) { return candidate.source === report.source && candidate.kind === item.kind && candidate.name === item.name; })[0];
215
+ if (total) { total.count += item.count; total.documents += 1; }
216
+ else unsupportedTotals.push({ source: report.source, kind: item.kind, name: item.name, count: item.count, documents: 1 });
217
+ });
218
+ });
219
+ unsupportedTotals.sort(function (left, right) { return (left.source + "\0" + left.kind + "\0" + left.name).localeCompare(right.source + "\0" + right.kind + "\0" + right.name); });
220
+ return {
221
+ schemaVersion: REPORT_SCHEMA_VERSION,
222
+ generator: REPORT_GENERATOR,
223
+ converterModule: loadedConverters ? loadedConverters.provenance : null,
224
+ readyForCutover: reports.every(function (report) { return report.readyForCutover; }),
225
+ summary: {
226
+ documents: reports.length,
227
+ readyDocuments: reports.filter(function (report) { return report.readyForCutover; }).length,
228
+ blockedDocuments: reports.filter(function (report) { return !report.readyForCutover; }).length,
229
+ unsupportedTotals: unsupportedTotals,
230
+ unsupportedByDocument: reports.filter(function (report) { return report.unsupportedContent.length; }).map(function (report, index) {
231
+ return { id: report.id == null ? String(index) : report.id, source: report.source, items: report.unsupportedContent };
232
+ })
233
+ },
234
+ reports: reports
235
+ };
236
+ }
237
+ if (!options.source) throw new TypeError("--source or --input is required");
238
+ var entry = { source: options.source };
239
+ if (options.config) entry.configFile = path.resolve(cwd, options.config);
240
+ if (options.content) entry.contentFile = path.resolve(cwd, options.content);
241
+ var report = resolveEntry(entry, cwd, 0, converters);
242
+ report.schemaVersion = REPORT_SCHEMA_VERSION;
243
+ report.generator = REPORT_GENERATOR;
244
+ report.converterModule = loadedConverters ? loadedConverters.provenance : null;
245
+ return report;
246
+ }
247
+
248
+ function run(argv, io) {
249
+ io = io || { cwd: process.cwd(), stdout: process.stdout, stderr: process.stderr };
250
+ try {
251
+ var options = parseArgs(argv);
252
+ if (options.help) { io.stdout.write(usage() + "\n"); return 0; }
253
+ if (options.printSchema) {
254
+ if (Object.keys(options).some(function (key) { return key !== "allowBlockers" && key !== "printSchema"; }) || options.allowBlockers) throw new TypeError("--print-schema cannot be combined with report options");
255
+ io.stdout.write(fs.readFileSync(path.join(__dirname, "report-schema-v2.json"), "utf8").replace(/\s*$/, "\n"));
256
+ return 0;
257
+ }
258
+ if (options.validateReport) {
259
+ if (Object.keys(options).some(function (key) { return key !== "allowBlockers" && key !== "validateReport" && key !== "reportSha256" && key !== "baselineReport"; }) || options.allowBlockers) throw new TypeError("--validate-report cannot be combined with report-generation options");
260
+ var reportPath = path.resolve(io.cwd, options.validateReport), reportBytes, parsedReport;
261
+ try { reportBytes = fs.readFileSync(reportPath); }
262
+ catch (error) { throw new Error("Unable to read migration report at " + reportPath + ": " + error.message); }
263
+ var reportDigest = crypto.createHash("sha256").update(reportBytes).digest("hex");
264
+ if (options.reportSha256 != null) {
265
+ var expectedReportDigest = String(options.reportSha256).toLowerCase();
266
+ if (!/^[a-f0-9]{64}$/.test(expectedReportDigest)) throw new TypeError("--report-sha256 must be a 64-character hexadecimal SHA-256 digest");
267
+ if (reportDigest !== expectedReportDigest) throw new Error("Migration report SHA-256 mismatch; refusing to validate " + path.basename(reportPath));
268
+ }
269
+ try { parsedReport = JSON.parse(reportBytes.toString("utf8")); }
270
+ catch (error) { throw new Error("Unable to parse migration report JSON at " + reportPath + ": " + error.message); }
271
+ validateStoredReport(parsedReport);
272
+ var validationOutput = { valid: true, schemaVersion: parsedReport.schemaVersion, label: path.basename(options.validateReport), sha256: reportDigest, hashVerifiedBeforeValidation: options.reportSha256 != null };
273
+ if (options.baselineReport) {
274
+ var baselinePath = path.resolve(io.cwd, options.baselineReport), baselineReport = readJson(baselinePath, "baseline migration report");
275
+ validationOutput.comparison = compareStoredReports(baselineReport, parsedReport);
276
+ }
277
+ io.stdout.write(JSON.stringify(validationOutput) + "\n");
278
+ return validationOutput.comparison && !validationOutput.comparison.regressionFree ? 2 : 0;
279
+ }
280
+ if (options.reportSha256 || options.baselineReport) throw new TypeError((options.reportSha256 ? "--report-sha256" : "--baseline-report") + " requires --validate-report");
281
+ var output = createOutput(options, io.cwd);
282
+ var json = JSON.stringify(output, null, 2) + "\n";
283
+ if (options.output) fs.writeFileSync(path.resolve(io.cwd, options.output), json, "utf8");
284
+ else io.stdout.write(json);
285
+ return output.readyForCutover || options.allowBlockers ? 0 : 2;
286
+ } catch (error) {
287
+ io.stderr.write("rte-migration-report: " + error.message + "\n\n" + usage() + "\n");
288
+ return 1;
289
+ }
290
+ }
291
+
292
+ if (require.main === module) process.exitCode = run(process.argv.slice(2));
293
+
294
+ module.exports = { REPORT_GENERATOR: REPORT_GENERATOR, REPORT_SCHEMA_VERSION: REPORT_SCHEMA_VERSION, compareStoredReports: compareStoredReports, createOutput: createOutput, loadConverters: loadConverters, parseArgs: parseArgs, run: run, usage: usage, validateStoredReport: validateStoredReport };
@@ -0,0 +1,18 @@
1
+ export type MigrationSource = "tinymce" | "ckeditor" | "ckeditor5" | "froala" | "quill" | "tiptap" | "lexical";
2
+ export interface MigrationConfigResult { source: string; config: Record<string, unknown>; mappedCommands: string[]; unmapped: string[]; warnings: string[]; }
3
+ export interface UnsupportedContentItem { kind: "embed" | "node" | "mark"; name: string; count: number; paths: string[]; }
4
+ export interface AppliedConverterItem { kind: "embed" | "node" | "mark"; name: string; count: number; paths: string[]; }
5
+ export type MigrationConverterResult = string | { html: string; warnings?: string[] } | null | undefined;
6
+ export type MigrationConverter = (value: unknown, context: { source: MigrationSource; kind: "embed" | "node" | "mark"; name: string; path?: string; html?: string; childrenHtml?: string; node?: unknown; operation?: unknown; attributes?: Record<string, unknown> }) => MigrationConverterResult;
7
+ export interface MigrationConverters { quill?: { embeds?: Record<string, MigrationConverter> }; tiptap?: { nodes?: Record<string, MigrationConverter>; marks?: Record<string, MigrationConverter> }; lexical?: { nodes?: Record<string, MigrationConverter> }; }
8
+ export interface MigrationOptions { converters?: MigrationConverters; }
9
+ export interface MigrationContentResult { source: string; html: string; warnings: string[]; unsupportedContent: UnsupportedContentItem[]; appliedConverters: AppliedConverterItem[]; }
10
+ export interface MigrationReport { source: string; readyForCutover: boolean; config: Record<string, unknown>; html: string; mappedCommands: string[]; unmapped: string[]; unsupportedContent: UnsupportedContentItem[]; appliedConverters: AppliedConverterItem[]; blockers: string[]; warnings: string[]; manualReview: string[]; }
11
+ export declare const SOURCES: string[];
12
+ export declare const COMMANDS: Record<string, string>;
13
+ export declare function mapToolbar(value: unknown): { commands: string[]; unmapped: string[] };
14
+ export declare function migrateConfig(source: MigrationSource, config: Record<string, unknown>): MigrationConfigResult;
15
+ export declare function convertContent(source: MigrationSource, value: unknown, options?: MigrationOptions): MigrationContentResult;
16
+ export declare function createMigrationReport(source: MigrationSource, options?: { config?: Record<string, unknown>; content?: unknown; converters?: MigrationConverters }): MigrationReport;
17
+ export declare function readLegacyContent(source: MigrationSource, editor: unknown): unknown;
18
+ export declare function migrateEditor(source: MigrationSource, legacyEditor: unknown, richTextEditor: { setHTMLCode(html: string): void }, options?: MigrationOptions): MigrationContentResult;