regen.mde 0.2.2
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/LICENSE +16 -0
- package/README.md +295 -0
- package/bin/build-corpus-editor.js +81 -0
- package/bin/build-corpus.js +41 -0
- package/bin/postinstall.js +187 -0
- package/bin/regen-mdeditor-install.js +27 -0
- package/bin/regen-mdeditor-uninstall.js +19 -0
- package/bin/validate-katex.js +93 -0
- package/desktop/BuildCorpusEditor/BuildCorpusBridge.cs +270 -0
- package/desktop/BuildCorpusEditor/BuildCorpusEditor.csproj +22 -0
- package/desktop/BuildCorpusEditor/EditorForm.cs +540 -0
- package/desktop/BuildCorpusEditor/Program.cs +81 -0
- package/desktop/BuildCorpusEditor/app.manifest +16 -0
- package/dist/release/regen.mde-0.2.2-win-x64-setup.exe +0 -0
- package/dist/release/regen.mde-0.2.2-win-x64.zip +0 -0
- package/dist/windows-editor/BuildCorpusEditor.deps.json +83 -0
- package/dist/windows-editor/BuildCorpusEditor.dll +0 -0
- package/dist/windows-editor/BuildCorpusEditor.exe +0 -0
- package/dist/windows-editor/BuildCorpusEditor.pdb +0 -0
- package/dist/windows-editor/BuildCorpusEditor.runtimeconfig.json +19 -0
- package/dist/windows-editor/Microsoft.Web.WebView2.Core.dll +0 -0
- package/dist/windows-editor/Microsoft.Web.WebView2.Core.xml +6817 -0
- package/dist/windows-editor/Microsoft.Web.WebView2.WinForms.dll +0 -0
- package/dist/windows-editor/Microsoft.Web.WebView2.WinForms.xml +510 -0
- package/dist/windows-editor/Microsoft.Web.WebView2.Wpf.dll +0 -0
- package/dist/windows-editor/Microsoft.Web.WebView2.Wpf.xml +1902 -0
- package/dist/windows-editor/WebView2Loader.dll +0 -0
- package/dist/windows-editor/runtimes/win-x64/native/WebView2Loader.dll +0 -0
- package/dist/windows-editor/wwwroot/assets/index-DjJ6xmhy.js +326 -0
- package/dist/windows-editor/wwwroot/assets/index-_dwMNNsm.css +1 -0
- package/dist/windows-editor/wwwroot/index.html +22 -0
- package/editor-web/index.html +21 -0
- package/editor-web/src/main.jsx +399 -0
- package/editor-web/src/styles.css +602 -0
- package/editor-web/vite.config.js +13 -0
- package/examples/build-corpus.config.example.json +21 -0
- package/installer/install-regen-mde.ps1 +175 -0
- package/installer/regen-mde.nsi +81 -0
- package/package.json +86 -0
- package/pyproject.toml +33 -0
- package/requirements.txt +4 -0
- package/scripts/build-windows-editor.ps1 +47 -0
- package/scripts/package-windows-editor.ps1 +90 -0
- package/scripts/run-corpus.ps1 +28 -0
- package/scripts/run-editor-implementation-plane.ps1 +203 -0
- package/scripts/run-required-tests.ps1 +98 -0
- package/scripts/run-smoke.ps1 +28 -0
- package/src/build_corpus/__init__.py +3 -0
- package/src/build_corpus/docx_exporter.py +798 -0
- package/src/build_corpus/exporter.py +1195 -0
- package/src/build_corpus/ppt_exporter.py +532 -0
- package/src/build_corpus/templates/__init__.py +1 -0
- package/src/build_corpus/templates/md-to-word-template.dotx +0 -0
- package/src/build_corpus/validate_assets.py +46 -0
- package/tools/audit_corpus.py +203 -0
- package/tools/collect_microsoft_word_templates.py +228 -0
- package/tools/collect_online_docx_corpus.py +272 -0
- package/tools/collect_online_pptx_corpus.py +252 -0
- package/tools/compare_pptx_inputs_outputs.py +87 -0
- package/tools/roundtrip_docx_corpus.py +171 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{color-scheme:light;font-family:Segoe UI,Inter,system-ui,sans-serif;background:#fbf8f0;color:#192028;--ink: oklch(24% .018 250);--muted: oklch(47% .022 250);--paper: oklch(98% .011 92);--panel: oklch(95% .014 92);--line: oklch(82% .018 92);--soft: oklch(91% .022 92);--accent: oklch(56% .135 176);--accent-dark: oklch(36% .09 176);--warning: oklch(52% .12 23);--violet: oklch(60% .11 284);--source: oklch(20% .022 250);--source-ink: oklch(93% .016 92);--shadow: 0 24px 70px rgba(34, 38, 46, .18);--button-bg: oklch(99% .006 92);--button-hover: oklch(94% .034 176);--topbar-bg: rgba(252, 249, 240, .94);--panel-bg: rgba(248, 245, 236, .96);--card-bg: oklch(99% .006 92);--page-bg: oklch(99% .005 92);--page-ink: oklch(27% .018 250);--rail-bg: oklch(28% .026 250);--rail-item-bg: oklch(37% .04 250);--grid-line-strong: rgba(41, 143, 132, .08);--grid-line-soft: rgba(41, 143, 132, .05);--warning-bg: oklch(93% .044 72);--warning-border: oklch(74% .075 72);--warning-ink: oklch(35% .075 72);--bubble-bg: oklch(24% .022 250);--bubble-line: oklch(35% .025 250);--bubble-ink: oklch(94% .01 92);--table-head: oklch(92% .025 92)}[data-theme=dark]{color-scheme:dark;--ink: oklch(91% .014 250);--muted: oklch(68% .018 250);--paper: oklch(20% .018 250);--panel: oklch(26% .018 250);--line: oklch(38% .022 250);--soft: oklch(16% .018 250);--accent: oklch(65% .13 176);--accent-dark: oklch(88% .058 176);--warning: oklch(71% .14 68);--violet: oklch(71% .1 284);--source: oklch(14% .018 250);--source-ink: oklch(90% .012 92);--shadow: 0 24px 80px rgba(0, 0, 0, .46);--button-bg: oklch(27% .018 250);--button-hover: oklch(32% .038 176);--topbar-bg: rgba(25, 29, 36, .96);--panel-bg: rgba(29, 33, 40, .96);--card-bg: oklch(24% .018 250);--page-bg: oklch(96% .008 92);--page-ink: oklch(25% .018 250);--rail-bg: oklch(13% .02 250);--rail-item-bg: oklch(24% .03 250);--grid-line-strong: rgba(86, 205, 190, .08);--grid-line-soft: rgba(86, 205, 190, .045);--warning-bg: oklch(25% .044 68);--warning-border: oklch(48% .11 68);--warning-ink: oklch(84% .09 68);--bubble-bg: oklch(91% .012 92);--bubble-line: oklch(72% .02 92);--bubble-ink: oklch(20% .022 250);--table-head: oklch(90% .018 92)}*{box-sizing:border-box}body{margin:0;min-width:920px}button{min-height:28px;border:1px solid var(--line);border-radius:5px;padding:4px 9px;background:var(--button-bg);color:var(--ink);font:600 12px/1 Segoe UI,system-ui,sans-serif;letter-spacing:0;cursor:pointer}button:hover:not(:disabled),button.active{border-color:var(--accent);background:var(--button-hover);color:var(--accent-dark)}button:disabled{cursor:default;opacity:.42}button.primary{background:var(--accent);border-color:#006453;border-color:oklch(45% .11 176);color:#f2fbf8}button.ghost{background:transparent}main.app-shell{width:100vw;height:100vh;min-height:720px;display:grid;grid-template-columns:58px 260px minmax(560px,1fr) 320px;grid-template-rows:48px 1fr 30px;overflow:hidden;background:linear-gradient(90deg,var(--grid-line-strong) 0 1px,transparent 1px 72px),linear-gradient(var(--grid-line-soft) 0 1px,transparent 1px 72px),var(--paper)}.brand-rail{grid-row:1 / 4;border-right:1px solid var(--line);background:var(--rail-bg);color:#e7e4dc;display:grid;grid-template-rows:64px repeat(5,42px) 1fr 42px;place-items:center}.mark{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;background:var(--accent);color:#f2fbf8;font-weight:800}.rail-item{width:34px;height:34px;border-radius:7px;display:grid;place-items:center;color:#c7c4bc;font-size:12px;font-weight:700}.rail-item.active{background:var(--rail-item-bg);color:#fbf8f1}.topbar{grid-column:2 / 5;border-bottom:1px solid var(--line);background:var(--topbar-bg);display:flex;align-items:center;justify-content:space-between;gap:16px;padding:0 14px}.file-title{min-width:0;display:flex;align-items:baseline;gap:10px}.file-title strong{font-size:15px}.file-title span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted);font:12px/1.2 Consolas,Cascadia Mono,monospace}.actions{display:flex;align-items:center;gap:6px}.side-panel{border-color:var(--line);background:var(--panel-bg);padding:14px;overflow:auto}.left-panel{grid-row:2 / 3;grid-column:2 / 3;border-right:1px solid var(--line)}.right-panel{grid-row:2 / 3;grid-column:4 / 5;border-left:1px solid var(--line)}.panel-title{margin:0 0 10px;color:var(--muted);font-size:11px;font-weight:800;text-transform:uppercase}.source-card,.check{border:1px solid var(--line);border-radius:7px;background:var(--card-bg)}.source-card{padding:12px;margin-bottom:12px}.source-card strong{display:block;font-size:13px;margin-bottom:5px}.source-card p{margin:0;color:var(--muted);font-size:12px;line-height:1.4}.format-flow,.checklist{display:grid;gap:8px;margin:14px 0}.flow-step{display:grid;grid-template-columns:32px 1fr auto;align-items:center;gap:9px;min-height:42px;padding:7px;border:1px solid var(--line);border-radius:7px;background:var(--panel)}.badge{height:26px;border-radius:5px;display:grid;place-items:center;background:#c0dfd6;color:#003d32;color:oklch(32% .07 176);font-weight:800;font-size:10px}.flow-step small{display:block;color:var(--muted);font-size:11px}.insert-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.insert-tile{min-height:58px;border:1px solid var(--line);border-radius:7px;padding:9px;background:var(--card-bg);font-size:12px;text-align:left}.insert-tile b{display:block;margin-bottom:5px}.workspace{grid-row:2 / 3;grid-column:3 / 4;min-width:0;display:grid;grid-template-rows:42px auto 1fr;background:var(--soft)}.mode-strip{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:0 14px}.segmented{display:inline-flex;gap:2px;padding:3px;border:1px solid var(--line);border-radius:7px;background:var(--card-bg)}.segmented button{border:0;min-height:24px;background:transparent}.segmented button.active{background:var(--ink);color:#fbf8f1}.large-file-notice{padding:8px 14px;border-bottom:1px solid var(--warning-border);background:var(--warning-bg);color:var(--warning-ink);font-size:12px}.canvas-wrap{min-height:0;display:grid;grid-template-columns:minmax(360px,1fr) minmax(300px,.72fr);overflow:hidden}.workspace.preview .canvas-wrap,.workspace.markdown .canvas-wrap{grid-template-columns:1fr}.workspace.preview .markdown-pane,.workspace.markdown .page-area{display:none}.page-area{min-width:0;overflow:auto;padding:26px 38px 60px}.page{position:relative;width:min(760px,100%);min-height:900px;margin:0 auto;padding:58px 64px;border:1px solid oklch(84% .014 92);border-radius:6px;background:var(--page-bg);box-shadow:var(--shadow)}.page-label{margin:0 0 18px;color:var(--warning);font-size:11px;font-weight:800;text-transform:uppercase}.rich .ProseMirror{min-height:720px;outline:none;color:var(--page-ink);line-height:1.62}.rich .ProseMirror h1{margin:0 0 18px;max-width:14ch;font-size:42px;line-height:.98}.rich .ProseMirror h2{margin-top:32px;font-size:25px}.rich .ProseMirror p,.rich .ProseMirror li{font-size:15px}.rich .ProseMirror blockquote{margin:24px 0;padding:15px;border:1px solid oklch(78% .055 176);border-radius:7px;background:color-mix(in oklch,var(--accent) 14%,var(--page-bg))}.rich .ProseMirror table{width:100%;margin:22px 0;border-collapse:collapse;font-size:13px}.rich .ProseMirror th,.rich .ProseMirror td{border:1px solid oklch(78% .016 92);padding:8px;text-align:left}.rich .ProseMirror th{background:var(--table-head)}.rich img{max-width:100%;height:auto;border-radius:5px}.markdown-pane{min-width:0;overflow:auto;padding:18px;background:var(--source);color:var(--source-ink)}.source{width:100%;min-height:100%;border:0;resize:none;outline:none;background:transparent;color:inherit;font:13px/1.55 Consolas,Cascadia Mono,monospace}.bubble-menu,.floating-menu{display:flex;align-items:center;gap:3px;padding:5px;border:1px solid var(--bubble-line);border-radius:8px;background:var(--bubble-bg);box-shadow:0 16px 32px #181c2438}.floating-menu{background:var(--card-bg);border-color:var(--line)}.bubble-menu button{min-width:28px;border-color:#3e4c5a;background:transparent;color:var(--bubble-ink)}.floating-menu button{min-width:28px;min-height:26px}.meter{height:8px;overflow:hidden;border-radius:999px;background:#d8d4c7;margin-top:10px}.meter span{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--violet))}.check{display:grid;grid-template-columns:24px 1fr;gap:8px;align-items:start;padding:8px;font-size:12px}.dot{width:16px;height:16px;margin-top:1px;border-radius:999px;background:var(--accent)}.dot.warn{background:var(--warning)}.tool-group{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin-bottom:12px}.tool-group button{min-width:0}.statusbar{grid-column:2 / 5;border-top:1px solid var(--line);background:#f5f2e9;color:var(--muted);display:flex;align-items:center;justify-content:space-between;gap:20px;padding:0 12px;font-size:12px}.statusbar span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media(max-width:1120px){body{min-width:760px}main.app-shell{grid-template-columns:52px 1fr;grid-template-rows:48px 1fr 30px}.topbar,.statusbar{grid-column:2 / 3}.left-panel,.right-panel{display:none}.workspace{grid-column:2 / 3}.canvas-wrap{grid-template-columns:1fr}.workspace.compare .markdown-pane{display:none}}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>regen.mde</title>
|
|
7
|
+
<script>
|
|
8
|
+
window.__REGEN_MDEDITOR_ERROR = "";
|
|
9
|
+
window.addEventListener("error", function (event) {
|
|
10
|
+
window.__REGEN_MDEDITOR_ERROR = event.message || "window error";
|
|
11
|
+
});
|
|
12
|
+
window.addEventListener("unhandledrejection", function (event) {
|
|
13
|
+
window.__REGEN_MDEDITOR_ERROR = event.reason && event.reason.message ? event.reason.message : String(event.reason || "unhandled rejection");
|
|
14
|
+
});
|
|
15
|
+
</script>
|
|
16
|
+
<script type="module" crossorigin src="./assets/index-DjJ6xmhy.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="./assets/index-_dwMNNsm.css">
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="root"></div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>regen.mde</title>
|
|
7
|
+
<script>
|
|
8
|
+
window.__REGEN_MDEDITOR_ERROR = "";
|
|
9
|
+
window.addEventListener("error", function (event) {
|
|
10
|
+
window.__REGEN_MDEDITOR_ERROR = event.message || "window error";
|
|
11
|
+
});
|
|
12
|
+
window.addEventListener("unhandledrejection", function (event) {
|
|
13
|
+
window.__REGEN_MDEDITOR_ERROR = event.reason && event.reason.message ? event.reason.message : String(event.reason || "unhandled rejection");
|
|
14
|
+
});
|
|
15
|
+
</script>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div id="root"></div>
|
|
19
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { EditorContent, useEditor } from "@tiptap/react";
|
|
4
|
+
import { BubbleMenu, FloatingMenu } from "@tiptap/react/menus";
|
|
5
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
6
|
+
import { Link } from "@tiptap/extension-link";
|
|
7
|
+
import { Placeholder } from "@tiptap/extension-placeholder";
|
|
8
|
+
import { Image } from "@tiptap/extension-image";
|
|
9
|
+
import { Table } from "@tiptap/extension-table";
|
|
10
|
+
import { TableRow } from "@tiptap/extension-table-row";
|
|
11
|
+
import { TableHeader } from "@tiptap/extension-table-header";
|
|
12
|
+
import { TableCell } from "@tiptap/extension-table-cell";
|
|
13
|
+
import { TaskList } from "@tiptap/extension-task-list";
|
|
14
|
+
import { TaskItem } from "@tiptap/extension-task-item";
|
|
15
|
+
import { marked } from "marked";
|
|
16
|
+
import TurndownService from "turndown";
|
|
17
|
+
import "./styles.css";
|
|
18
|
+
|
|
19
|
+
const turndown = new TurndownService({
|
|
20
|
+
headingStyle: "atx",
|
|
21
|
+
bulletListMarker: "-",
|
|
22
|
+
codeBlockStyle: "fenced",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const bridge = {
|
|
26
|
+
nextId: 1,
|
|
27
|
+
pending: new Map(),
|
|
28
|
+
call(method, params = {}) {
|
|
29
|
+
const id = this.nextId++;
|
|
30
|
+
const payload = { id, method, params };
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
this.pending.set(id, { resolve, reject });
|
|
33
|
+
if (window.chrome?.webview) {
|
|
34
|
+
window.chrome.webview.postMessage(payload);
|
|
35
|
+
} else {
|
|
36
|
+
reject(new Error("Native bridge is not available."));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (window.chrome?.webview) {
|
|
43
|
+
window.chrome.webview.addEventListener("message", (event) => {
|
|
44
|
+
const message = event.data;
|
|
45
|
+
const pending = bridge.pending.get(message.id);
|
|
46
|
+
if (!pending) return;
|
|
47
|
+
bridge.pending.delete(message.id);
|
|
48
|
+
if (message.ok) pending.resolve(message.result);
|
|
49
|
+
else pending.reject(new Error(message.error || "Native bridge failed."));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function markdownToHtml(markdown) {
|
|
54
|
+
return marked.parse(markdown || "", { gfm: true, breaks: false });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function htmlToMarkdown(html) {
|
|
58
|
+
return turndown.turndown(html || "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ToolbarButton({ label, title, active, disabled, onClick, className = "" }) {
|
|
62
|
+
return (
|
|
63
|
+
<button type="button" title={title || label} className={`${active ? "active" : ""} ${className}`.trim()} disabled={disabled} onClick={onClick}>
|
|
64
|
+
{label}
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function App() {
|
|
70
|
+
const [markdown, setMarkdown] = React.useState("# regen.mde\n\nOpen a Markdown or Word file to begin.");
|
|
71
|
+
const [markdownPath, setMarkdownPath] = React.useState("");
|
|
72
|
+
const [workingPath, setWorkingPath] = React.useState("");
|
|
73
|
+
const [sourcePath, setSourcePath] = React.useState("");
|
|
74
|
+
const [exportPath, setExportPath] = React.useState("");
|
|
75
|
+
const [originalFormat, setOriginalFormat] = React.useState("");
|
|
76
|
+
const [status, setStatus] = React.useState("Ready");
|
|
77
|
+
const [mode, setMode] = React.useState("compare");
|
|
78
|
+
const [theme, setTheme] = React.useState(() => {
|
|
79
|
+
const saved = window.localStorage?.getItem("build-corpus-editor-theme");
|
|
80
|
+
if (saved === "dark" || saved === "light") return saved;
|
|
81
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
82
|
+
});
|
|
83
|
+
const [dirty, setDirty] = React.useState(false);
|
|
84
|
+
const [largeFile, setLargeFile] = React.useState(false);
|
|
85
|
+
|
|
86
|
+
const editor = useEditor({
|
|
87
|
+
extensions: [
|
|
88
|
+
StarterKit,
|
|
89
|
+
Link.configure({ openOnClick: false, autolink: true }),
|
|
90
|
+
Placeholder.configure({ placeholder: "Start writing..." }),
|
|
91
|
+
Image.configure({ inline: false, allowBase64: true }),
|
|
92
|
+
Table.configure({ resizable: true }),
|
|
93
|
+
TableRow,
|
|
94
|
+
TableHeader,
|
|
95
|
+
TableCell,
|
|
96
|
+
TaskList,
|
|
97
|
+
TaskItem.configure({ nested: true }),
|
|
98
|
+
],
|
|
99
|
+
content: markdownToHtml(markdown),
|
|
100
|
+
immediatelyRender: false,
|
|
101
|
+
onUpdate({ editor }) {
|
|
102
|
+
const next = htmlToMarkdown(editor.getHTML());
|
|
103
|
+
setMarkdown(next);
|
|
104
|
+
setDirty(true);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const loadDocument = React.useCallback(async (path) => {
|
|
109
|
+
setStatus("Opening...");
|
|
110
|
+
const doc = await bridge.call("open", { path });
|
|
111
|
+
const format = doc.originalFormat || "";
|
|
112
|
+
const isMarkdown = format === "markdown";
|
|
113
|
+
setMarkdownPath(doc.workingPath || (isMarkdown ? doc.sourcePath || "" : ""));
|
|
114
|
+
setWorkingPath(doc.workingPath || "");
|
|
115
|
+
setSourcePath(doc.sourcePath || "");
|
|
116
|
+
setExportPath("");
|
|
117
|
+
setOriginalFormat(format);
|
|
118
|
+
setMarkdown(doc.content || "");
|
|
119
|
+
setLargeFile((doc.content || "").length > 50000);
|
|
120
|
+
editor?.commands.setContent(markdownToHtml(doc.content || ""));
|
|
121
|
+
setDirty(false);
|
|
122
|
+
setMode((doc.content || "").length > 50000 ? "markdown" : "compare");
|
|
123
|
+
setStatus(`Opened ${doc.sourcePath}`);
|
|
124
|
+
}, [editor]);
|
|
125
|
+
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
bridge.call("startup").then((startup) => {
|
|
128
|
+
if (startup.initialPath) loadDocument(startup.initialPath);
|
|
129
|
+
}).catch((error) => setStatus(error.message));
|
|
130
|
+
}, [loadDocument]);
|
|
131
|
+
|
|
132
|
+
React.useEffect(() => {
|
|
133
|
+
if (editor && !dirty) editor.commands.setContent(markdownToHtml(markdown));
|
|
134
|
+
}, [editor, markdown, dirty]);
|
|
135
|
+
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
window.localStorage?.setItem("build-corpus-editor-theme", theme);
|
|
138
|
+
}, [theme]);
|
|
139
|
+
|
|
140
|
+
async function chooseOpen() {
|
|
141
|
+
try {
|
|
142
|
+
const result = await bridge.call("chooseOpen");
|
|
143
|
+
if (result?.path) await loadDocument(result.path);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
setStatus(error.message);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function save() {
|
|
150
|
+
try {
|
|
151
|
+
setStatus("Saving...");
|
|
152
|
+
const result = await bridge.call("save", { path: markdownPath, content: markdown });
|
|
153
|
+
setMarkdownPath(result.output);
|
|
154
|
+
setWorkingPath(result.output);
|
|
155
|
+
setDirty(false);
|
|
156
|
+
setStatus(`Saved Markdown ${result.output}`);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
setStatus(error.message);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function saveAs(format) {
|
|
163
|
+
try {
|
|
164
|
+
setStatus("Saving as...");
|
|
165
|
+
const smokeOut = window.__REGEN_MDEDITOR_SMOKE_OUT;
|
|
166
|
+
const method = smokeOut ? "saveAsDirect" : "saveAs";
|
|
167
|
+
const params = { suggestedPath: markdownPath || sourcePath || workingPath, format, content: markdown };
|
|
168
|
+
if (smokeOut) {
|
|
169
|
+
const extension = format === "word" ? ".docx" : ".md";
|
|
170
|
+
params.targetPath = `${smokeOut}\\ui-save-as-${format}${extension}`;
|
|
171
|
+
}
|
|
172
|
+
const result = await bridge.call(method, params);
|
|
173
|
+
if (result?.output) {
|
|
174
|
+
if (format === "markdown") {
|
|
175
|
+
setMarkdownPath(result.output);
|
|
176
|
+
setWorkingPath(result.output);
|
|
177
|
+
setDirty(false);
|
|
178
|
+
setStatus(`Saved Markdown ${result.output}`);
|
|
179
|
+
} else {
|
|
180
|
+
setExportPath(result.output);
|
|
181
|
+
setStatus(`Exported Word ${result.output}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
setStatus(error.message);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function setLink() {
|
|
190
|
+
const previous = editor?.getAttributes("link").href || "";
|
|
191
|
+
const href = window.prompt("URL", previous);
|
|
192
|
+
if (href === null) return;
|
|
193
|
+
if (href.trim() === "") {
|
|
194
|
+
editor?.chain().focus().unsetLink().run();
|
|
195
|
+
} else {
|
|
196
|
+
editor?.chain().focus().extendMarkRange("link").setLink({ href: href.trim() }).run();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function insertImageFile() {
|
|
201
|
+
try {
|
|
202
|
+
const result = await bridge.call("chooseImage");
|
|
203
|
+
if (result?.src) {
|
|
204
|
+
editor?.chain().focus().setImage({ src: result.src, alt: result.alt || "" }).run();
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
setStatus(error.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function insertImageUrl() {
|
|
212
|
+
const src = window.prompt("Image URL");
|
|
213
|
+
if (src) editor?.chain().focus().setImage({ src: src.trim() }).run();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function syncSource(next) {
|
|
217
|
+
setMarkdown(next);
|
|
218
|
+
setDirty(true);
|
|
219
|
+
if (editor && next.length <= 50000) editor.commands.setContent(markdownToHtml(next));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
React.useEffect(() => {
|
|
223
|
+
if (!window.__REGEN_MDEDITOR_SMOKE_OUT) return undefined;
|
|
224
|
+
window.__REGEN_MDEDITOR_SMOKE_API = {
|
|
225
|
+
appendMarkdown(text) {
|
|
226
|
+
const next = `${markdown}${text}`;
|
|
227
|
+
syncSource(next);
|
|
228
|
+
return next;
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
return () => {
|
|
232
|
+
delete window.__REGEN_MDEDITOR_SMOKE_API;
|
|
233
|
+
};
|
|
234
|
+
}, [markdown, editor]);
|
|
235
|
+
|
|
236
|
+
const canUseRich = Boolean(editor) && !largeFile;
|
|
237
|
+
const visiblePath = sourcePath || markdownPath || workingPath || "No file open";
|
|
238
|
+
const formatLabel = originalFormat ? originalFormat.toUpperCase() : "NEW";
|
|
239
|
+
const wordCount = markdown.trim() ? markdown.trim().split(/\s+/).length : 0;
|
|
240
|
+
const tableCount = (markdown.match(/\n\|/g) || []).length > 1 ? 1 : 0;
|
|
241
|
+
const imageCount = (markdown.match(/!\[[^\]]*]\(/g) || []).length;
|
|
242
|
+
const linkCount = (markdown.match(/https?:\/\//g) || []).length;
|
|
243
|
+
const confidence = Math.max(18, Math.min(94, 92 - imageCount * 8 - tableCount * 5));
|
|
244
|
+
const sourceBadge = formatLabel === "MARKDOWN" ? "MD" : formatLabel === "NEW" ? "NEW" : formatLabel;
|
|
245
|
+
const sourceText = sourcePath ? `${formatLabel} source loaded` : "No source loaded";
|
|
246
|
+
const draftText = markdownPath ? "Markdown target selected" : "Unsaved Markdown draft";
|
|
247
|
+
const exportText = exportPath ? "Word export written" : "Word export ready";
|
|
248
|
+
const nextTheme = theme === "dark" ? "light" : "dark";
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<main className="app-shell" data-theme={theme}>
|
|
252
|
+
<aside className="brand-rail" aria-label="Primary tools">
|
|
253
|
+
<div className="mark">BC</div>
|
|
254
|
+
<div className="rail-item active">Ed</div>
|
|
255
|
+
<div className="rail-item">Cv</div>
|
|
256
|
+
<div className="rail-item">Tb</div>
|
|
257
|
+
<div className="rail-item">Im</div>
|
|
258
|
+
<div className="rail-item">Qa</div>
|
|
259
|
+
<div />
|
|
260
|
+
<div className="rail-item">?</div>
|
|
261
|
+
</aside>
|
|
262
|
+
|
|
263
|
+
<header className="topbar">
|
|
264
|
+
<div className="file-title">
|
|
265
|
+
<strong>regen.mde</strong>
|
|
266
|
+
<span title={visiblePath}>{visiblePath}</span>
|
|
267
|
+
</div>
|
|
268
|
+
<div className="actions">
|
|
269
|
+
<button className="ghost" onClick={chooseOpen}>Open</button>
|
|
270
|
+
<button onClick={save} disabled={!markdownPath}>Save MD</button>
|
|
271
|
+
<button onClick={() => saveAs("markdown")}>Save MD As...</button>
|
|
272
|
+
<button onClick={() => saveAs("word")}>Export Word...</button>
|
|
273
|
+
<button className="ghost" onClick={() => setTheme(nextTheme)}>{theme === "dark" ? "Light" : "Dark"}</button>
|
|
274
|
+
<button className="primary" onClick={() => setStatus(`Checked ${wordCount} words, ${tableCount} tables, ${imageCount} images, ${linkCount} links`)}>Run Check</button>
|
|
275
|
+
</div>
|
|
276
|
+
</header>
|
|
277
|
+
|
|
278
|
+
<section className="side-panel left-panel">
|
|
279
|
+
<h2 className="panel-title">Document Flow</h2>
|
|
280
|
+
<div className="source-card">
|
|
281
|
+
<strong>{sourceText}</strong>
|
|
282
|
+
<p>{dirty ? "The Markdown draft has unsaved changes." : "The Markdown draft is in sync with its save target."}</p>
|
|
283
|
+
</div>
|
|
284
|
+
<div className="format-flow">
|
|
285
|
+
<div className="flow-step">
|
|
286
|
+
<div className="badge">{sourceBadge}</div>
|
|
287
|
+
<div><b>Original</b><small>{sourcePath || "Choose a file to begin"}</small></div>
|
|
288
|
+
<small>{sourcePath ? "loaded" : "empty"}</small>
|
|
289
|
+
</div>
|
|
290
|
+
<div className="flow-step">
|
|
291
|
+
<div className="badge">MD</div>
|
|
292
|
+
<div><b>Working draft</b><small>{draftText}</small></div>
|
|
293
|
+
<small>{dirty ? "dirty" : "saved"}</small>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="flow-step">
|
|
296
|
+
<div className="badge">DOCX</div>
|
|
297
|
+
<div><b>Word export</b><small>{exportText}</small></div>
|
|
298
|
+
<small>{exportPath ? "done" : "ready"}</small>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<h2 className="panel-title">Insert</h2>
|
|
303
|
+
<div className="insert-grid">
|
|
304
|
+
<button className="insert-tile" disabled={!canUseRich} onClick={() => editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}><b>Table</b>Rows, columns, headers</button>
|
|
305
|
+
<button className="insert-tile" disabled={!canUseRich} onClick={insertImageFile}><b>Image</b>File with alt text</button>
|
|
306
|
+
<button className="insert-tile" disabled={!canUseRich} onClick={() => editor?.chain().focus().toggleBlockquote().run()}><b>Callout</b>Note or quotation</button>
|
|
307
|
+
<button className="insert-tile" disabled={!canUseRich} onClick={() => editor?.chain().focus().toggleTaskList().run()}><b>Task</b>Checklist block</button>
|
|
308
|
+
</div>
|
|
309
|
+
</section>
|
|
310
|
+
|
|
311
|
+
<section className={`workspace ${mode}`}>
|
|
312
|
+
<div className="mode-strip">
|
|
313
|
+
<div className="segmented" aria-label="Editor mode">
|
|
314
|
+
<button className={mode === "preview" ? "active" : ""} onClick={() => setMode("preview")} disabled={largeFile}>Preview</button>
|
|
315
|
+
<button className={mode === "compare" ? "active" : ""} onClick={() => setMode("compare")} disabled={largeFile}>Compare</button>
|
|
316
|
+
<button className={mode === "markdown" ? "active" : ""} onClick={() => setMode("markdown")}>Markdown</button>
|
|
317
|
+
</div>
|
|
318
|
+
<div className="actions">
|
|
319
|
+
<button className="ghost" disabled={!canUseRich} onClick={() => editor?.chain().focus().undo().run()}>Undo</button>
|
|
320
|
+
<button className="ghost" disabled={!canUseRich} onClick={() => editor?.chain().focus().redo().run()}>Redo</button>
|
|
321
|
+
<button className="ghost" disabled={!canUseRich} onClick={setLink}>Link</button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{largeFile && <div className="large-file-notice">Large file: rich preview is paused for performance. Markdown mode stays fully editable.</div>}
|
|
326
|
+
|
|
327
|
+
<div className="canvas-wrap">
|
|
328
|
+
{mode !== "markdown" && !largeFile && editor && (
|
|
329
|
+
<section className="page-area">
|
|
330
|
+
<article className="page rich">
|
|
331
|
+
<p className="page-label">{formatLabel === "NEW" ? "New Markdown draft" : `Converted from ${formatLabel}`}</p>
|
|
332
|
+
<BubbleMenu editor={editor} options={{ placement: "top" }} className="bubble-menu">
|
|
333
|
+
<ToolbarButton label="B" title="Bold" active={editor?.isActive("bold")} onClick={() => editor?.chain().focus().toggleBold().run()} />
|
|
334
|
+
<ToolbarButton label="I" title="Italic" active={editor?.isActive("italic")} onClick={() => editor?.chain().focus().toggleItalic().run()} />
|
|
335
|
+
<ToolbarButton label="S" title="Strike" active={editor?.isActive("strike")} onClick={() => editor?.chain().focus().toggleStrike().run()} />
|
|
336
|
+
<ToolbarButton label="Code" active={editor?.isActive("code")} onClick={() => editor?.chain().focus().toggleCode().run()} />
|
|
337
|
+
<ToolbarButton label="Link" active={editor?.isActive("link")} onClick={setLink} />
|
|
338
|
+
</BubbleMenu>
|
|
339
|
+
<FloatingMenu editor={editor} options={{ placement: "right-start" }} className="floating-menu">
|
|
340
|
+
<ToolbarButton label="H1" onClick={() => editor?.chain().focus().toggleHeading({ level: 1 }).run()} />
|
|
341
|
+
<ToolbarButton label="H2" onClick={() => editor?.chain().focus().toggleHeading({ level: 2 }).run()} />
|
|
342
|
+
<ToolbarButton label="List" onClick={() => editor?.chain().focus().toggleBulletList().run()} />
|
|
343
|
+
<ToolbarButton label="Table" onClick={() => editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()} />
|
|
344
|
+
<ToolbarButton label="Image" onClick={insertImageFile} />
|
|
345
|
+
</FloatingMenu>
|
|
346
|
+
<EditorContent editor={editor} />
|
|
347
|
+
</article>
|
|
348
|
+
</section>
|
|
349
|
+
)}
|
|
350
|
+
|
|
351
|
+
{(mode === "compare" || mode === "markdown" || largeFile) && (
|
|
352
|
+
<aside className="markdown-pane">
|
|
353
|
+
<textarea className="source" value={markdown} onChange={(event) => syncSource(event.target.value)} spellCheck="false" />
|
|
354
|
+
</aside>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
</section>
|
|
358
|
+
|
|
359
|
+
<aside className="side-panel right-panel">
|
|
360
|
+
<h2 className="panel-title">Output Health</h2>
|
|
361
|
+
<div className="source-card">
|
|
362
|
+
<strong>Round-trip confidence</strong>
|
|
363
|
+
<p>Tables, images, links, and headings are tracked as conversion risks.</p>
|
|
364
|
+
<div className="meter" aria-label="Round-trip confidence"><span style={{ width: `${confidence}%` }} /></div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<h2 className="panel-title">Table Tools</h2>
|
|
368
|
+
<div className="tool-group">
|
|
369
|
+
<button disabled={!canUseRich || !editor?.isActive("table")} onClick={() => editor?.chain().focus().addRowAfter().run()}>Row +</button>
|
|
370
|
+
<button disabled={!canUseRich || !editor?.isActive("table")} onClick={() => editor?.chain().focus().addColumnAfter().run()}>Col +</button>
|
|
371
|
+
<button disabled={!canUseRich || !editor?.isActive("table")} onClick={() => editor?.chain().focus().deleteRow().run()}>Row -</button>
|
|
372
|
+
<button disabled={!canUseRich || !editor?.isActive("table")} onClick={() => editor?.chain().focus().deleteColumn().run()}>Col -</button>
|
|
373
|
+
<button disabled={!canUseRich || !editor?.isActive("table")} onClick={() => editor?.chain().focus().toggleHeaderRow().run()}>Header</button>
|
|
374
|
+
<button disabled={!canUseRich || !editor?.isActive("table")} onClick={() => editor?.chain().focus().deleteTable().run()}>Delete</button>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<h2 className="panel-title">Image Tools</h2>
|
|
378
|
+
<div className="tool-group">
|
|
379
|
+
<button disabled={!canUseRich} onClick={insertImageFile}>File</button>
|
|
380
|
+
<button disabled={!canUseRich} onClick={insertImageUrl}>URL</button>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div className="checklist">
|
|
384
|
+
<div className="check"><span className="dot" /><div><b>Markdown target</b><br />{markdownPath || "Choose Save MD As..."}</div></div>
|
|
385
|
+
<div className="check"><span className="dot" /><div><b>Word export</b><br />{exportPath || "Ready to export with template hook."}</div></div>
|
|
386
|
+
<div className="check"><span className={imageCount ? "dot warn" : "dot"} /><div><b>Images</b><br />{imageCount} detected. Asset folder policy still needs final pass.</div></div>
|
|
387
|
+
<div className="check"><span className={tableCount ? "dot warn" : "dot"} /><div><b>Tables</b><br />{tableCount} detected. Advanced cell controls are surfaced here.</div></div>
|
|
388
|
+
</div>
|
|
389
|
+
</aside>
|
|
390
|
+
|
|
391
|
+
<footer className="statusbar">
|
|
392
|
+
<span>{status}{exportPath ? ` - Last Word export: ${exportPath}` : ""}</span>
|
|
393
|
+
<span>Words {wordCount} | Tables {tableCount} | Images {imageCount} | Links {linkCount}</span>
|
|
394
|
+
</footer>
|
|
395
|
+
</main>
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
createRoot(document.getElementById("root")).render(<App />);
|