@richscripts/richtexteditor 2.5.4 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/package.json +1 -1
- package/richtexteditor/CHANGELOG.md +2714 -2201
- package/richtexteditor/all_plugins.js +2008 -1272
- package/richtexteditor/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/accessibilitychecker.js +60 -3
- package/richtexteditor/plugins/aitoolkit.js +24 -6
- package/richtexteditor/plugins/aitoolkit.min.css +1 -0
- package/richtexteditor/plugins/all_plugins.js +2008 -1272
- package/richtexteditor/plugins/all_plugins.min.js +52 -52
- package/richtexteditor/plugins/documentimport.js +1279 -1263
- package/richtexteditor/plugins/layouttable.js +383 -0
- package/richtexteditor/plugins/shadowdom.js +252 -0
- package/richtexteditor/plugins/slashcommand.js +8 -0
- package/richtexteditor/rte-headless.js +360 -0
- package/richtexteditor/rte-watchdog.js +303 -0
- package/richtexteditor/rte-webcomponent.js +351 -0
- package/richtexteditor/rte.js +2 -2
- package/richtexteditor/rte_mobile.min.css +1 -0
- package/richtexteditor/rte_theme_default.min.css +1 -0
- package/richtexteditor/runtime/richtexteditor_content.min.css +1 -0
- package/richtexteditor/runtime/richtexteditor_preview.min.css +1 -0
|
@@ -1,1263 +1,1279 @@
|
|
|
1
|
-
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
-
|
|
3
|
-
// 2026-06-09 Document import — the natural pair to Export-to-Word/Markdown/PDF.
|
|
4
|
-
// Opens a local file and loads it into the editor. Library-free for the common
|
|
5
|
-
// formats: Markdown (.md/.markdown via the core fromMarkdown engine), HTML
|
|
6
|
-
// (.html/.htm), plain text (.txt), and Word's HTML-based format (.doc, which
|
|
7
|
-
// Word saves as MSO-flavored HTML) — Word junk (mso-* styles, o:/w: tags,
|
|
8
|
-
// conditional comments) is stripped the same way paste-from-Word is.
|
|
9
|
-
//
|
|
10
|
-
// .docx IS parsed library-free: it is a ZIP of XML, and the ZIP central
|
|
11
|
-
// directory is read directly while DecompressionStream("deflate-raw") handles
|
|
12
|
-
// the inflate. No dependency, no server round-trip.
|
|
13
|
-
// (This comment used to say .docx was not parseable library-free. That was true
|
|
14
|
-
// when it was written and stopped being true when the ZIP reader landed below;
|
|
15
|
-
// it then misled a reader into reporting the feature as missing. Kept explicit
|
|
16
|
-
// so it does not happen again.)
|
|
17
|
-
//
|
|
18
|
-
// 2026-07-28 fidelity pass — what a naive converter silently loses, and what
|
|
19
|
-
// this now preserves:
|
|
20
|
-
// - HYPERLINK URLS. w:hyperlink carries only an r:id; the address lives in
|
|
21
|
-
// word/_rels/document.xml.rels. Without resolving it every link imports as
|
|
22
|
-
// plain text with the URL discarded.
|
|
23
|
-
// - IMAGES. w:drawing -> a:blip r:embed -> word/media/*, inlined as data URIs
|
|
24
|
-
// (set config.documentImportImages = false to skip, for image-heavy files).
|
|
25
|
-
// - ORDERED vs BULLET lists and NESTING. document.xml stores every list the
|
|
26
|
-
// same way; only word/numbering.xml says which is which, so a naive reader
|
|
27
|
-
// turns numbered lists into bullets. w:ilvl gives real nesting, placed
|
|
28
|
-
// inside the parent <li> so the markup is valid.
|
|
29
|
-
// - Paragraph alignment (w:jc).
|
|
30
|
-
// Supply config.documentImportResolver(file) to override any type (e.g. to run
|
|
31
|
-
// a server-side converter for formats this does not cover).
|
|
32
|
-
//
|
|
33
|
-
// API:
|
|
34
|
-
// editor.openImportDialog(options?) -> file picker, then import
|
|
35
|
-
// editor.importFile(file, options?) -> import a File object (Promise)
|
|
36
|
-
// editor.htmlFromImportText(text, kind) -> convert text to HTML (kind: md|html|txt|doc)
|
|
37
|
-
// Command: exec_command "importdocument" opens the picker. Slash: "/import".
|
|
38
|
-
// Config:
|
|
39
|
-
// config.documentImport = false // disable
|
|
40
|
-
// config.documentImportMode = "replace" | "insert" // default "replace"
|
|
41
|
-
// config.documentImportAccept = ".md,.markdown,.html,.htm,.txt,.doc,.docx"
|
|
42
|
-
// config.documentImportResolver = function(file){ return htmlOrPromise; }
|
|
43
|
-
RTE_DefaultConfig.plugin_documentimport = RTE_Plugin_DocumentImport;
|
|
44
|
-
if (typeof RTE_DefaultConfig.documentImport === "undefined") RTE_DefaultConfig.documentImport = true;
|
|
45
|
-
|
|
46
|
-
function RTE_Plugin_DocumentImport() {
|
|
47
|
-
var obj = this;
|
|
48
|
-
var config, editor;
|
|
49
|
-
|
|
50
|
-
obj.PluginName = "DocumentImport";
|
|
51
|
-
|
|
52
|
-
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
53
|
-
|
|
54
|
-
obj.InitEditor = function (argeditor) {
|
|
55
|
-
editor = argeditor;
|
|
56
|
-
if (config.documentImport === false) return;
|
|
57
|
-
|
|
58
|
-
editor.htmlFromImportText = function (text, kind) { return toHtml(String(text == null ? "" : text), kind); };
|
|
59
|
-
editor.importFile = function (file, options) { return importFile(file, options || {}); };
|
|
60
|
-
editor.openImportDialog = function (options) { return openPicker(options || {}); };
|
|
61
|
-
|
|
62
|
-
editor.attachEvent("exec_command_importdocument", function (state) {
|
|
63
|
-
state.returnValue = true;
|
|
64
|
-
state.stopBubble = true;
|
|
65
|
-
openPicker({});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
if (editor.slashCommands && typeof editor.slashCommands.register === "function") {
|
|
69
|
-
try {
|
|
70
|
-
editor.slashCommands.register({
|
|
71
|
-
id: "import-document",
|
|
72
|
-
title: "Import document",
|
|
73
|
-
description: "Open a Markdown, HTML, text, or Word (.doc) file into the editor",
|
|
74
|
-
keywords: ["import", "open", "file", "word", "markdown", "upload"],
|
|
75
|
-
action: function () { openPicker({}); }
|
|
76
|
-
});
|
|
77
|
-
} catch (e) {}
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
function kindFromName(name) {
|
|
82
|
-
var n = String(name || "").toLowerCase();
|
|
83
|
-
if (/\.(md|markdown|mdown|mkd)$/.test(n)) return "md";
|
|
84
|
-
if (/\.html?$/.test(n)) return "html";
|
|
85
|
-
if (/\.doc$/.test(n)) return "doc";
|
|
86
|
-
if (/\.docx$/.test(n)) return "docx";
|
|
87
|
-
if (/\.txt$/.test(n)) return "txt";
|
|
88
|
-
return "txt";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function esc(s) {
|
|
92
|
-
return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// A Word text box and a SmartArt graphic are BOXES on the page. Importing
|
|
96
|
-
// them as bare <div>s keeps the words but loses the thing that made them
|
|
97
|
-
// read as separate from the body text, so the import looks lossier than it
|
|
98
|
-
// is. Styled inline rather than by class because this is saved content: it
|
|
99
|
-
// has to survive wherever the host renders it, with no stylesheet of ours
|
|
100
|
-
// loaded. (Found by check-css-contract.mjs -- the classes were emitted with
|
|
101
|
-
// no rule anywhere.)
|
|
102
|
-
var TEXTBOX_STYLE = "border:1px solid #b9c2cf;border-radius:4px;padding:10px 14px;margin:12px 0;background:#fbfcfe";
|
|
103
|
-
var SMARTART_STYLE = "border:1px solid #b9c2cf;border-radius:4px;padding:10px 14px 10px 6px;margin:12px 0;background:#f7faff";
|
|
104
|
-
|
|
105
|
-
// Strip Word/MSO debris from .doc HTML (same intent as paste-from-Word).
|
|
106
|
-
function cleanWordHtml(html) {
|
|
107
|
-
var h = String(html || "");
|
|
108
|
-
// body only, drop head/xml/style islands and conditional comments
|
|
109
|
-
var bodyMatch = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(h);
|
|
110
|
-
if (bodyMatch) h = bodyMatch[1];
|
|
111
|
-
h = h.replace(/<!--\[if[\s\S]*?\[endif\]-->/gi, "");
|
|
112
|
-
h = h.replace(/<!--[\s\S]*?-->/g, "");
|
|
113
|
-
h = h.replace(/<\/?(o:p|o:|w:|xml|style|meta|link|title|head)[^>]*>/gi, "");
|
|
114
|
-
h = h.replace(/<\\?\?xml[^>]*>/gi, "");
|
|
115
|
-
// strip mso-* declarations + empty style/class/lang attrs
|
|
116
|
-
h = h.replace(/\sstyle="[^"]*"/gi, function (m) {
|
|
117
|
-
var cleaned = m.replace(/mso-[^;"]*;?/gi, "").replace(/style="\s*;*\s*"/i, "");
|
|
118
|
-
return /style="\s*"/.test(cleaned) || cleaned === ' style=""' ? "" : cleaned;
|
|
119
|
-
});
|
|
120
|
-
h = h.replace(/\sclass="Mso[^"]*"/gi, "");
|
|
121
|
-
h = h.replace(/\s(lang|xmlns(:\w+)?)="[^"]*"/gi, "");
|
|
122
|
-
return h;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function toHtml(text, kind) {
|
|
126
|
-
switch (kind) {
|
|
127
|
-
case "md":
|
|
128
|
-
if (editor && typeof editor.fromMarkdown === "function") return editor.fromMarkdown(text, { apply: false });
|
|
129
|
-
return "<p>" + esc(text).replace(/\n{2,}/g, "</p><p>").replace(/\n/g, "<br>") + "</p>";
|
|
130
|
-
case "html":
|
|
131
|
-
return text;
|
|
132
|
-
case "doc":
|
|
133
|
-
return cleanWordHtml(text);
|
|
134
|
-
case "txt":
|
|
135
|
-
default:
|
|
136
|
-
var paras = String(text).replace(/\r\n?/g, "\n").split(/\n{2,}/);
|
|
137
|
-
return paras.map(function (p) {
|
|
138
|
-
return "<p>" + esc(p).replace(/\n/g, "<br>") + "</p>";
|
|
139
|
-
}).join("");
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function applyHtml(html, mode) {
|
|
144
|
-
if (html == null) return false;
|
|
145
|
-
if (mode === "insert") {
|
|
146
|
-
if (typeof editor.insertHTML === "function") { editor.insertHTML(html); return true; }
|
|
147
|
-
}
|
|
148
|
-
if (typeof editor.setHTMLCode === "function") { editor.setHTMLCode(html); return true; }
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function importFile(file, options) {
|
|
153
|
-
var mode = options.mode || config.documentImportMode || "replace";
|
|
154
|
-
return new Promise(function (resolve) {
|
|
155
|
-
if (!file) { resolve(false); return; }
|
|
156
|
-
var kind = kindFromName(file.name);
|
|
157
|
-
|
|
158
|
-
// BYOK resolver wins for any type (lets a host handle .docx etc.).
|
|
159
|
-
if (typeof config.documentImportResolver === "function") {
|
|
160
|
-
try {
|
|
161
|
-
var r = config.documentImportResolver(file);
|
|
162
|
-
Promise.resolve(r).then(function (html) {
|
|
163
|
-
if (typeof html === "string") { resolve(applyHtml(html, mode)); }
|
|
164
|
-
else { builtin(); }
|
|
165
|
-
}, function () { builtin(); });
|
|
166
|
-
return;
|
|
167
|
-
} catch (e) { builtin(); return; }
|
|
168
|
-
}
|
|
169
|
-
builtin();
|
|
170
|
-
|
|
171
|
-
function builtin() {
|
|
172
|
-
if (kind === "docx") {
|
|
173
|
-
// Library-free .docx: unzip word/document.xml (native
|
|
174
|
-
// DecompressionStream) + transform WordprocessingML -> HTML.
|
|
175
|
-
if (!docxSupported()) {
|
|
176
|
-
notify("This browser can't unpack .docx (no DecompressionStream). Save as .doc/.html/.md, or wire config.documentImportResolver.");
|
|
177
|
-
resolve(false);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
readDocx(file).then(function (html) {
|
|
181
|
-
if (html != null) resolve(applyHtml(html, mode));
|
|
182
|
-
else { notify("Could not read this .docx file."); resolve(false); }
|
|
183
|
-
}, function () { notify("Could not read this .docx file."); resolve(false); });
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
if (typeof FileReader === "undefined") { resolve(false); return; }
|
|
187
|
-
var reader = new FileReader();
|
|
188
|
-
reader.onload = function () {
|
|
189
|
-
var html = toHtml(reader.result, kind);
|
|
190
|
-
resolve(applyHtml(html, mode));
|
|
191
|
-
};
|
|
192
|
-
reader.onerror = function () { resolve(false); };
|
|
193
|
-
reader.readAsText(file);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function openPicker(options) {
|
|
199
|
-
try {
|
|
200
|
-
var input = document.createElement("input");
|
|
201
|
-
input.type = "file";
|
|
202
|
-
input.accept = options.accept || config.documentImportAccept || ".md,.markdown,.html,.htm,.txt,.doc,.docx";
|
|
203
|
-
input.style.position = "fixed";
|
|
204
|
-
input.style.left = "-9999px";
|
|
205
|
-
input.addEventListener("change", function () {
|
|
206
|
-
var f = input.files && input.files[0];
|
|
207
|
-
if (f) importFile(f, options);
|
|
208
|
-
setTimeout(function () { try { document.body.removeChild(input); } catch (e) {} }, 0);
|
|
209
|
-
});
|
|
210
|
-
document.body.appendChild(input);
|
|
211
|
-
input.click();
|
|
212
|
-
return true;
|
|
213
|
-
} catch (e) {
|
|
214
|
-
if (window.console) console.error("documentimport: openPicker failed", e);
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ---- library-free .docx (ZIP of XML) ----
|
|
220
|
-
function docxSupported() {
|
|
221
|
-
return typeof DecompressionStream === "function" && typeof DOMParser === "function" &&
|
|
222
|
-
(typeof Response === "function" || typeof Blob === "function");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Locate an entry via the ZIP central directory (reliable comp sizes/offsets).
|
|
226
|
-
function findZipEntry(bytes, wantName) {
|
|
227
|
-
var dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
228
|
-
// End Of Central Directory: signature 0x06054b50, scan from the end.
|
|
229
|
-
var eocd = -1;
|
|
230
|
-
for (var i = bytes.length - 22; i >= 0 && i >= bytes.length - 22 - 65536; i--) {
|
|
231
|
-
if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
|
|
232
|
-
}
|
|
233
|
-
if (eocd < 0) return null;
|
|
234
|
-
var cdOffset = dv.getUint32(eocd + 16, true);
|
|
235
|
-
var cdCount = dv.getUint16(eocd + 10, true);
|
|
236
|
-
var p = cdOffset;
|
|
237
|
-
for (var n = 0; n < cdCount; n++) {
|
|
238
|
-
if (dv.getUint32(p, true) !== 0x02014b50) break;
|
|
239
|
-
var method = dv.getUint16(p + 10, true);
|
|
240
|
-
var compSize = dv.getUint32(p + 20, true);
|
|
241
|
-
var nameLen = dv.getUint16(p + 28, true);
|
|
242
|
-
var extraLen = dv.getUint16(p + 30, true);
|
|
243
|
-
var commentLen = dv.getUint16(p + 32, true);
|
|
244
|
-
var localOff = dv.getUint32(p + 42, true);
|
|
245
|
-
// Normalize separators: the ZIP spec mandates "/", but some Windows
|
|
246
|
-
// tools (.NET ZipFile on older runtimes) emit "\".
|
|
247
|
-
var name = utf8(bytes.subarray(p + 46, p + 46 + nameLen)).replace(/\\/g, "/");
|
|
248
|
-
if (name === wantName) {
|
|
249
|
-
var lh = new DataView(bytes.buffer, bytes.byteOffset + localOff, 30);
|
|
250
|
-
var lNameLen = lh.getUint16(26, true);
|
|
251
|
-
var lExtraLen = lh.getUint16(28, true);
|
|
252
|
-
var dataStart = localOff + 30 + lNameLen + lExtraLen;
|
|
253
|
-
return { method: method, data: bytes.subarray(dataStart, dataStart + compSize) };
|
|
254
|
-
}
|
|
255
|
-
p += 46 + nameLen + extraLen + commentLen;
|
|
256
|
-
}
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function utf8(u8) {
|
|
261
|
-
try { return new TextDecoder("utf-8").decode(u8); } catch (e) {
|
|
262
|
-
var s = ""; for (var i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]); return s;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function inflateRaw(u8) {
|
|
267
|
-
// method 0 = stored, 8 = deflate (raw).
|
|
268
|
-
var ds = new DecompressionStream("deflate-raw");
|
|
269
|
-
var blob = new Blob([u8]);
|
|
270
|
-
return new Response(blob.stream().pipeThrough(ds)).arrayBuffer().then(function (ab) {
|
|
271
|
-
return new Uint8Array(ab);
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Every entry whose name matches, decompressed. Needed because a faithful
|
|
276
|
-
// conversion is not just document.xml: the hyperlink URLs live in the rels
|
|
277
|
-
// part, whether a list is bulleted or numbered lives in numbering.xml, and
|
|
278
|
-
// the images live in word/media.
|
|
279
|
-
function listZipEntries(bytes) {
|
|
280
|
-
var dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
281
|
-
var eocd = -1;
|
|
282
|
-
for (var i = bytes.length - 22; i >= 0 && i >= bytes.length - 22 - 65536; i--) {
|
|
283
|
-
if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
|
|
284
|
-
}
|
|
285
|
-
if (eocd < 0) return [];
|
|
286
|
-
var cdOffset = dv.getUint32(eocd + 16, true);
|
|
287
|
-
var cdCount = dv.getUint16(eocd + 10, true);
|
|
288
|
-
var p = cdOffset, out = [];
|
|
289
|
-
for (var n = 0; n < cdCount; n++) {
|
|
290
|
-
if (dv.getUint32(p, true) !== 0x02014b50) break;
|
|
291
|
-
var method = dv.getUint16(p + 10, true);
|
|
292
|
-
var compSize = dv.getUint32(p + 20, true);
|
|
293
|
-
var nameLen = dv.getUint16(p + 28, true);
|
|
294
|
-
var extraLen = dv.getUint16(p + 30, true);
|
|
295
|
-
var commentLen = dv.getUint16(p + 32, true);
|
|
296
|
-
var localOff = dv.getUint32(p + 42, true);
|
|
297
|
-
var name = utf8(bytes.subarray(p + 46, p + 46 + nameLen)).replace(/\\/g, "/");
|
|
298
|
-
var lh = new DataView(bytes.buffer, bytes.byteOffset + localOff, 30);
|
|
299
|
-
var dataStart = localOff + 30 + lh.getUint16(26, true) + lh.getUint16(28, true);
|
|
300
|
-
out.push({ name: name, method: method, data: bytes.subarray(dataStart, dataStart + compSize) });
|
|
301
|
-
p += 46 + nameLen + extraLen + commentLen;
|
|
302
|
-
}
|
|
303
|
-
return out;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function entryBytes(entry) {
|
|
307
|
-
return entry.method === 0 ? Promise.resolve(entry.data) : inflateRaw(entry.data);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function mimeForExt(name) {
|
|
311
|
-
var e = (name.split(".").pop() || "").toLowerCase();
|
|
312
|
-
if (e === "png") return "image/png";
|
|
313
|
-
if (e === "jpg" || e === "jpeg") return "image/jpeg";
|
|
314
|
-
if (e === "gif") return "image/gif";
|
|
315
|
-
if (e === "bmp") return "image/bmp";
|
|
316
|
-
if (e === "webp") return "image/webp";
|
|
317
|
-
if (e === "svg") return "image/svg+xml";
|
|
318
|
-
return null; // wmf/emf and friends have no browser-renderable form
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function bytesToDataUri(u8, mime) {
|
|
322
|
-
var CHUNK = 0x8000, s = "";
|
|
323
|
-
for (var i = 0; i < u8.length; i += CHUNK) {
|
|
324
|
-
s += String.fromCharCode.apply(null, u8.subarray(i, i + CHUNK));
|
|
325
|
-
}
|
|
326
|
-
return "data:" + mime + ";base64," + btoa(s);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function readDocx(file) {
|
|
330
|
-
var bufPromise = file.arrayBuffer ? file.arrayBuffer() : new Promise(function (res, rej) {
|
|
331
|
-
var r = new FileReader(); r.onload = function () { res(r.result); }; r.onerror = rej; r.readAsArrayBuffer(file);
|
|
332
|
-
});
|
|
333
|
-
return bufPromise.then(function (ab) {
|
|
334
|
-
var bytes = new Uint8Array(ab);
|
|
335
|
-
var entries = listZipEntries(bytes);
|
|
336
|
-
var byName = {};
|
|
337
|
-
for (var i = 0; i < entries.length; i++) byName[entries[i].name] = entries[i];
|
|
338
|
-
if (!byName["word/document.xml"]) return null;
|
|
339
|
-
|
|
340
|
-
var ctx = { rels: {}, numbering: {}, media: {}, footnotes: {}, endnotes: {}, comments: {}, diagrams: {}, vectorNames: {} };
|
|
341
|
-
|
|
342
|
-
// Images are opt-in: a document full of photographs would otherwise
|
|
343
|
-
// inline megabytes of base64 into the editor.
|
|
344
|
-
var wantMedia = config.documentImportImages !== false;
|
|
345
|
-
|
|
346
|
-
var jobs = [entryBytes(byName["word/document.xml"]).then(function (b) { ctx.documentXml = utf8(b); })];
|
|
347
|
-
|
|
348
|
-
if (byName["word/_rels/document.xml.rels"]) {
|
|
349
|
-
jobs.push(entryBytes(byName["word/_rels/document.xml.rels"]).then(function (b) {
|
|
350
|
-
ctx.rels = parseRels(utf8(b));
|
|
351
|
-
}));
|
|
352
|
-
}
|
|
353
|
-
if (byName["word/numbering.xml"]) {
|
|
354
|
-
jobs.push(entryBytes(byName["word/numbering.xml"]).then(function (b) {
|
|
355
|
-
ctx.numbering = parseNumbering(utf8(b));
|
|
356
|
-
}));
|
|
357
|
-
}
|
|
358
|
-
// Footnotes live in their OWN part; document.xml only carries
|
|
359
|
-
// w:footnoteReference with an id. Skip this and a footnoted contract
|
|
360
|
-
// imports with every citation silently deleted.
|
|
361
|
-
if (byName["word/footnotes.xml"]) {
|
|
362
|
-
jobs.push(entryBytes(byName["word/footnotes.xml"]).then(function (b) {
|
|
363
|
-
ctx.footnotes = parseFootnotes(utf8(b), "footnote");
|
|
364
|
-
}));
|
|
365
|
-
}
|
|
366
|
-
// Comment bodies live in their own part too, keyed by id; the
|
|
367
|
-
// document only marks the commented RANGE.
|
|
368
|
-
// Endnotes are structurally identical to footnotes, in their own part.
|
|
369
|
-
if (byName["word/endnotes.xml"]) {
|
|
370
|
-
jobs.push(entryBytes(byName["word/endnotes.xml"]).then(function (b) {
|
|
371
|
-
ctx.endnotes = parseFootnotes(utf8(b), "endnote");
|
|
372
|
-
}));
|
|
373
|
-
}
|
|
374
|
-
if (byName["word/comments.xml"]) {
|
|
375
|
-
jobs.push(entryBytes(byName["word/comments.xml"]).then(function (b) {
|
|
376
|
-
ctx.comments = parseComments(utf8(b));
|
|
377
|
-
}));
|
|
378
|
-
}
|
|
379
|
-
// SmartArt. The drawing in document.xml holds only relationship ids;
|
|
380
|
-
// every word of the diagram lives in word/diagrams/dataN.xml, in the
|
|
381
|
-
// DrawingML *diagram* namespace. A reader that looks for a:blip finds
|
|
382
|
-
// nothing and drops the whole graphic, so an org chart or a process
|
|
383
|
-
// flow imports as empty space.
|
|
384
|
-
for (var dg = 0; dg < entries.length; dg++) {
|
|
385
|
-
(function (ent) {
|
|
386
|
-
if (!/^word\/diagrams\/data\d*\.xml$/.test(ent.name)) return;
|
|
387
|
-
jobs.push(entryBytes(ent).then(function (b) {
|
|
388
|
-
try { ctx.diagrams[ent.name] = parseDiagram(utf8(b)); } catch (e) {}
|
|
389
|
-
}));
|
|
390
|
-
})(entries[dg]);
|
|
391
|
-
}
|
|
392
|
-
if (wantMedia) {
|
|
393
|
-
for (var n = 0; n < entries.length; n++) {
|
|
394
|
-
(function (ent) {
|
|
395
|
-
if (ent.name.indexOf("word/media/") !== 0) return;
|
|
396
|
-
var mime = mimeForExt(ent.name);
|
|
397
|
-
if (!mime) return;
|
|
398
|
-
jobs.push(entryBytes(ent).then(function (b) {
|
|
399
|
-
try { ctx.media[ent.name] = bytesToDataUri(b, mime); } catch (e) {}
|
|
400
|
-
}));
|
|
401
|
-
})(entries[n]);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return Promise.all(jobs).then(function () { return docxXmlToHtml(ctx.documentXml, ctx); });
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// r:id -> target. Without this, every hyperlink in the document survives as
|
|
410
|
-
// plain text with its URL silently discarded.
|
|
411
|
-
function parseRels(xml) {
|
|
412
|
-
var map = {};
|
|
413
|
-
try {
|
|
414
|
-
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
415
|
-
var rs = d.getElementsByTagName("*");
|
|
416
|
-
for (var i = 0; i < rs.length; i++) {
|
|
417
|
-
if (rs[i].localName !== "Relationship") continue;
|
|
418
|
-
var id = rs[i].getAttribute("Id");
|
|
419
|
-
var target = rs[i].getAttribute("Target");
|
|
420
|
-
var mode = rs[i].getAttribute("TargetMode");
|
|
421
|
-
if (id && target) map[id] = { target: target, external: mode === "External" };
|
|
422
|
-
}
|
|
423
|
-
} catch (e) {}
|
|
424
|
-
return map;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// w:id -> plain text of the footnote body.
|
|
428
|
-
// Word reserves ids 0 and -1 for the separator rules drawn above footnotes;
|
|
429
|
-
// those carry type="separator"/"continuationSeparator" and are not content.
|
|
430
|
-
// tagName is "footnote" or "endnote": the two parts share a schema but NOT
|
|
431
|
-
// an element name, so parsing endnotes.xml with "footnote" silently returns
|
|
432
|
-
// nothing at all.
|
|
433
|
-
function parseFootnotes(xml, tagName) {
|
|
434
|
-
var out = {};
|
|
435
|
-
try {
|
|
436
|
-
var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
437
|
-
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
438
|
-
var notes = d.getElementsByTagNameNS(W, tagName || "footnote");
|
|
439
|
-
for (var i = 0; i < notes.length; i++) {
|
|
440
|
-
var n = notes[i];
|
|
441
|
-
var type = n.getAttributeNS(W, "type") || n.getAttribute("w:type") || "";
|
|
442
|
-
if (type) continue; // separator, not a real note
|
|
443
|
-
var id = n.getAttributeNS(W, "id") || n.getAttribute("w:id");
|
|
444
|
-
if (id == null) continue;
|
|
445
|
-
// Body text only; the leading footnote-reference mark inside the
|
|
446
|
-
// note is Word's own numbering and would duplicate ours.
|
|
447
|
-
var txt = (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
448
|
-
if (txt) out[String(id)] = txt;
|
|
449
|
-
}
|
|
450
|
-
} catch (e) {}
|
|
451
|
-
return out;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// w:id -> { text, author }
|
|
455
|
-
function parseComments(xml) {
|
|
456
|
-
var out = {};
|
|
457
|
-
try {
|
|
458
|
-
var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
459
|
-
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
460
|
-
var cs = d.getElementsByTagNameNS(W, "comment");
|
|
461
|
-
for (var i = 0; i < cs.length; i++) {
|
|
462
|
-
var c = cs[i];
|
|
463
|
-
var id = c.getAttributeNS(W, "id") || c.getAttribute("w:id");
|
|
464
|
-
if (id == null) continue;
|
|
465
|
-
out[String(id)] = {
|
|
466
|
-
text: (c.textContent || "").replace(/\s+/g, " ").trim(),
|
|
467
|
-
author: c.getAttributeNS(W, "author") || c.getAttribute("w:author") || ""
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
} catch (e) {}
|
|
471
|
-
return out;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// word/diagrams/dataN.xml -> the diagram's text, one entry per node, in
|
|
475
|
-
// document order.
|
|
476
|
-
//
|
|
477
|
-
// SmartArt has no browser equivalent -- it is a laid-out graphic driven by a
|
|
478
|
-
// layout algorithm -- but its CONTENT is ordinary text, and text is what the
|
|
479
|
-
// reader actually needs. An org chart that imports as an outline is usable; an
|
|
480
|
-
// org chart that imports as nothing is a hole in the document.
|
|
481
|
-
//
|
|
482
|
-
// Nodes with presentation-only roles (`dgm:prSet` with no text, and the
|
|
483
|
-
// ptType "pres"/"parTrans"/"sibTrans" connector points) carry no content and
|
|
484
|
-
// would otherwise contribute blank list items.
|
|
485
|
-
function parseDiagram(xml) {
|
|
486
|
-
var out = [];
|
|
487
|
-
try {
|
|
488
|
-
var DGM = "http://schemas.openxmlformats.org/drawingml/2006/diagram";
|
|
489
|
-
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
490
|
-
var pts = d.getElementsByTagNameNS(DGM, "pt");
|
|
491
|
-
for (var i = 0; i < pts.length; i++) {
|
|
492
|
-
var type = pts[i].getAttribute("type") || "";
|
|
493
|
-
if (type === "pres" || type === "parTrans" || type === "sibTrans") continue;
|
|
494
|
-
var t = pts[i].getElementsByTagNameNS(DGM, "t")[0];
|
|
495
|
-
if (!t) continue;
|
|
496
|
-
var text = (t.textContent || "").replace(/\s+/g, " ").trim();
|
|
497
|
-
if (text) out.push(text);
|
|
498
|
-
}
|
|
499
|
-
} catch (e) {}
|
|
500
|
-
return out;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// numId -> { level -> "bullet" | "decimal" | ... }. Word stores every list
|
|
504
|
-
// the same way in document.xml; only numbering.xml says whether it renders
|
|
505
|
-
// as bullets or numbers, which is why a naive converter turns every ordered
|
|
506
|
-
// list into a <ul>.
|
|
507
|
-
function parseNumbering(xml) {
|
|
508
|
-
var out = {};
|
|
509
|
-
try {
|
|
510
|
-
var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
511
|
-
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
512
|
-
function val(el) { return el ? (el.getAttributeNS(W, "val") || el.getAttribute("w:val") || "") : ""; }
|
|
513
|
-
|
|
514
|
-
// abstractNumId -> { ilvl -> numFmt }
|
|
515
|
-
var abstracts = {};
|
|
516
|
-
var aNums = d.getElementsByTagNameNS(W, "abstractNum");
|
|
517
|
-
for (var i = 0; i < aNums.length; i++) {
|
|
518
|
-
var aid = aNums[i].getAttributeNS(W, "abstractNumId") || aNums[i].getAttribute("w:abstractNumId");
|
|
519
|
-
var levels = {};
|
|
520
|
-
var lvls = aNums[i].getElementsByTagNameNS(W, "lvl");
|
|
521
|
-
for (var j = 0; j < lvls.length; j++) {
|
|
522
|
-
var ilvl = lvls[j].getAttributeNS(W, "ilvl") || lvls[j].getAttribute("w:ilvl") || "0";
|
|
523
|
-
var fmt = val(lvls[j].getElementsByTagNameNS(W, "numFmt")[0]);
|
|
524
|
-
levels[ilvl] = fmt || "decimal";
|
|
525
|
-
}
|
|
526
|
-
if (aid != null) abstracts[aid] = levels;
|
|
527
|
-
}
|
|
528
|
-
// num -> abstractNumId
|
|
529
|
-
var nums = d.getElementsByTagNameNS(W, "num");
|
|
530
|
-
for (var k = 0; k < nums.length; k++) {
|
|
531
|
-
var nid = nums[k].getAttributeNS(W, "numId") || nums[k].getAttribute("w:numId");
|
|
532
|
-
var ref = nums[k].getElementsByTagNameNS(W, "abstractNumId")[0];
|
|
533
|
-
var aref = val(ref);
|
|
534
|
-
if (nid != null && abstracts[aref]) out[nid] = abstracts[aref];
|
|
535
|
-
}
|
|
536
|
-
} catch (e) {}
|
|
537
|
-
return out;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// ---------------------------------------------------------------------
|
|
541
|
-
// OMML (Office Math Markup) -> LaTeX.
|
|
542
|
-
//
|
|
543
|
-
// Word does not store equations as text, MathML or an image: it stores an
|
|
544
|
-
// <m:oMath> tree in its OWN namespace, interleaved between the runs of a
|
|
545
|
-
// paragraph. A converter that walks w:r elements never sees it, so every
|
|
546
|
-
// equation in the document disappears without a trace -- and because the
|
|
547
|
-
// surrounding sentence still imports, nothing looks broken.
|
|
548
|
-
//
|
|
549
|
-
// The target is the editor's own inline-math format
|
|
550
|
-
// (<span class="rte-math-inline" data-tex="...">), so imported equations
|
|
551
|
-
// are editable and re-render through whichever renderer the host page
|
|
552
|
-
// loads, exactly like one typed in the math dialog.
|
|
553
|
-
//
|
|
554
|
-
// This covers the constructs Word's equation editor actually produces.
|
|
555
|
-
// Anything unrecognised degrades to its literal text rather than vanishing
|
|
556
|
-
// -- a wrong-looking formula is recoverable, a missing one is not.
|
|
557
|
-
var OMML_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math";
|
|
558
|
-
|
|
559
|
-
// Word writes operators as Unicode, but a TeX renderer wants command names.
|
|
560
|
-
// Passing "∑" through untouched produces a glyph that KaTeX cannot size or
|
|
561
|
-
// place limits on.
|
|
562
|
-
var MATH_SYMBOLS = {
|
|
563
|
-
"∑": "\\sum", "∏": "\\prod", "∐": "\\coprod",
|
|
564
|
-
"∫": "\\int", "∬": "\\iint", "∭": "\\iiint", "∮": "\\oint",
|
|
565
|
-
"⋃": "\\bigcup", "⋂": "\\bigcap",
|
|
566
|
-
"±": "\\pm", "∓": "\\mp", "×": "\\times", "÷": "\\div",
|
|
567
|
-
"−": "-", "≠": "\\neq", "≤": "\\leq", "≥": "\\geq",
|
|
568
|
-
"≈": "\\approx", "≡": "\\equiv", "∝": "\\propto",
|
|
569
|
-
"∞": "\\infty", "∂": "\\partial", "∇": "\\nabla",
|
|
570
|
-
"√": "\\sqrt", "∠": "\\angle", "⋅": "\\cdot", "…": "\\dots",
|
|
571
|
-
"→": "\\to", "←": "\\leftarrow", "⇒": "\\Rightarrow", "⇔": "\\Leftrightarrow",
|
|
572
|
-
"∈": "\\in", "∉": "\\notin", "⊂": "\\subset", "⊆": "\\subseteq",
|
|
573
|
-
"∪": "\\cup", "∩": "\\cap", "∅": "\\emptyset",
|
|
574
|
-
"∀": "\\forall", "∃": "\\exists", "¬": "\\neg",
|
|
575
|
-
"∧": "\\land", "∨": "\\lor", "∴": "\\therefore",
|
|
576
|
-
"α": "\\alpha", "β": "\\beta", "γ": "\\gamma", "δ": "\\delta",
|
|
577
|
-
"ε": "\\epsilon", "ζ": "\\zeta", "η": "\\eta", "θ": "\\theta",
|
|
578
|
-
"ι": "\\iota", "κ": "\\kappa", "λ": "\\lambda", "μ": "\\mu",
|
|
579
|
-
"ν": "\\nu", "ξ": "\\xi", "π": "\\pi", "ρ": "\\rho",
|
|
580
|
-
"σ": "\\sigma", "τ": "\\tau", "υ": "\\upsilon", "φ": "\\varphi",
|
|
581
|
-
"χ": "\\chi", "ψ": "\\psi", "ω": "\\omega",
|
|
582
|
-
"Γ": "\\Gamma", "Δ": "\\Delta", "Θ": "\\Theta", "Λ": "\\Lambda",
|
|
583
|
-
"Ξ": "\\Xi", "Π": "\\Pi", "Σ": "\\Sigma", "Φ": "\\Phi",
|
|
584
|
-
"Ψ": "\\Psi", "Ω": "\\Omega"
|
|
585
|
-
};
|
|
586
|
-
// Accent character -> TeX accent command (m:acc carries the mark as a chr).
|
|
587
|
-
var MATH_ACCENTS = {
|
|
588
|
-
"̂": "\\hat", "̃": "\\tilde", "̄": "\\bar", "̅": "\\overline",
|
|
589
|
-
"̆": "\\breve", "̇": "\\dot", "̈": "\\ddot", "̊": "\\mathring",
|
|
590
|
-
"̌": "\\check", "→": "\\vec", "⃗": "\\vec", "́": "\\acute", "̀": "\\grave"
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
function ommlToTex(node) {
|
|
594
|
-
var M = OMML_NS;
|
|
595
|
-
function attrVal(parent, ln) {
|
|
596
|
-
// Properties sit in a child element whose only content is m:val.
|
|
597
|
-
for (var i = 0; i < parent.childNodes.length; i++) {
|
|
598
|
-
var c = parent.childNodes[i];
|
|
599
|
-
if (c.nodeType === 1 && c.localName === ln) {
|
|
600
|
-
return c.getAttributeNS(M, "val") || c.getAttribute("m:val") || "";
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
return "";
|
|
604
|
-
}
|
|
605
|
-
function propOf(el, propName, ln) {
|
|
606
|
-
var p = child(el, propName);
|
|
607
|
-
return p ? attrVal(p, ln) : "";
|
|
608
|
-
}
|
|
609
|
-
function child(el, ln) {
|
|
610
|
-
for (var i = 0; i < el.childNodes.length; i++) {
|
|
611
|
-
var c = el.childNodes[i];
|
|
612
|
-
if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === M) return c;
|
|
613
|
-
}
|
|
614
|
-
return null;
|
|
615
|
-
}
|
|
616
|
-
function children(el, ln) {
|
|
617
|
-
var out = [];
|
|
618
|
-
for (var i = 0; i < el.childNodes.length; i++) {
|
|
619
|
-
var c = el.childNodes[i];
|
|
620
|
-
if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === M) out.push(c);
|
|
621
|
-
}
|
|
622
|
-
return out;
|
|
623
|
-
}
|
|
624
|
-
// An argument slot: braces so multi-character content binds correctly.
|
|
625
|
-
// "x+1" without them turns \frac{x+1}{2} into \fracx+12.
|
|
626
|
-
function arg(el) {
|
|
627
|
-
return "{" + (el ? walk(el).trim() : "") + "}";
|
|
628
|
-
}
|
|
629
|
-
// A slot where braces are optional. Emitting them unconditionally is
|
|
630
|
-
// correct but produces "{x}^{2}" and "{{i}^{2}}" -- valid TeX that is
|
|
631
|
-
// unreadable the moment the user opens the equation in the math dialog.
|
|
632
|
-
// A lone letter, digit or command needs none.
|
|
633
|
-
function atom(el) {
|
|
634
|
-
var t = el ? walk(el).trim() : "";
|
|
635
|
-
if (/^[A-Za-z0-9]$/.test(t) || /^\\[A-Za-z]+$/.test(t)) return t;
|
|
636
|
-
return "{" + t + "}";
|
|
637
|
-
}
|
|
638
|
-
function mapText(s) {
|
|
639
|
-
var out = "";
|
|
640
|
-
for (var i = 0; i < s.length; i++) {
|
|
641
|
-
var ch = s.charAt(i);
|
|
642
|
-
var sym = MATH_SYMBOLS[ch];
|
|
643
|
-
if (sym) { out += sym + " "; continue; }
|
|
644
|
-
// TeX metacharacters inside literal math text.
|
|
645
|
-
if ("#$%&_{}".indexOf(ch) >= 0) { out += "\\" + ch; continue; }
|
|
646
|
-
if (ch === "\\") { out += "\\backslash "; continue; }
|
|
647
|
-
out += ch;
|
|
648
|
-
}
|
|
649
|
-
return out;
|
|
650
|
-
}
|
|
651
|
-
function walk(el) {
|
|
652
|
-
var tex = "";
|
|
653
|
-
for (var i = 0; i < el.childNodes.length; i++) {
|
|
654
|
-
var c = el.childNodes[i];
|
|
655
|
-
if (c.nodeType !== 1) continue;
|
|
656
|
-
if (c.namespaceURI !== M) { tex += mapText(c.textContent || ""); continue; }
|
|
657
|
-
tex += convert(c);
|
|
658
|
-
}
|
|
659
|
-
return tex;
|
|
660
|
-
}
|
|
661
|
-
function convert(el) {
|
|
662
|
-
var ln = el.localName;
|
|
663
|
-
switch (ln) {
|
|
664
|
-
case "oMath":
|
|
665
|
-
case "oMathPara":
|
|
666
|
-
case "e":
|
|
667
|
-
case "num":
|
|
668
|
-
case "den":
|
|
669
|
-
case "sub":
|
|
670
|
-
case "sup":
|
|
671
|
-
case "deg":
|
|
672
|
-
case "lim":
|
|
673
|
-
case "fName":
|
|
674
|
-
return walk(el);
|
|
675
|
-
// Properties describe how to render, not what to render; emitting
|
|
676
|
-
// their text would leak stray characters into the formula.
|
|
677
|
-
case "rPr": case "ctrlPr": case "fPr": case "radPr": case "naryPr":
|
|
678
|
-
case "dPr": case "sSupPr": case "sSubPr": case "sSubSupPr": case "accPr":
|
|
679
|
-
case "barPr": case "funcPr": case "mPr": case "limLowPr": case "limUppPr":
|
|
680
|
-
case "groupChrPr": case "mcPr": case "eqArrPr": case "boxPr": case "borderBoxPr":
|
|
681
|
-
case "phantPr": case "sPrePr":
|
|
682
|
-
return "";
|
|
683
|
-
case "r":
|
|
684
|
-
return mapText(el.textContent || "");
|
|
685
|
-
case "t":
|
|
686
|
-
return mapText(el.textContent || "");
|
|
687
|
-
case "f": {
|
|
688
|
-
// A "no bar" fraction is Word's binomial/stacked form.
|
|
689
|
-
var type = propOf(el, "fPr", "type");
|
|
690
|
-
var n = arg(child(el, "num")), d = arg(child(el, "den"));
|
|
691
|
-
if (type === "noBar") return "\\binom" + n + d;
|
|
692
|
-
if (type === "skw" || type === "lin") return n + "/" + d;
|
|
693
|
-
return "\\frac" + n + d;
|
|
694
|
-
}
|
|
695
|
-
case "sSup": return atom(child(el, "e")) + "^" + arg(child(el, "sup"));
|
|
696
|
-
case "sSub": return atom(child(el, "e")) + "_" + arg(child(el, "sub"));
|
|
697
|
-
case "sSubSup": return atom(child(el, "e")) + "_" + arg(child(el, "sub")) + "^" + arg(child(el, "sup"));
|
|
698
|
-
case "sPre": return "{}_" + arg(child(el, "sub")) + "^" + arg(child(el, "sup")) + arg(child(el, "e"));
|
|
699
|
-
case "rad": {
|
|
700
|
-
// degHide means a plain square root; otherwise it is \sqrt[n]{}.
|
|
701
|
-
var hide = propOf(el, "radPr", "degHide");
|
|
702
|
-
var deg = child(el, "deg");
|
|
703
|
-
var body = arg(child(el, "e"));
|
|
704
|
-
if (hide === "1" || hide === "on" || hide === "true" || !deg || !(deg.textContent || "").trim()) return "\\sqrt" + body;
|
|
705
|
-
return "\\sqrt[" + walk(deg) + "]" + body;
|
|
706
|
-
}
|
|
707
|
-
case "nary": {
|
|
708
|
-
// The operator itself is an ATTRIBUTE (m:chr), not text. Miss
|
|
709
|
-
// it and every summation imports as a bare limit pair.
|
|
710
|
-
var chr = propOf(el, "naryPr", "chr") || "∫";
|
|
711
|
-
var op = MATH_SYMBOLS[chr] || mapText(chr);
|
|
712
|
-
var subHide = propOf(el, "naryPr", "subHide");
|
|
713
|
-
var supHide = propOf(el, "naryPr", "supHide");
|
|
714
|
-
var s = op;
|
|
715
|
-
if (subHide !== "1" && subHide !== "on" && child(el, "sub")) s += "_" + arg(child(el, "sub"));
|
|
716
|
-
if (supHide !== "1" && supHide !== "on" && child(el, "sup")) s += "^" + arg(child(el, "sup"));
|
|
717
|
-
// The integrand takes no braces: \sum_{i=1}^{n} i^{2}, not
|
|
718
|
-
// \sum_{i=1}^{n} {i^{2}}.
|
|
719
|
-
var nbody = child(el, "e");
|
|
720
|
-
return s + " " + (nbody ? walk(nbody).trim() : "");
|
|
721
|
-
}
|
|
722
|
-
case "d": {
|
|
723
|
-
// \left/\right so the fences grow with their content.
|
|
724
|
-
var beg = propOf(el, "dPr", "begChr");
|
|
725
|
-
var end = propOf(el, "dPr", "endChr");
|
|
726
|
-
var sep = propOf(el, "dPr", "sepChr") || "|";
|
|
727
|
-
if (beg === "") beg = "(";
|
|
728
|
-
if (end === "") end = ")";
|
|
729
|
-
function fence(ch, side) {
|
|
730
|
-
if (!ch) return side === "l" ? "\\left." : "\\right.";
|
|
731
|
-
if (ch === "{" || ch === "}") return "\\" + ch;
|
|
732
|
-
if (ch === "|") return "|";
|
|
733
|
-
if (ch === "‖") return "\\|";
|
|
734
|
-
// THREE different codepoints render as an angle
|
|
735
|
-
// bracket and are indistinguishable on screen: the
|
|
736
|
-
// mathematical pair (U+27E8/9), the CJK pair
|
|
737
|
-
// (U+3008/9) and the deprecated pair (U+2329/A) that
|
|
738
|
-
// several generators still emit. Comparing against a
|
|
739
|
-
// literal in the source would silently match only one
|
|
740
|
-
// of them, so compare by codepoint.
|
|
741
|
-
var cp = ch.charCodeAt(0);
|
|
742
|
-
if (cp === 0x27E8 || cp === 0x3008 || cp === 0x2329) return "\\langle";
|
|
743
|
-
if (cp === 0x27E9 || cp === 0x3009 || cp === 0x232A) return "\\rangle";
|
|
744
|
-
if (ch === "⌊") return "\\lfloor";
|
|
745
|
-
if (ch === "⌋") return "\\rfloor";
|
|
746
|
-
if (ch === "⌈") return "\\lceil";
|
|
747
|
-
if (ch === "⌉") return "\\rceil";
|
|
748
|
-
return ch;
|
|
749
|
-
}
|
|
750
|
-
var parts = children(el, "e");
|
|
751
|
-
var inner = [];
|
|
752
|
-
for (var q = 0; q < parts.length; q++) inner.push(walk(parts[q]));
|
|
753
|
-
return "\\left" + fence(beg, "l") + inner.join(" " + fence(sep, "m") + " ") + "\\right" + fence(end, "r");
|
|
754
|
-
}
|
|
755
|
-
case "func": {
|
|
756
|
-
// Word stores "sin"/"log" as ordinary text; without
|
|
757
|
-
// \operatorname TeX renders it as s*i*n in italics.
|
|
758
|
-
var name = walk(child(el, "fName")).trim();
|
|
759
|
-
var known = /^(sin|cos|tan|cot|sec|csc|sinh|cosh|tanh|log|ln|lg|exp|lim|max|min|det|dim|gcd|arg|deg|hom|ker|Pr|sup|inf)$/;
|
|
760
|
-
var head = known.test(name) ? "\\" + name : (name ? "\\operatorname{" + name + "}" : "");
|
|
761
|
-
return head + arg(child(el, "e"));
|
|
762
|
-
}
|
|
763
|
-
case "limLow": {
|
|
764
|
-
var base = walk(child(el, "e")).trim();
|
|
765
|
-
if (base === "lim" || base === "\\lim") return "\\lim_" + arg(child(el, "lim"));
|
|
766
|
-
return "\\underset" + arg(child(el, "lim")) + "{" + base + "}";
|
|
767
|
-
}
|
|
768
|
-
case "limUpp":
|
|
769
|
-
return "\\overset" + arg(child(el, "lim")) + arg(child(el, "e"));
|
|
770
|
-
case "acc": {
|
|
771
|
-
var mark = propOf(el, "accPr", "chr") || "̂";
|
|
772
|
-
var cmd = MATH_ACCENTS[mark] || "\\hat";
|
|
773
|
-
return cmd + arg(child(el, "e"));
|
|
774
|
-
}
|
|
775
|
-
case "bar":
|
|
776
|
-
return (propOf(el, "barPr", "pos") === "bot" ? "\\underline" : "\\overline") + arg(child(el, "e"));
|
|
777
|
-
case "groupChr": {
|
|
778
|
-
var g = propOf(el, "groupChrPr", "chr");
|
|
779
|
-
if (g === "⏟") return "\\underbrace" + arg(child(el, "e"));
|
|
780
|
-
return "\\overbrace" + arg(child(el, "e"));
|
|
781
|
-
}
|
|
782
|
-
case "borderBox":
|
|
783
|
-
return "\\boxed" + arg(child(el, "e"));
|
|
784
|
-
case "box":
|
|
785
|
-
case "phant":
|
|
786
|
-
return walk(el);
|
|
787
|
-
case "m": {
|
|
788
|
-
// Matrix: rows are m:mr, cells are m:e inside them.
|
|
789
|
-
var rows = children(el, "mr"), lines = [];
|
|
790
|
-
for (var r = 0; r < rows.length; r++) {
|
|
791
|
-
var cells = children(rows[r], "e"), cs = [];
|
|
792
|
-
for (var cix = 0; cix < cells.length; cix++) cs.push(walk(cells[cix]));
|
|
793
|
-
lines.push(cs.join(" & "));
|
|
794
|
-
}
|
|
795
|
-
return "\\begin{matrix}" + lines.join(" \\\\ ") + "\\end{matrix}";
|
|
796
|
-
}
|
|
797
|
-
case "eqArr": {
|
|
798
|
-
var eqs = children(el, "e"), rowsOut = [];
|
|
799
|
-
for (var e2 = 0; e2 < eqs.length; e2++) rowsOut.push(walk(eqs[e2]));
|
|
800
|
-
return "\\begin{aligned}" + rowsOut.join(" \\\\ ") + "\\end{aligned}";
|
|
801
|
-
}
|
|
802
|
-
default:
|
|
803
|
-
// Unknown construct: keep the content rather than losing it.
|
|
804
|
-
return walk(el);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
try {
|
|
808
|
-
return convert(node).replace(/\s+/g, " ").trim();
|
|
809
|
-
} catch (e) { return (node.textContent || "").trim(); }
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function docxXmlToHtml(xml, ctx) {
|
|
813
|
-
ctx = ctx || { rels: {}, numbering: {}, media: {} };
|
|
814
|
-
if (!ctx.diagrams) ctx.diagrams = {};
|
|
815
|
-
var doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
816
|
-
var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
817
|
-
var R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
818
|
-
|
|
819
|
-
function relTarget(el) {
|
|
820
|
-
if (!el) return null;
|
|
821
|
-
var id = el.getAttributeNS(R, "id") || el.getAttribute("r:id") ||
|
|
822
|
-
el.getAttributeNS(R, "embed") || el.getAttribute("r:embed");
|
|
823
|
-
if (!id) return null;
|
|
824
|
-
return ctx.rels[id] || null;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// <w:drawing>/<w:pict> -> <img> from word/media, inlined as a data URI.
|
|
828
|
-
function imageHtml(node) {
|
|
829
|
-
var blips = node.getElementsByTagName("*");
|
|
830
|
-
for (var i = 0; i < blips.length; i++) {
|
|
831
|
-
if (blips[i].localName !== "blip" && blips[i].localName !== "imagedata") continue;
|
|
832
|
-
var rel = relTarget(blips[i]);
|
|
833
|
-
if (!rel) continue;
|
|
834
|
-
// Targets are relative to word/: "media/image1.png".
|
|
835
|
-
var path = rel.target.replace(/^\.\//, "");
|
|
836
|
-
var key = path.indexOf("word/") === 0 ? path : "word/" + path;
|
|
837
|
-
var uri = ctx.media[key];
|
|
838
|
-
if (uri) return '<img src="' + uri + '" alt="">';
|
|
839
|
-
if (rel.external) return '<img src="' + esc(rel.target) + '" alt="">';
|
|
840
|
-
// A vector metafile (WMF/EMF) has no browser-renderable form, so
|
|
841
|
-
// there is no data URI for it. Dropping it silently leaves the
|
|
842
|
-
// reader with no idea a figure was ever there -- and no filename
|
|
843
|
-
// to go looking for in the original.
|
|
844
|
-
if (/\.(wmf|emf|emz|wmz)$/i.test(path)) {
|
|
845
|
-
var fileName = path.split("/").pop();
|
|
846
|
-
queueBlock('<p class="rte-imported-unsupported" data-source="' + esc(fileName) +
|
|
847
|
-
'"><em>[Unsupported image: ' + esc(fileName) +
|
|
848
|
-
" — Windows metafiles have no browser-renderable form]</em></p>");
|
|
849
|
-
return "";
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
queueBlock(diagramHtml(node));
|
|
853
|
-
return "";
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// SmartArt: the drawing references word/diagrams/dataN.xml through
|
|
857
|
-
// r:dm. Rendered as a labelled list -- the layout cannot be reproduced,
|
|
858
|
-
// but every word of it survives and stays editable.
|
|
859
|
-
function diagramHtml(node) {
|
|
860
|
-
var all = node.getElementsByTagName("*");
|
|
861
|
-
for (var i = 0; i < all.length; i++) {
|
|
862
|
-
if (all[i].localName !== "relIds") continue;
|
|
863
|
-
var id = all[i].getAttributeNS(R, "dm") || all[i].getAttribute("r:dm");
|
|
864
|
-
var rel = id ? ctx.rels[id] : null;
|
|
865
|
-
if (!rel) continue;
|
|
866
|
-
var path = rel.target.replace(/^\.\//, "");
|
|
867
|
-
var key = path.indexOf("word/") === 0 ? path : "word/" + path;
|
|
868
|
-
var nodes = ctx.diagrams[key];
|
|
869
|
-
if (!nodes || !nodes.length) continue;
|
|
870
|
-
var items = "";
|
|
871
|
-
for (var n = 0; n < nodes.length; n++) items += "<li>" + esc(nodes[n]) + "</li>";
|
|
872
|
-
return '<div class="rte-imported-smartart" style="' + SMARTART_STYLE + '"><ul>' + items + "</ul></div>";
|
|
873
|
-
}
|
|
874
|
-
return "";
|
|
875
|
-
}
|
|
876
|
-
function els(parent, ln) { return parent.getElementsByTagNameNS(W, ln); }
|
|
877
|
-
function firstChildEl(parent, ln) {
|
|
878
|
-
for (var i = 0; i < parent.childNodes.length; i++) {
|
|
879
|
-
var c = parent.childNodes[i];
|
|
880
|
-
if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === W) return c;
|
|
881
|
-
}
|
|
882
|
-
return null;
|
|
883
|
-
}
|
|
884
|
-
function runText(r) {
|
|
885
|
-
var t = "", kids = r.childNodes;
|
|
886
|
-
for (var i = 0; i < kids.length; i++) {
|
|
887
|
-
var c = kids[i];
|
|
888
|
-
if (c.nodeType !== 1) continue;
|
|
889
|
-
// Deleted text is stored as w:delText, never w:t — read only w:t
|
|
890
|
-
// and every tracked deletion imports as an empty run.
|
|
891
|
-
if (c.localName === "t" || c.localName === "delText") t += c.textContent || "";
|
|
892
|
-
else if (c.localName === "tab") t += "\t";
|
|
893
|
-
else if (c.localName === "br" || c.localName === "cr") t += "\n";
|
|
894
|
-
}
|
|
895
|
-
return t;
|
|
896
|
-
}
|
|
897
|
-
function runHtml(r) {
|
|
898
|
-
// A run can carry a picture instead of text; without this every
|
|
899
|
-
// image in the document is silently dropped on import.
|
|
900
|
-
var pics = "";
|
|
901
|
-
for (var d = 0; d < r.childNodes.length; d++) {
|
|
902
|
-
var ch = r.childNodes[d];
|
|
903
|
-
if (ch.nodeType === 1 && (ch.localName === "drawing" || ch.localName === "pict")) {
|
|
904
|
-
pics += imageHtml(ch);
|
|
905
|
-
}
|
|
906
|
-
// A run holding a footnote reference has no text of its own.
|
|
907
|
-
if (ch.nodeType === 1 && ch.localName === "footnoteReference") {
|
|
908
|
-
var fid = ch.getAttributeNS(W, "id") || ch.getAttribute("w:id");
|
|
909
|
-
var body = fid != null ? ctx.footnotes[String(fid)] : null;
|
|
910
|
-
if (body) pics += footnoteMarker(body);
|
|
911
|
-
}
|
|
912
|
-
// Endnotes render through the same notes machinery: Word puts
|
|
913
|
-
// both at the end of the document, and the distinction is not
|
|
914
|
-
// one HTML has a separate representation for.
|
|
915
|
-
if (ch.nodeType === 1 && ch.localName === "endnoteReference") {
|
|
916
|
-
var eid = ch.getAttributeNS(W, "id") || ch.getAttribute("w:id");
|
|
917
|
-
var ebody = eid != null ? ctx.endnotes[String(eid)] : null;
|
|
918
|
-
if (ebody) pics += footnoteMarker(ebody);
|
|
919
|
-
}
|
|
920
|
-
// TEXT BOXES. Their content is nested several levels below the
|
|
921
|
-
// run (w:pict > v:shape > v:textbox > w:txbxContent, or the
|
|
922
|
-
// DrawingML mc:AlternateContent equivalent), so a reader that
|
|
923
|
-
// only looks at a run's DIRECT children drops every word inside
|
|
924
|
-
// them -- silently, with no sign anything is missing.
|
|
925
|
-
if (ch.nodeType === 1 && (ch.localName === "pict" || ch.localName === "drawing" ||
|
|
926
|
-
ch.localName === "object" || ch.localName === "AlternateContent")) {
|
|
927
|
-
collectTextBoxes(ch);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
var txt = esc(runText(r));
|
|
931
|
-
if (!txt) return pics;
|
|
932
|
-
txt = txt.replace(/\n/g, "<br>");
|
|
933
|
-
var rpr = firstChildEl(r, "rPr");
|
|
934
|
-
if (rpr) {
|
|
935
|
-
if (firstChildEl(rpr, "b")) txt = "<strong>" + txt + "</strong>";
|
|
936
|
-
if (firstChildEl(rpr, "i")) txt = "<em>" + txt + "</em>";
|
|
937
|
-
if (firstChildEl(rpr, "u")) txt = "<u>" + txt + "</u>";
|
|
938
|
-
if (firstChildEl(rpr, "strike")) txt = "<s>" + txt + "</s>";
|
|
939
|
-
var vert = firstChildEl(rpr, "vertAlign");
|
|
940
|
-
if (vert) {
|
|
941
|
-
var va = vert.getAttributeNS(W, "val") || vert.getAttribute("w:val") || "";
|
|
942
|
-
if (va === "superscript") txt = "<sup>" + txt + "</sup>";
|
|
943
|
-
else if (va === "subscript") txt = "<sub>" + txt + "</sub>";
|
|
944
|
-
}
|
|
945
|
-
// Character colour and size. w:sz is in HALF-points, so 24 = 12pt;
|
|
946
|
-
// forgetting the halving makes every imported document twice the
|
|
947
|
-
// size it should be.
|
|
948
|
-
var css = "";
|
|
949
|
-
var col = firstChildEl(rpr, "color");
|
|
950
|
-
if (col) {
|
|
951
|
-
var cv = col.getAttributeNS(W, "val") || col.getAttribute("w:val") || "";
|
|
952
|
-
if (cv && /^[0-9A-Fa-f]{6}$/.test(cv)) css += "color:#" + cv + ";";
|
|
953
|
-
}
|
|
954
|
-
var hl = firstChildEl(rpr, "highlight");
|
|
955
|
-
if (hl) {
|
|
956
|
-
var hv = hl.getAttributeNS(W, "val") || hl.getAttribute("w:val") || "";
|
|
957
|
-
if (hv && hv !== "none") css += "background-color:" + hv + ";";
|
|
958
|
-
}
|
|
959
|
-
var sz = firstChildEl(rpr, "sz");
|
|
960
|
-
if (sz) {
|
|
961
|
-
var sv = parseInt(sz.getAttributeNS(W, "val") || sz.getAttribute("w:val") || "", 10);
|
|
962
|
-
if (sv > 0) css += "font-size:" + (sv / 2) + "pt;";
|
|
963
|
-
}
|
|
964
|
-
if (css) txt = '<span style="' + css + '">' + txt + "</span>";
|
|
965
|
-
}
|
|
966
|
-
return pics + txt;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// Block-level content discovered while rendering a paragraph: text
|
|
970
|
-
// boxes, SmartArt outlines, and placeholders for images the browser
|
|
971
|
-
// cannot render. None of them may be emitted inline inside the
|
|
972
|
-
// paragraph that referenced them -- a <div> inside a <p> is invalid and
|
|
973
|
-
// browsers silently close the paragraph around it -- so each is
|
|
974
|
-
// buffered here, already wrapped, and flushed straight after.
|
|
975
|
-
var pendingBlocks = [];
|
|
976
|
-
function queueBlock(html) {
|
|
977
|
-
// Dedup on the whole string: an mc:AlternateContent block carries
|
|
978
|
-
// the same graphic twice, once per rendering flavour.
|
|
979
|
-
if (html && pendingBlocks.indexOf(html) < 0) pendingBlocks.push(html);
|
|
980
|
-
}
|
|
981
|
-
function collectTextBoxes(node) {
|
|
982
|
-
var boxes = node.getElementsByTagName("*");
|
|
983
|
-
for (var i = 0; i < boxes.length; i++) {
|
|
984
|
-
if (boxes[i].localName !== "txbxContent") continue;
|
|
985
|
-
var inner = "";
|
|
986
|
-
var kids = boxes[i].childNodes;
|
|
987
|
-
for (var k = 0; k < kids.length; k++) {
|
|
988
|
-
var c = kids[k];
|
|
989
|
-
if (c.nodeType !== 1) continue;
|
|
990
|
-
if (c.localName === "p") {
|
|
991
|
-
var t = paraInner(c);
|
|
992
|
-
if (t) inner += "<p>" + t + "</p>";
|
|
993
|
-
} else if (c.localName === "tbl") {
|
|
994
|
-
inner += tableHtml(c);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
if (inner) queueBlock('<div class="rte-imported-textbox" style="' + TEXTBOX_STYLE + '">' + inner + "</div>");
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
function flushBlocks() {
|
|
1002
|
-
if (!pendingBlocks.length) return "";
|
|
1003
|
-
var html = pendingBlocks.join("");
|
|
1004
|
-
pendingBlocks = [];
|
|
1005
|
-
return html;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Emit a marker in the shape footnotes.js already understands, and stash
|
|
1009
|
-
// the body so the notes section can be built at the end. The plugin
|
|
1010
|
-
// renumbers on load, so the number written here is provisional.
|
|
1011
|
-
var importedNotes = [];
|
|
1012
|
-
// OMML -> the editor's own inline-math span, so an imported equation is
|
|
1013
|
-
// editable in the math dialog and renders through whatever renderer the
|
|
1014
|
-
// host page loads. The visible text is the TeX itself: that is what a
|
|
1015
|
-
// page with no renderer shows, and it is readable rather than blank.
|
|
1016
|
-
function mathHtml(node) {
|
|
1017
|
-
var tex = ommlToTex(node);
|
|
1018
|
-
if (!tex) return "";
|
|
1019
|
-
var display = node.localName === "oMathPara";
|
|
1020
|
-
return '<span class="rte-math-inline' + (display ? " rte-math-display" : "") +
|
|
1021
|
-
'" data-tex="' + esc(tex) + '">' + esc(tex) + "</span>";
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function footnoteMarker(text) {
|
|
1025
|
-
var id = "fnimp" + (importedNotes.length + 1);
|
|
1026
|
-
importedNotes.push({ id: id, text: text });
|
|
1027
|
-
var n = importedNotes.length;
|
|
1028
|
-
return '<sup class="rte-fn-ref" data-fn-id="' + id + '" data-fn-number="' + n +
|
|
1029
|
-
'" id="fnref-' + id + '" contenteditable="false">' + n + "</sup>";
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
function footnotesSection() {
|
|
1033
|
-
if (!importedNotes.length) return "";
|
|
1034
|
-
var items = "";
|
|
1035
|
-
for (var i = 0; i < importedNotes.length; i++) {
|
|
1036
|
-
var f = importedNotes[i];
|
|
1037
|
-
items += '<li class="rte-fn-note" data-fn-id="' + f.id + '" data-fn-number="' + (i + 1) +
|
|
1038
|
-
'" id="fn-' + f.id + '">' + esc(f.text) +
|
|
1039
|
-
'<a class="rte-fn-back" data-fn-id="' + f.id + '" href="#fnref-' + f.id +
|
|
1040
|
-
'" contenteditable="false" title="Back to reference">↩</a></li>';
|
|
1041
|
-
}
|
|
1042
|
-
return '<section class="rte-footnotes"><h2 class="rte-fn-title">Footnotes</h2>' +
|
|
1043
|
-
'<ol class="rte-fn-list" data-numbering="decimal">' + items + "</ol></section>";
|
|
1044
|
-
}
|
|
1045
|
-
// Runs inside a w:ins / w:del wrapper, rendered with the review markup
|
|
1046
|
-
// that the DOCX *export* already round-trips (<ins data-author>/<del>).
|
|
1047
|
-
function revisionHtml(node, tag) {
|
|
1048
|
-
var inner = "";
|
|
1049
|
-
for (var i = 0; i < node.childNodes.length; i++) {
|
|
1050
|
-
var c = node.childNodes[i];
|
|
1051
|
-
if (c.nodeType !== 1) continue;
|
|
1052
|
-
if (c.localName === "r") inner += runHtml(c);
|
|
1053
|
-
else if (c.localName === "ins") inner += revisionHtml(c, "ins");
|
|
1054
|
-
else if (c.localName === "del") inner += revisionHtml(c, "del");
|
|
1055
|
-
}
|
|
1056
|
-
if (!inner) return "";
|
|
1057
|
-
var author = node.getAttributeNS(W, "author") || node.getAttribute("w:author") || "";
|
|
1058
|
-
var date = node.getAttributeNS(W, "date") || node.getAttribute("w:date") || "";
|
|
1059
|
-
return "<" + tag +
|
|
1060
|
-
(author ? ' data-author="' + esc(author) + '"' : "") +
|
|
1061
|
-
(date ? ' data-date="' + esc(date) + '"' : "") +
|
|
1062
|
-
">" + inner + "</" + tag + ">";
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
function paraInner(p) {
|
|
1066
|
-
var h = "", kids = p.childNodes;
|
|
1067
|
-
// Word marks a commented RANGE with start/end markers around the runs,
|
|
1068
|
-
// so the wrapper has to be opened and closed as we walk, not derived
|
|
1069
|
-
// from any single element.
|
|
1070
|
-
var openComments = [];
|
|
1071
|
-
function commentOpen(id) {
|
|
1072
|
-
var c = ctx.comments[String(id)];
|
|
1073
|
-
if (!c) return "";
|
|
1074
|
-
openComments.push(id);
|
|
1075
|
-
return '<span data-comment="' + esc(c.text) + '"' +
|
|
1076
|
-
(c.author ? ' data-comment-author="' + esc(c.author) + '"' : "") + ">";
|
|
1077
|
-
}
|
|
1078
|
-
for (var i = 0; i < kids.length; i++) {
|
|
1079
|
-
var c = kids[i];
|
|
1080
|
-
if (c.nodeType === 1 && c.localName === "commentRangeStart") {
|
|
1081
|
-
h += commentOpen(c.getAttributeNS(W, "id") || c.getAttribute("w:id"));
|
|
1082
|
-
continue;
|
|
1083
|
-
}
|
|
1084
|
-
if (c.nodeType === 1 && c.localName === "commentRangeEnd") {
|
|
1085
|
-
if (openComments.length) { openComments.pop(); h += "</span>"; }
|
|
1086
|
-
continue;
|
|
1087
|
-
}
|
|
1088
|
-
if (c.nodeType === 1 && (c.localName === "ins" || c.localName === "del") && c.namespaceURI === W) {
|
|
1089
|
-
h += revisionHtml(c, c.localName);
|
|
1090
|
-
continue;
|
|
1091
|
-
}
|
|
1092
|
-
// EQUATIONS. OMML is a sibling of the runs, in its own
|
|
1093
|
-
// namespace -- a walker that only recognises w:r skips it and
|
|
1094
|
-
// the equation is gone. m:oMathPara is the display (own-line)
|
|
1095
|
-
// wrapper around one or more m:oMath.
|
|
1096
|
-
if (c.nodeType === 1 && c.namespaceURI === OMML_NS &&
|
|
1097
|
-
(c.localName === "oMath" || c.localName === "oMathPara")) {
|
|
1098
|
-
h += mathHtml(c);
|
|
1099
|
-
continue;
|
|
1100
|
-
}
|
|
1101
|
-
if (c.nodeType === 1 && c.localName === "r" && c.namespaceURI === W) h += runHtml(c);
|
|
1102
|
-
else if (c.nodeType === 1 && c.localName === "hyperlink") {
|
|
1103
|
-
var inner = "";
|
|
1104
|
-
for (var j = 0; j < c.childNodes.length; j++) if (c.childNodes[j].localName === "r") inner += runHtml(c.childNodes[j]);
|
|
1105
|
-
// Resolve r:id through the rels part. Previously the run text
|
|
1106
|
-
// was emitted bare and the URL was thrown away, so every link
|
|
1107
|
-
// in an imported document became plain text.
|
|
1108
|
-
var rel = relTarget(c);
|
|
1109
|
-
var anchor = c.getAttributeNS(W, "anchor") || c.getAttribute("w:anchor");
|
|
1110
|
-
var href = rel ? rel.target : (anchor ? "#" + anchor : null);
|
|
1111
|
-
h += href ? ('<a href="' + esc(href) + '">' + (inner || esc(href)) + "</a>") : inner;
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
// A comment range left open at the end of the paragraph would leak an
|
|
1115
|
-
// unbalanced <span> into the document.
|
|
1116
|
-
while (openComments.length) { openComments.pop(); h += "</span>"; }
|
|
1117
|
-
return h;
|
|
1118
|
-
}
|
|
1119
|
-
function paraStyle(p) {
|
|
1120
|
-
var ppr = firstChildEl(p, "pPr"); if (!ppr) return {};
|
|
1121
|
-
var info = {};
|
|
1122
|
-
var ps = firstChildEl(ppr, "pStyle");
|
|
1123
|
-
if (ps) {
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
info.
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
var
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
var
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
if (node.
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
}
|
|
1
|
+
if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
|
|
2
|
+
|
|
3
|
+
// 2026-06-09 Document import — the natural pair to Export-to-Word/Markdown/PDF.
|
|
4
|
+
// Opens a local file and loads it into the editor. Library-free for the common
|
|
5
|
+
// formats: Markdown (.md/.markdown via the core fromMarkdown engine), HTML
|
|
6
|
+
// (.html/.htm), plain text (.txt), and Word's HTML-based format (.doc, which
|
|
7
|
+
// Word saves as MSO-flavored HTML) — Word junk (mso-* styles, o:/w: tags,
|
|
8
|
+
// conditional comments) is stripped the same way paste-from-Word is.
|
|
9
|
+
//
|
|
10
|
+
// .docx IS parsed library-free: it is a ZIP of XML, and the ZIP central
|
|
11
|
+
// directory is read directly while DecompressionStream("deflate-raw") handles
|
|
12
|
+
// the inflate. No dependency, no server round-trip.
|
|
13
|
+
// (This comment used to say .docx was not parseable library-free. That was true
|
|
14
|
+
// when it was written and stopped being true when the ZIP reader landed below;
|
|
15
|
+
// it then misled a reader into reporting the feature as missing. Kept explicit
|
|
16
|
+
// so it does not happen again.)
|
|
17
|
+
//
|
|
18
|
+
// 2026-07-28 fidelity pass — what a naive converter silently loses, and what
|
|
19
|
+
// this now preserves:
|
|
20
|
+
// - HYPERLINK URLS. w:hyperlink carries only an r:id; the address lives in
|
|
21
|
+
// word/_rels/document.xml.rels. Without resolving it every link imports as
|
|
22
|
+
// plain text with the URL discarded.
|
|
23
|
+
// - IMAGES. w:drawing -> a:blip r:embed -> word/media/*, inlined as data URIs
|
|
24
|
+
// (set config.documentImportImages = false to skip, for image-heavy files).
|
|
25
|
+
// - ORDERED vs BULLET lists and NESTING. document.xml stores every list the
|
|
26
|
+
// same way; only word/numbering.xml says which is which, so a naive reader
|
|
27
|
+
// turns numbered lists into bullets. w:ilvl gives real nesting, placed
|
|
28
|
+
// inside the parent <li> so the markup is valid.
|
|
29
|
+
// - Paragraph alignment (w:jc).
|
|
30
|
+
// Supply config.documentImportResolver(file) to override any type (e.g. to run
|
|
31
|
+
// a server-side converter for formats this does not cover).
|
|
32
|
+
//
|
|
33
|
+
// API:
|
|
34
|
+
// editor.openImportDialog(options?) -> file picker, then import
|
|
35
|
+
// editor.importFile(file, options?) -> import a File object (Promise)
|
|
36
|
+
// editor.htmlFromImportText(text, kind) -> convert text to HTML (kind: md|html|txt|doc)
|
|
37
|
+
// Command: exec_command "importdocument" opens the picker. Slash: "/import".
|
|
38
|
+
// Config:
|
|
39
|
+
// config.documentImport = false // disable
|
|
40
|
+
// config.documentImportMode = "replace" | "insert" // default "replace"
|
|
41
|
+
// config.documentImportAccept = ".md,.markdown,.html,.htm,.txt,.doc,.docx"
|
|
42
|
+
// config.documentImportResolver = function(file){ return htmlOrPromise; }
|
|
43
|
+
RTE_DefaultConfig.plugin_documentimport = RTE_Plugin_DocumentImport;
|
|
44
|
+
if (typeof RTE_DefaultConfig.documentImport === "undefined") RTE_DefaultConfig.documentImport = true;
|
|
45
|
+
|
|
46
|
+
function RTE_Plugin_DocumentImport() {
|
|
47
|
+
var obj = this;
|
|
48
|
+
var config, editor;
|
|
49
|
+
|
|
50
|
+
obj.PluginName = "DocumentImport";
|
|
51
|
+
|
|
52
|
+
obj.InitConfig = function (argconfig) { config = argconfig; };
|
|
53
|
+
|
|
54
|
+
obj.InitEditor = function (argeditor) {
|
|
55
|
+
editor = argeditor;
|
|
56
|
+
if (config.documentImport === false) return;
|
|
57
|
+
|
|
58
|
+
editor.htmlFromImportText = function (text, kind) { return toHtml(String(text == null ? "" : text), kind); };
|
|
59
|
+
editor.importFile = function (file, options) { return importFile(file, options || {}); };
|
|
60
|
+
editor.openImportDialog = function (options) { return openPicker(options || {}); };
|
|
61
|
+
|
|
62
|
+
editor.attachEvent("exec_command_importdocument", function (state) {
|
|
63
|
+
state.returnValue = true;
|
|
64
|
+
state.stopBubble = true;
|
|
65
|
+
openPicker({});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (editor.slashCommands && typeof editor.slashCommands.register === "function") {
|
|
69
|
+
try {
|
|
70
|
+
editor.slashCommands.register({
|
|
71
|
+
id: "import-document",
|
|
72
|
+
title: "Import document",
|
|
73
|
+
description: "Open a Markdown, HTML, text, or Word (.doc) file into the editor",
|
|
74
|
+
keywords: ["import", "open", "file", "word", "markdown", "upload"],
|
|
75
|
+
action: function () { openPicker({}); }
|
|
76
|
+
});
|
|
77
|
+
} catch (e) {}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function kindFromName(name) {
|
|
82
|
+
var n = String(name || "").toLowerCase();
|
|
83
|
+
if (/\.(md|markdown|mdown|mkd)$/.test(n)) return "md";
|
|
84
|
+
if (/\.html?$/.test(n)) return "html";
|
|
85
|
+
if (/\.doc$/.test(n)) return "doc";
|
|
86
|
+
if (/\.docx$/.test(n)) return "docx";
|
|
87
|
+
if (/\.txt$/.test(n)) return "txt";
|
|
88
|
+
return "txt";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function esc(s) {
|
|
92
|
+
return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A Word text box and a SmartArt graphic are BOXES on the page. Importing
|
|
96
|
+
// them as bare <div>s keeps the words but loses the thing that made them
|
|
97
|
+
// read as separate from the body text, so the import looks lossier than it
|
|
98
|
+
// is. Styled inline rather than by class because this is saved content: it
|
|
99
|
+
// has to survive wherever the host renders it, with no stylesheet of ours
|
|
100
|
+
// loaded. (Found by check-css-contract.mjs -- the classes were emitted with
|
|
101
|
+
// no rule anywhere.)
|
|
102
|
+
var TEXTBOX_STYLE = "border:1px solid #b9c2cf;border-radius:4px;padding:10px 14px;margin:12px 0;background:#fbfcfe";
|
|
103
|
+
var SMARTART_STYLE = "border:1px solid #b9c2cf;border-radius:4px;padding:10px 14px 10px 6px;margin:12px 0;background:#f7faff";
|
|
104
|
+
|
|
105
|
+
// Strip Word/MSO debris from .doc HTML (same intent as paste-from-Word).
|
|
106
|
+
function cleanWordHtml(html) {
|
|
107
|
+
var h = String(html || "");
|
|
108
|
+
// body only, drop head/xml/style islands and conditional comments
|
|
109
|
+
var bodyMatch = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(h);
|
|
110
|
+
if (bodyMatch) h = bodyMatch[1];
|
|
111
|
+
h = h.replace(/<!--\[if[\s\S]*?\[endif\]-->/gi, "");
|
|
112
|
+
h = h.replace(/<!--[\s\S]*?-->/g, "");
|
|
113
|
+
h = h.replace(/<\/?(o:p|o:|w:|xml|style|meta|link|title|head)[^>]*>/gi, "");
|
|
114
|
+
h = h.replace(/<\\?\?xml[^>]*>/gi, "");
|
|
115
|
+
// strip mso-* declarations + empty style/class/lang attrs
|
|
116
|
+
h = h.replace(/\sstyle="[^"]*"/gi, function (m) {
|
|
117
|
+
var cleaned = m.replace(/mso-[^;"]*;?/gi, "").replace(/style="\s*;*\s*"/i, "");
|
|
118
|
+
return /style="\s*"/.test(cleaned) || cleaned === ' style=""' ? "" : cleaned;
|
|
119
|
+
});
|
|
120
|
+
h = h.replace(/\sclass="Mso[^"]*"/gi, "");
|
|
121
|
+
h = h.replace(/\s(lang|xmlns(:\w+)?)="[^"]*"/gi, "");
|
|
122
|
+
return h;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function toHtml(text, kind) {
|
|
126
|
+
switch (kind) {
|
|
127
|
+
case "md":
|
|
128
|
+
if (editor && typeof editor.fromMarkdown === "function") return editor.fromMarkdown(text, { apply: false });
|
|
129
|
+
return "<p>" + esc(text).replace(/\n{2,}/g, "</p><p>").replace(/\n/g, "<br>") + "</p>";
|
|
130
|
+
case "html":
|
|
131
|
+
return text;
|
|
132
|
+
case "doc":
|
|
133
|
+
return cleanWordHtml(text);
|
|
134
|
+
case "txt":
|
|
135
|
+
default:
|
|
136
|
+
var paras = String(text).replace(/\r\n?/g, "\n").split(/\n{2,}/);
|
|
137
|
+
return paras.map(function (p) {
|
|
138
|
+
return "<p>" + esc(p).replace(/\n/g, "<br>") + "</p>";
|
|
139
|
+
}).join("");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function applyHtml(html, mode) {
|
|
144
|
+
if (html == null) return false;
|
|
145
|
+
if (mode === "insert") {
|
|
146
|
+
if (typeof editor.insertHTML === "function") { editor.insertHTML(html); return true; }
|
|
147
|
+
}
|
|
148
|
+
if (typeof editor.setHTMLCode === "function") { editor.setHTMLCode(html); return true; }
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function importFile(file, options) {
|
|
153
|
+
var mode = options.mode || config.documentImportMode || "replace";
|
|
154
|
+
return new Promise(function (resolve) {
|
|
155
|
+
if (!file) { resolve(false); return; }
|
|
156
|
+
var kind = kindFromName(file.name);
|
|
157
|
+
|
|
158
|
+
// BYOK resolver wins for any type (lets a host handle .docx etc.).
|
|
159
|
+
if (typeof config.documentImportResolver === "function") {
|
|
160
|
+
try {
|
|
161
|
+
var r = config.documentImportResolver(file);
|
|
162
|
+
Promise.resolve(r).then(function (html) {
|
|
163
|
+
if (typeof html === "string") { resolve(applyHtml(html, mode)); }
|
|
164
|
+
else { builtin(); }
|
|
165
|
+
}, function () { builtin(); });
|
|
166
|
+
return;
|
|
167
|
+
} catch (e) { builtin(); return; }
|
|
168
|
+
}
|
|
169
|
+
builtin();
|
|
170
|
+
|
|
171
|
+
function builtin() {
|
|
172
|
+
if (kind === "docx") {
|
|
173
|
+
// Library-free .docx: unzip word/document.xml (native
|
|
174
|
+
// DecompressionStream) + transform WordprocessingML -> HTML.
|
|
175
|
+
if (!docxSupported()) {
|
|
176
|
+
notify("This browser can't unpack .docx (no DecompressionStream). Save as .doc/.html/.md, or wire config.documentImportResolver.");
|
|
177
|
+
resolve(false);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
readDocx(file).then(function (html) {
|
|
181
|
+
if (html != null) resolve(applyHtml(html, mode));
|
|
182
|
+
else { notify("Could not read this .docx file."); resolve(false); }
|
|
183
|
+
}, function () { notify("Could not read this .docx file."); resolve(false); });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (typeof FileReader === "undefined") { resolve(false); return; }
|
|
187
|
+
var reader = new FileReader();
|
|
188
|
+
reader.onload = function () {
|
|
189
|
+
var html = toHtml(reader.result, kind);
|
|
190
|
+
resolve(applyHtml(html, mode));
|
|
191
|
+
};
|
|
192
|
+
reader.onerror = function () { resolve(false); };
|
|
193
|
+
reader.readAsText(file);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function openPicker(options) {
|
|
199
|
+
try {
|
|
200
|
+
var input = document.createElement("input");
|
|
201
|
+
input.type = "file";
|
|
202
|
+
input.accept = options.accept || config.documentImportAccept || ".md,.markdown,.html,.htm,.txt,.doc,.docx";
|
|
203
|
+
input.style.position = "fixed";
|
|
204
|
+
input.style.left = "-9999px";
|
|
205
|
+
input.addEventListener("change", function () {
|
|
206
|
+
var f = input.files && input.files[0];
|
|
207
|
+
if (f) importFile(f, options);
|
|
208
|
+
setTimeout(function () { try { document.body.removeChild(input); } catch (e) {} }, 0);
|
|
209
|
+
});
|
|
210
|
+
document.body.appendChild(input);
|
|
211
|
+
input.click();
|
|
212
|
+
return true;
|
|
213
|
+
} catch (e) {
|
|
214
|
+
if (window.console) console.error("documentimport: openPicker failed", e);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---- library-free .docx (ZIP of XML) ----
|
|
220
|
+
function docxSupported() {
|
|
221
|
+
return typeof DecompressionStream === "function" && typeof DOMParser === "function" &&
|
|
222
|
+
(typeof Response === "function" || typeof Blob === "function");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Locate an entry via the ZIP central directory (reliable comp sizes/offsets).
|
|
226
|
+
function findZipEntry(bytes, wantName) {
|
|
227
|
+
var dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
228
|
+
// End Of Central Directory: signature 0x06054b50, scan from the end.
|
|
229
|
+
var eocd = -1;
|
|
230
|
+
for (var i = bytes.length - 22; i >= 0 && i >= bytes.length - 22 - 65536; i--) {
|
|
231
|
+
if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
|
|
232
|
+
}
|
|
233
|
+
if (eocd < 0) return null;
|
|
234
|
+
var cdOffset = dv.getUint32(eocd + 16, true);
|
|
235
|
+
var cdCount = dv.getUint16(eocd + 10, true);
|
|
236
|
+
var p = cdOffset;
|
|
237
|
+
for (var n = 0; n < cdCount; n++) {
|
|
238
|
+
if (dv.getUint32(p, true) !== 0x02014b50) break;
|
|
239
|
+
var method = dv.getUint16(p + 10, true);
|
|
240
|
+
var compSize = dv.getUint32(p + 20, true);
|
|
241
|
+
var nameLen = dv.getUint16(p + 28, true);
|
|
242
|
+
var extraLen = dv.getUint16(p + 30, true);
|
|
243
|
+
var commentLen = dv.getUint16(p + 32, true);
|
|
244
|
+
var localOff = dv.getUint32(p + 42, true);
|
|
245
|
+
// Normalize separators: the ZIP spec mandates "/", but some Windows
|
|
246
|
+
// tools (.NET ZipFile on older runtimes) emit "\".
|
|
247
|
+
var name = utf8(bytes.subarray(p + 46, p + 46 + nameLen)).replace(/\\/g, "/");
|
|
248
|
+
if (name === wantName) {
|
|
249
|
+
var lh = new DataView(bytes.buffer, bytes.byteOffset + localOff, 30);
|
|
250
|
+
var lNameLen = lh.getUint16(26, true);
|
|
251
|
+
var lExtraLen = lh.getUint16(28, true);
|
|
252
|
+
var dataStart = localOff + 30 + lNameLen + lExtraLen;
|
|
253
|
+
return { method: method, data: bytes.subarray(dataStart, dataStart + compSize) };
|
|
254
|
+
}
|
|
255
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function utf8(u8) {
|
|
261
|
+
try { return new TextDecoder("utf-8").decode(u8); } catch (e) {
|
|
262
|
+
var s = ""; for (var i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]); return s;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function inflateRaw(u8) {
|
|
267
|
+
// method 0 = stored, 8 = deflate (raw).
|
|
268
|
+
var ds = new DecompressionStream("deflate-raw");
|
|
269
|
+
var blob = new Blob([u8]);
|
|
270
|
+
return new Response(blob.stream().pipeThrough(ds)).arrayBuffer().then(function (ab) {
|
|
271
|
+
return new Uint8Array(ab);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Every entry whose name matches, decompressed. Needed because a faithful
|
|
276
|
+
// conversion is not just document.xml: the hyperlink URLs live in the rels
|
|
277
|
+
// part, whether a list is bulleted or numbered lives in numbering.xml, and
|
|
278
|
+
// the images live in word/media.
|
|
279
|
+
function listZipEntries(bytes) {
|
|
280
|
+
var dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
281
|
+
var eocd = -1;
|
|
282
|
+
for (var i = bytes.length - 22; i >= 0 && i >= bytes.length - 22 - 65536; i--) {
|
|
283
|
+
if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
|
|
284
|
+
}
|
|
285
|
+
if (eocd < 0) return [];
|
|
286
|
+
var cdOffset = dv.getUint32(eocd + 16, true);
|
|
287
|
+
var cdCount = dv.getUint16(eocd + 10, true);
|
|
288
|
+
var p = cdOffset, out = [];
|
|
289
|
+
for (var n = 0; n < cdCount; n++) {
|
|
290
|
+
if (dv.getUint32(p, true) !== 0x02014b50) break;
|
|
291
|
+
var method = dv.getUint16(p + 10, true);
|
|
292
|
+
var compSize = dv.getUint32(p + 20, true);
|
|
293
|
+
var nameLen = dv.getUint16(p + 28, true);
|
|
294
|
+
var extraLen = dv.getUint16(p + 30, true);
|
|
295
|
+
var commentLen = dv.getUint16(p + 32, true);
|
|
296
|
+
var localOff = dv.getUint32(p + 42, true);
|
|
297
|
+
var name = utf8(bytes.subarray(p + 46, p + 46 + nameLen)).replace(/\\/g, "/");
|
|
298
|
+
var lh = new DataView(bytes.buffer, bytes.byteOffset + localOff, 30);
|
|
299
|
+
var dataStart = localOff + 30 + lh.getUint16(26, true) + lh.getUint16(28, true);
|
|
300
|
+
out.push({ name: name, method: method, data: bytes.subarray(dataStart, dataStart + compSize) });
|
|
301
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function entryBytes(entry) {
|
|
307
|
+
return entry.method === 0 ? Promise.resolve(entry.data) : inflateRaw(entry.data);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function mimeForExt(name) {
|
|
311
|
+
var e = (name.split(".").pop() || "").toLowerCase();
|
|
312
|
+
if (e === "png") return "image/png";
|
|
313
|
+
if (e === "jpg" || e === "jpeg") return "image/jpeg";
|
|
314
|
+
if (e === "gif") return "image/gif";
|
|
315
|
+
if (e === "bmp") return "image/bmp";
|
|
316
|
+
if (e === "webp") return "image/webp";
|
|
317
|
+
if (e === "svg") return "image/svg+xml";
|
|
318
|
+
return null; // wmf/emf and friends have no browser-renderable form
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function bytesToDataUri(u8, mime) {
|
|
322
|
+
var CHUNK = 0x8000, s = "";
|
|
323
|
+
for (var i = 0; i < u8.length; i += CHUNK) {
|
|
324
|
+
s += String.fromCharCode.apply(null, u8.subarray(i, i + CHUNK));
|
|
325
|
+
}
|
|
326
|
+
return "data:" + mime + ";base64," + btoa(s);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function readDocx(file) {
|
|
330
|
+
var bufPromise = file.arrayBuffer ? file.arrayBuffer() : new Promise(function (res, rej) {
|
|
331
|
+
var r = new FileReader(); r.onload = function () { res(r.result); }; r.onerror = rej; r.readAsArrayBuffer(file);
|
|
332
|
+
});
|
|
333
|
+
return bufPromise.then(function (ab) {
|
|
334
|
+
var bytes = new Uint8Array(ab);
|
|
335
|
+
var entries = listZipEntries(bytes);
|
|
336
|
+
var byName = {};
|
|
337
|
+
for (var i = 0; i < entries.length; i++) byName[entries[i].name] = entries[i];
|
|
338
|
+
if (!byName["word/document.xml"]) return null;
|
|
339
|
+
|
|
340
|
+
var ctx = { rels: {}, numbering: {}, media: {}, footnotes: {}, endnotes: {}, comments: {}, diagrams: {}, vectorNames: {} };
|
|
341
|
+
|
|
342
|
+
// Images are opt-in: a document full of photographs would otherwise
|
|
343
|
+
// inline megabytes of base64 into the editor.
|
|
344
|
+
var wantMedia = config.documentImportImages !== false;
|
|
345
|
+
|
|
346
|
+
var jobs = [entryBytes(byName["word/document.xml"]).then(function (b) { ctx.documentXml = utf8(b); })];
|
|
347
|
+
|
|
348
|
+
if (byName["word/_rels/document.xml.rels"]) {
|
|
349
|
+
jobs.push(entryBytes(byName["word/_rels/document.xml.rels"]).then(function (b) {
|
|
350
|
+
ctx.rels = parseRels(utf8(b));
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
if (byName["word/numbering.xml"]) {
|
|
354
|
+
jobs.push(entryBytes(byName["word/numbering.xml"]).then(function (b) {
|
|
355
|
+
ctx.numbering = parseNumbering(utf8(b));
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
// Footnotes live in their OWN part; document.xml only carries
|
|
359
|
+
// w:footnoteReference with an id. Skip this and a footnoted contract
|
|
360
|
+
// imports with every citation silently deleted.
|
|
361
|
+
if (byName["word/footnotes.xml"]) {
|
|
362
|
+
jobs.push(entryBytes(byName["word/footnotes.xml"]).then(function (b) {
|
|
363
|
+
ctx.footnotes = parseFootnotes(utf8(b), "footnote");
|
|
364
|
+
}));
|
|
365
|
+
}
|
|
366
|
+
// Comment bodies live in their own part too, keyed by id; the
|
|
367
|
+
// document only marks the commented RANGE.
|
|
368
|
+
// Endnotes are structurally identical to footnotes, in their own part.
|
|
369
|
+
if (byName["word/endnotes.xml"]) {
|
|
370
|
+
jobs.push(entryBytes(byName["word/endnotes.xml"]).then(function (b) {
|
|
371
|
+
ctx.endnotes = parseFootnotes(utf8(b), "endnote");
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
if (byName["word/comments.xml"]) {
|
|
375
|
+
jobs.push(entryBytes(byName["word/comments.xml"]).then(function (b) {
|
|
376
|
+
ctx.comments = parseComments(utf8(b));
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
// SmartArt. The drawing in document.xml holds only relationship ids;
|
|
380
|
+
// every word of the diagram lives in word/diagrams/dataN.xml, in the
|
|
381
|
+
// DrawingML *diagram* namespace. A reader that looks for a:blip finds
|
|
382
|
+
// nothing and drops the whole graphic, so an org chart or a process
|
|
383
|
+
// flow imports as empty space.
|
|
384
|
+
for (var dg = 0; dg < entries.length; dg++) {
|
|
385
|
+
(function (ent) {
|
|
386
|
+
if (!/^word\/diagrams\/data\d*\.xml$/.test(ent.name)) return;
|
|
387
|
+
jobs.push(entryBytes(ent).then(function (b) {
|
|
388
|
+
try { ctx.diagrams[ent.name] = parseDiagram(utf8(b)); } catch (e) {}
|
|
389
|
+
}));
|
|
390
|
+
})(entries[dg]);
|
|
391
|
+
}
|
|
392
|
+
if (wantMedia) {
|
|
393
|
+
for (var n = 0; n < entries.length; n++) {
|
|
394
|
+
(function (ent) {
|
|
395
|
+
if (ent.name.indexOf("word/media/") !== 0) return;
|
|
396
|
+
var mime = mimeForExt(ent.name);
|
|
397
|
+
if (!mime) return;
|
|
398
|
+
jobs.push(entryBytes(ent).then(function (b) {
|
|
399
|
+
try { ctx.media[ent.name] = bytesToDataUri(b, mime); } catch (e) {}
|
|
400
|
+
}));
|
|
401
|
+
})(entries[n]);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return Promise.all(jobs).then(function () { return docxXmlToHtml(ctx.documentXml, ctx); });
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// r:id -> target. Without this, every hyperlink in the document survives as
|
|
410
|
+
// plain text with its URL silently discarded.
|
|
411
|
+
function parseRels(xml) {
|
|
412
|
+
var map = {};
|
|
413
|
+
try {
|
|
414
|
+
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
415
|
+
var rs = d.getElementsByTagName("*");
|
|
416
|
+
for (var i = 0; i < rs.length; i++) {
|
|
417
|
+
if (rs[i].localName !== "Relationship") continue;
|
|
418
|
+
var id = rs[i].getAttribute("Id");
|
|
419
|
+
var target = rs[i].getAttribute("Target");
|
|
420
|
+
var mode = rs[i].getAttribute("TargetMode");
|
|
421
|
+
if (id && target) map[id] = { target: target, external: mode === "External" };
|
|
422
|
+
}
|
|
423
|
+
} catch (e) {}
|
|
424
|
+
return map;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// w:id -> plain text of the footnote body.
|
|
428
|
+
// Word reserves ids 0 and -1 for the separator rules drawn above footnotes;
|
|
429
|
+
// those carry type="separator"/"continuationSeparator" and are not content.
|
|
430
|
+
// tagName is "footnote" or "endnote": the two parts share a schema but NOT
|
|
431
|
+
// an element name, so parsing endnotes.xml with "footnote" silently returns
|
|
432
|
+
// nothing at all.
|
|
433
|
+
function parseFootnotes(xml, tagName) {
|
|
434
|
+
var out = {};
|
|
435
|
+
try {
|
|
436
|
+
var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
437
|
+
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
438
|
+
var notes = d.getElementsByTagNameNS(W, tagName || "footnote");
|
|
439
|
+
for (var i = 0; i < notes.length; i++) {
|
|
440
|
+
var n = notes[i];
|
|
441
|
+
var type = n.getAttributeNS(W, "type") || n.getAttribute("w:type") || "";
|
|
442
|
+
if (type) continue; // separator, not a real note
|
|
443
|
+
var id = n.getAttributeNS(W, "id") || n.getAttribute("w:id");
|
|
444
|
+
if (id == null) continue;
|
|
445
|
+
// Body text only; the leading footnote-reference mark inside the
|
|
446
|
+
// note is Word's own numbering and would duplicate ours.
|
|
447
|
+
var txt = (n.textContent || "").replace(/\s+/g, " ").trim();
|
|
448
|
+
if (txt) out[String(id)] = txt;
|
|
449
|
+
}
|
|
450
|
+
} catch (e) {}
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// w:id -> { text, author }
|
|
455
|
+
function parseComments(xml) {
|
|
456
|
+
var out = {};
|
|
457
|
+
try {
|
|
458
|
+
var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
459
|
+
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
460
|
+
var cs = d.getElementsByTagNameNS(W, "comment");
|
|
461
|
+
for (var i = 0; i < cs.length; i++) {
|
|
462
|
+
var c = cs[i];
|
|
463
|
+
var id = c.getAttributeNS(W, "id") || c.getAttribute("w:id");
|
|
464
|
+
if (id == null) continue;
|
|
465
|
+
out[String(id)] = {
|
|
466
|
+
text: (c.textContent || "").replace(/\s+/g, " ").trim(),
|
|
467
|
+
author: c.getAttributeNS(W, "author") || c.getAttribute("w:author") || ""
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
} catch (e) {}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// word/diagrams/dataN.xml -> the diagram's text, one entry per node, in
|
|
475
|
+
// document order.
|
|
476
|
+
//
|
|
477
|
+
// SmartArt has no browser equivalent -- it is a laid-out graphic driven by a
|
|
478
|
+
// layout algorithm -- but its CONTENT is ordinary text, and text is what the
|
|
479
|
+
// reader actually needs. An org chart that imports as an outline is usable; an
|
|
480
|
+
// org chart that imports as nothing is a hole in the document.
|
|
481
|
+
//
|
|
482
|
+
// Nodes with presentation-only roles (`dgm:prSet` with no text, and the
|
|
483
|
+
// ptType "pres"/"parTrans"/"sibTrans" connector points) carry no content and
|
|
484
|
+
// would otherwise contribute blank list items.
|
|
485
|
+
function parseDiagram(xml) {
|
|
486
|
+
var out = [];
|
|
487
|
+
try {
|
|
488
|
+
var DGM = "http://schemas.openxmlformats.org/drawingml/2006/diagram";
|
|
489
|
+
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
490
|
+
var pts = d.getElementsByTagNameNS(DGM, "pt");
|
|
491
|
+
for (var i = 0; i < pts.length; i++) {
|
|
492
|
+
var type = pts[i].getAttribute("type") || "";
|
|
493
|
+
if (type === "pres" || type === "parTrans" || type === "sibTrans") continue;
|
|
494
|
+
var t = pts[i].getElementsByTagNameNS(DGM, "t")[0];
|
|
495
|
+
if (!t) continue;
|
|
496
|
+
var text = (t.textContent || "").replace(/\s+/g, " ").trim();
|
|
497
|
+
if (text) out.push(text);
|
|
498
|
+
}
|
|
499
|
+
} catch (e) {}
|
|
500
|
+
return out;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// numId -> { level -> "bullet" | "decimal" | ... }. Word stores every list
|
|
504
|
+
// the same way in document.xml; only numbering.xml says whether it renders
|
|
505
|
+
// as bullets or numbers, which is why a naive converter turns every ordered
|
|
506
|
+
// list into a <ul>.
|
|
507
|
+
function parseNumbering(xml) {
|
|
508
|
+
var out = {};
|
|
509
|
+
try {
|
|
510
|
+
var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
511
|
+
var d = new DOMParser().parseFromString(xml, "application/xml");
|
|
512
|
+
function val(el) { return el ? (el.getAttributeNS(W, "val") || el.getAttribute("w:val") || "") : ""; }
|
|
513
|
+
|
|
514
|
+
// abstractNumId -> { ilvl -> numFmt }
|
|
515
|
+
var abstracts = {};
|
|
516
|
+
var aNums = d.getElementsByTagNameNS(W, "abstractNum");
|
|
517
|
+
for (var i = 0; i < aNums.length; i++) {
|
|
518
|
+
var aid = aNums[i].getAttributeNS(W, "abstractNumId") || aNums[i].getAttribute("w:abstractNumId");
|
|
519
|
+
var levels = {};
|
|
520
|
+
var lvls = aNums[i].getElementsByTagNameNS(W, "lvl");
|
|
521
|
+
for (var j = 0; j < lvls.length; j++) {
|
|
522
|
+
var ilvl = lvls[j].getAttributeNS(W, "ilvl") || lvls[j].getAttribute("w:ilvl") || "0";
|
|
523
|
+
var fmt = val(lvls[j].getElementsByTagNameNS(W, "numFmt")[0]);
|
|
524
|
+
levels[ilvl] = fmt || "decimal";
|
|
525
|
+
}
|
|
526
|
+
if (aid != null) abstracts[aid] = levels;
|
|
527
|
+
}
|
|
528
|
+
// num -> abstractNumId
|
|
529
|
+
var nums = d.getElementsByTagNameNS(W, "num");
|
|
530
|
+
for (var k = 0; k < nums.length; k++) {
|
|
531
|
+
var nid = nums[k].getAttributeNS(W, "numId") || nums[k].getAttribute("w:numId");
|
|
532
|
+
var ref = nums[k].getElementsByTagNameNS(W, "abstractNumId")[0];
|
|
533
|
+
var aref = val(ref);
|
|
534
|
+
if (nid != null && abstracts[aref]) out[nid] = abstracts[aref];
|
|
535
|
+
}
|
|
536
|
+
} catch (e) {}
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ---------------------------------------------------------------------
|
|
541
|
+
// OMML (Office Math Markup) -> LaTeX.
|
|
542
|
+
//
|
|
543
|
+
// Word does not store equations as text, MathML or an image: it stores an
|
|
544
|
+
// <m:oMath> tree in its OWN namespace, interleaved between the runs of a
|
|
545
|
+
// paragraph. A converter that walks w:r elements never sees it, so every
|
|
546
|
+
// equation in the document disappears without a trace -- and because the
|
|
547
|
+
// surrounding sentence still imports, nothing looks broken.
|
|
548
|
+
//
|
|
549
|
+
// The target is the editor's own inline-math format
|
|
550
|
+
// (<span class="rte-math-inline" data-tex="...">), so imported equations
|
|
551
|
+
// are editable and re-render through whichever renderer the host page
|
|
552
|
+
// loads, exactly like one typed in the math dialog.
|
|
553
|
+
//
|
|
554
|
+
// This covers the constructs Word's equation editor actually produces.
|
|
555
|
+
// Anything unrecognised degrades to its literal text rather than vanishing
|
|
556
|
+
// -- a wrong-looking formula is recoverable, a missing one is not.
|
|
557
|
+
var OMML_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math";
|
|
558
|
+
|
|
559
|
+
// Word writes operators as Unicode, but a TeX renderer wants command names.
|
|
560
|
+
// Passing "∑" through untouched produces a glyph that KaTeX cannot size or
|
|
561
|
+
// place limits on.
|
|
562
|
+
var MATH_SYMBOLS = {
|
|
563
|
+
"∑": "\\sum", "∏": "\\prod", "∐": "\\coprod",
|
|
564
|
+
"∫": "\\int", "∬": "\\iint", "∭": "\\iiint", "∮": "\\oint",
|
|
565
|
+
"⋃": "\\bigcup", "⋂": "\\bigcap",
|
|
566
|
+
"±": "\\pm", "∓": "\\mp", "×": "\\times", "÷": "\\div",
|
|
567
|
+
"−": "-", "≠": "\\neq", "≤": "\\leq", "≥": "\\geq",
|
|
568
|
+
"≈": "\\approx", "≡": "\\equiv", "∝": "\\propto",
|
|
569
|
+
"∞": "\\infty", "∂": "\\partial", "∇": "\\nabla",
|
|
570
|
+
"√": "\\sqrt", "∠": "\\angle", "⋅": "\\cdot", "…": "\\dots",
|
|
571
|
+
"→": "\\to", "←": "\\leftarrow", "⇒": "\\Rightarrow", "⇔": "\\Leftrightarrow",
|
|
572
|
+
"∈": "\\in", "∉": "\\notin", "⊂": "\\subset", "⊆": "\\subseteq",
|
|
573
|
+
"∪": "\\cup", "∩": "\\cap", "∅": "\\emptyset",
|
|
574
|
+
"∀": "\\forall", "∃": "\\exists", "¬": "\\neg",
|
|
575
|
+
"∧": "\\land", "∨": "\\lor", "∴": "\\therefore",
|
|
576
|
+
"α": "\\alpha", "β": "\\beta", "γ": "\\gamma", "δ": "\\delta",
|
|
577
|
+
"ε": "\\epsilon", "ζ": "\\zeta", "η": "\\eta", "θ": "\\theta",
|
|
578
|
+
"ι": "\\iota", "κ": "\\kappa", "λ": "\\lambda", "μ": "\\mu",
|
|
579
|
+
"ν": "\\nu", "ξ": "\\xi", "π": "\\pi", "ρ": "\\rho",
|
|
580
|
+
"σ": "\\sigma", "τ": "\\tau", "υ": "\\upsilon", "φ": "\\varphi",
|
|
581
|
+
"χ": "\\chi", "ψ": "\\psi", "ω": "\\omega",
|
|
582
|
+
"Γ": "\\Gamma", "Δ": "\\Delta", "Θ": "\\Theta", "Λ": "\\Lambda",
|
|
583
|
+
"Ξ": "\\Xi", "Π": "\\Pi", "Σ": "\\Sigma", "Φ": "\\Phi",
|
|
584
|
+
"Ψ": "\\Psi", "Ω": "\\Omega"
|
|
585
|
+
};
|
|
586
|
+
// Accent character -> TeX accent command (m:acc carries the mark as a chr).
|
|
587
|
+
var MATH_ACCENTS = {
|
|
588
|
+
"̂": "\\hat", "̃": "\\tilde", "̄": "\\bar", "̅": "\\overline",
|
|
589
|
+
"̆": "\\breve", "̇": "\\dot", "̈": "\\ddot", "̊": "\\mathring",
|
|
590
|
+
"̌": "\\check", "→": "\\vec", "⃗": "\\vec", "́": "\\acute", "̀": "\\grave"
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
function ommlToTex(node) {
|
|
594
|
+
var M = OMML_NS;
|
|
595
|
+
function attrVal(parent, ln) {
|
|
596
|
+
// Properties sit in a child element whose only content is m:val.
|
|
597
|
+
for (var i = 0; i < parent.childNodes.length; i++) {
|
|
598
|
+
var c = parent.childNodes[i];
|
|
599
|
+
if (c.nodeType === 1 && c.localName === ln) {
|
|
600
|
+
return c.getAttributeNS(M, "val") || c.getAttribute("m:val") || "";
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return "";
|
|
604
|
+
}
|
|
605
|
+
function propOf(el, propName, ln) {
|
|
606
|
+
var p = child(el, propName);
|
|
607
|
+
return p ? attrVal(p, ln) : "";
|
|
608
|
+
}
|
|
609
|
+
function child(el, ln) {
|
|
610
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
611
|
+
var c = el.childNodes[i];
|
|
612
|
+
if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === M) return c;
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
function children(el, ln) {
|
|
617
|
+
var out = [];
|
|
618
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
619
|
+
var c = el.childNodes[i];
|
|
620
|
+
if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === M) out.push(c);
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
// An argument slot: braces so multi-character content binds correctly.
|
|
625
|
+
// "x+1" without them turns \frac{x+1}{2} into \fracx+12.
|
|
626
|
+
function arg(el) {
|
|
627
|
+
return "{" + (el ? walk(el).trim() : "") + "}";
|
|
628
|
+
}
|
|
629
|
+
// A slot where braces are optional. Emitting them unconditionally is
|
|
630
|
+
// correct but produces "{x}^{2}" and "{{i}^{2}}" -- valid TeX that is
|
|
631
|
+
// unreadable the moment the user opens the equation in the math dialog.
|
|
632
|
+
// A lone letter, digit or command needs none.
|
|
633
|
+
function atom(el) {
|
|
634
|
+
var t = el ? walk(el).trim() : "";
|
|
635
|
+
if (/^[A-Za-z0-9]$/.test(t) || /^\\[A-Za-z]+$/.test(t)) return t;
|
|
636
|
+
return "{" + t + "}";
|
|
637
|
+
}
|
|
638
|
+
function mapText(s) {
|
|
639
|
+
var out = "";
|
|
640
|
+
for (var i = 0; i < s.length; i++) {
|
|
641
|
+
var ch = s.charAt(i);
|
|
642
|
+
var sym = MATH_SYMBOLS[ch];
|
|
643
|
+
if (sym) { out += sym + " "; continue; }
|
|
644
|
+
// TeX metacharacters inside literal math text.
|
|
645
|
+
if ("#$%&_{}".indexOf(ch) >= 0) { out += "\\" + ch; continue; }
|
|
646
|
+
if (ch === "\\") { out += "\\backslash "; continue; }
|
|
647
|
+
out += ch;
|
|
648
|
+
}
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
651
|
+
function walk(el) {
|
|
652
|
+
var tex = "";
|
|
653
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
654
|
+
var c = el.childNodes[i];
|
|
655
|
+
if (c.nodeType !== 1) continue;
|
|
656
|
+
if (c.namespaceURI !== M) { tex += mapText(c.textContent || ""); continue; }
|
|
657
|
+
tex += convert(c);
|
|
658
|
+
}
|
|
659
|
+
return tex;
|
|
660
|
+
}
|
|
661
|
+
function convert(el) {
|
|
662
|
+
var ln = el.localName;
|
|
663
|
+
switch (ln) {
|
|
664
|
+
case "oMath":
|
|
665
|
+
case "oMathPara":
|
|
666
|
+
case "e":
|
|
667
|
+
case "num":
|
|
668
|
+
case "den":
|
|
669
|
+
case "sub":
|
|
670
|
+
case "sup":
|
|
671
|
+
case "deg":
|
|
672
|
+
case "lim":
|
|
673
|
+
case "fName":
|
|
674
|
+
return walk(el);
|
|
675
|
+
// Properties describe how to render, not what to render; emitting
|
|
676
|
+
// their text would leak stray characters into the formula.
|
|
677
|
+
case "rPr": case "ctrlPr": case "fPr": case "radPr": case "naryPr":
|
|
678
|
+
case "dPr": case "sSupPr": case "sSubPr": case "sSubSupPr": case "accPr":
|
|
679
|
+
case "barPr": case "funcPr": case "mPr": case "limLowPr": case "limUppPr":
|
|
680
|
+
case "groupChrPr": case "mcPr": case "eqArrPr": case "boxPr": case "borderBoxPr":
|
|
681
|
+
case "phantPr": case "sPrePr":
|
|
682
|
+
return "";
|
|
683
|
+
case "r":
|
|
684
|
+
return mapText(el.textContent || "");
|
|
685
|
+
case "t":
|
|
686
|
+
return mapText(el.textContent || "");
|
|
687
|
+
case "f": {
|
|
688
|
+
// A "no bar" fraction is Word's binomial/stacked form.
|
|
689
|
+
var type = propOf(el, "fPr", "type");
|
|
690
|
+
var n = arg(child(el, "num")), d = arg(child(el, "den"));
|
|
691
|
+
if (type === "noBar") return "\\binom" + n + d;
|
|
692
|
+
if (type === "skw" || type === "lin") return n + "/" + d;
|
|
693
|
+
return "\\frac" + n + d;
|
|
694
|
+
}
|
|
695
|
+
case "sSup": return atom(child(el, "e")) + "^" + arg(child(el, "sup"));
|
|
696
|
+
case "sSub": return atom(child(el, "e")) + "_" + arg(child(el, "sub"));
|
|
697
|
+
case "sSubSup": return atom(child(el, "e")) + "_" + arg(child(el, "sub")) + "^" + arg(child(el, "sup"));
|
|
698
|
+
case "sPre": return "{}_" + arg(child(el, "sub")) + "^" + arg(child(el, "sup")) + arg(child(el, "e"));
|
|
699
|
+
case "rad": {
|
|
700
|
+
// degHide means a plain square root; otherwise it is \sqrt[n]{}.
|
|
701
|
+
var hide = propOf(el, "radPr", "degHide");
|
|
702
|
+
var deg = child(el, "deg");
|
|
703
|
+
var body = arg(child(el, "e"));
|
|
704
|
+
if (hide === "1" || hide === "on" || hide === "true" || !deg || !(deg.textContent || "").trim()) return "\\sqrt" + body;
|
|
705
|
+
return "\\sqrt[" + walk(deg) + "]" + body;
|
|
706
|
+
}
|
|
707
|
+
case "nary": {
|
|
708
|
+
// The operator itself is an ATTRIBUTE (m:chr), not text. Miss
|
|
709
|
+
// it and every summation imports as a bare limit pair.
|
|
710
|
+
var chr = propOf(el, "naryPr", "chr") || "∫";
|
|
711
|
+
var op = MATH_SYMBOLS[chr] || mapText(chr);
|
|
712
|
+
var subHide = propOf(el, "naryPr", "subHide");
|
|
713
|
+
var supHide = propOf(el, "naryPr", "supHide");
|
|
714
|
+
var s = op;
|
|
715
|
+
if (subHide !== "1" && subHide !== "on" && child(el, "sub")) s += "_" + arg(child(el, "sub"));
|
|
716
|
+
if (supHide !== "1" && supHide !== "on" && child(el, "sup")) s += "^" + arg(child(el, "sup"));
|
|
717
|
+
// The integrand takes no braces: \sum_{i=1}^{n} i^{2}, not
|
|
718
|
+
// \sum_{i=1}^{n} {i^{2}}.
|
|
719
|
+
var nbody = child(el, "e");
|
|
720
|
+
return s + " " + (nbody ? walk(nbody).trim() : "");
|
|
721
|
+
}
|
|
722
|
+
case "d": {
|
|
723
|
+
// \left/\right so the fences grow with their content.
|
|
724
|
+
var beg = propOf(el, "dPr", "begChr");
|
|
725
|
+
var end = propOf(el, "dPr", "endChr");
|
|
726
|
+
var sep = propOf(el, "dPr", "sepChr") || "|";
|
|
727
|
+
if (beg === "") beg = "(";
|
|
728
|
+
if (end === "") end = ")";
|
|
729
|
+
function fence(ch, side) {
|
|
730
|
+
if (!ch) return side === "l" ? "\\left." : "\\right.";
|
|
731
|
+
if (ch === "{" || ch === "}") return "\\" + ch;
|
|
732
|
+
if (ch === "|") return "|";
|
|
733
|
+
if (ch === "‖") return "\\|";
|
|
734
|
+
// THREE different codepoints render as an angle
|
|
735
|
+
// bracket and are indistinguishable on screen: the
|
|
736
|
+
// mathematical pair (U+27E8/9), the CJK pair
|
|
737
|
+
// (U+3008/9) and the deprecated pair (U+2329/A) that
|
|
738
|
+
// several generators still emit. Comparing against a
|
|
739
|
+
// literal in the source would silently match only one
|
|
740
|
+
// of them, so compare by codepoint.
|
|
741
|
+
var cp = ch.charCodeAt(0);
|
|
742
|
+
if (cp === 0x27E8 || cp === 0x3008 || cp === 0x2329) return "\\langle";
|
|
743
|
+
if (cp === 0x27E9 || cp === 0x3009 || cp === 0x232A) return "\\rangle";
|
|
744
|
+
if (ch === "⌊") return "\\lfloor";
|
|
745
|
+
if (ch === "⌋") return "\\rfloor";
|
|
746
|
+
if (ch === "⌈") return "\\lceil";
|
|
747
|
+
if (ch === "⌉") return "\\rceil";
|
|
748
|
+
return ch;
|
|
749
|
+
}
|
|
750
|
+
var parts = children(el, "e");
|
|
751
|
+
var inner = [];
|
|
752
|
+
for (var q = 0; q < parts.length; q++) inner.push(walk(parts[q]));
|
|
753
|
+
return "\\left" + fence(beg, "l") + inner.join(" " + fence(sep, "m") + " ") + "\\right" + fence(end, "r");
|
|
754
|
+
}
|
|
755
|
+
case "func": {
|
|
756
|
+
// Word stores "sin"/"log" as ordinary text; without
|
|
757
|
+
// \operatorname TeX renders it as s*i*n in italics.
|
|
758
|
+
var name = walk(child(el, "fName")).trim();
|
|
759
|
+
var known = /^(sin|cos|tan|cot|sec|csc|sinh|cosh|tanh|log|ln|lg|exp|lim|max|min|det|dim|gcd|arg|deg|hom|ker|Pr|sup|inf)$/;
|
|
760
|
+
var head = known.test(name) ? "\\" + name : (name ? "\\operatorname{" + name + "}" : "");
|
|
761
|
+
return head + arg(child(el, "e"));
|
|
762
|
+
}
|
|
763
|
+
case "limLow": {
|
|
764
|
+
var base = walk(child(el, "e")).trim();
|
|
765
|
+
if (base === "lim" || base === "\\lim") return "\\lim_" + arg(child(el, "lim"));
|
|
766
|
+
return "\\underset" + arg(child(el, "lim")) + "{" + base + "}";
|
|
767
|
+
}
|
|
768
|
+
case "limUpp":
|
|
769
|
+
return "\\overset" + arg(child(el, "lim")) + arg(child(el, "e"));
|
|
770
|
+
case "acc": {
|
|
771
|
+
var mark = propOf(el, "accPr", "chr") || "̂";
|
|
772
|
+
var cmd = MATH_ACCENTS[mark] || "\\hat";
|
|
773
|
+
return cmd + arg(child(el, "e"));
|
|
774
|
+
}
|
|
775
|
+
case "bar":
|
|
776
|
+
return (propOf(el, "barPr", "pos") === "bot" ? "\\underline" : "\\overline") + arg(child(el, "e"));
|
|
777
|
+
case "groupChr": {
|
|
778
|
+
var g = propOf(el, "groupChrPr", "chr");
|
|
779
|
+
if (g === "⏟") return "\\underbrace" + arg(child(el, "e"));
|
|
780
|
+
return "\\overbrace" + arg(child(el, "e"));
|
|
781
|
+
}
|
|
782
|
+
case "borderBox":
|
|
783
|
+
return "\\boxed" + arg(child(el, "e"));
|
|
784
|
+
case "box":
|
|
785
|
+
case "phant":
|
|
786
|
+
return walk(el);
|
|
787
|
+
case "m": {
|
|
788
|
+
// Matrix: rows are m:mr, cells are m:e inside them.
|
|
789
|
+
var rows = children(el, "mr"), lines = [];
|
|
790
|
+
for (var r = 0; r < rows.length; r++) {
|
|
791
|
+
var cells = children(rows[r], "e"), cs = [];
|
|
792
|
+
for (var cix = 0; cix < cells.length; cix++) cs.push(walk(cells[cix]));
|
|
793
|
+
lines.push(cs.join(" & "));
|
|
794
|
+
}
|
|
795
|
+
return "\\begin{matrix}" + lines.join(" \\\\ ") + "\\end{matrix}";
|
|
796
|
+
}
|
|
797
|
+
case "eqArr": {
|
|
798
|
+
var eqs = children(el, "e"), rowsOut = [];
|
|
799
|
+
for (var e2 = 0; e2 < eqs.length; e2++) rowsOut.push(walk(eqs[e2]));
|
|
800
|
+
return "\\begin{aligned}" + rowsOut.join(" \\\\ ") + "\\end{aligned}";
|
|
801
|
+
}
|
|
802
|
+
default:
|
|
803
|
+
// Unknown construct: keep the content rather than losing it.
|
|
804
|
+
return walk(el);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
return convert(node).replace(/\s+/g, " ").trim();
|
|
809
|
+
} catch (e) { return (node.textContent || "").trim(); }
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function docxXmlToHtml(xml, ctx) {
|
|
813
|
+
ctx = ctx || { rels: {}, numbering: {}, media: {} };
|
|
814
|
+
if (!ctx.diagrams) ctx.diagrams = {};
|
|
815
|
+
var doc = new DOMParser().parseFromString(xml, "application/xml");
|
|
816
|
+
var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
817
|
+
var R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
818
|
+
|
|
819
|
+
function relTarget(el) {
|
|
820
|
+
if (!el) return null;
|
|
821
|
+
var id = el.getAttributeNS(R, "id") || el.getAttribute("r:id") ||
|
|
822
|
+
el.getAttributeNS(R, "embed") || el.getAttribute("r:embed");
|
|
823
|
+
if (!id) return null;
|
|
824
|
+
return ctx.rels[id] || null;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// <w:drawing>/<w:pict> -> <img> from word/media, inlined as a data URI.
|
|
828
|
+
function imageHtml(node) {
|
|
829
|
+
var blips = node.getElementsByTagName("*");
|
|
830
|
+
for (var i = 0; i < blips.length; i++) {
|
|
831
|
+
if (blips[i].localName !== "blip" && blips[i].localName !== "imagedata") continue;
|
|
832
|
+
var rel = relTarget(blips[i]);
|
|
833
|
+
if (!rel) continue;
|
|
834
|
+
// Targets are relative to word/: "media/image1.png".
|
|
835
|
+
var path = rel.target.replace(/^\.\//, "");
|
|
836
|
+
var key = path.indexOf("word/") === 0 ? path : "word/" + path;
|
|
837
|
+
var uri = ctx.media[key];
|
|
838
|
+
if (uri) return '<img src="' + uri + '" alt="">';
|
|
839
|
+
if (rel.external) return '<img src="' + esc(rel.target) + '" alt="">';
|
|
840
|
+
// A vector metafile (WMF/EMF) has no browser-renderable form, so
|
|
841
|
+
// there is no data URI for it. Dropping it silently leaves the
|
|
842
|
+
// reader with no idea a figure was ever there -- and no filename
|
|
843
|
+
// to go looking for in the original.
|
|
844
|
+
if (/\.(wmf|emf|emz|wmz)$/i.test(path)) {
|
|
845
|
+
var fileName = path.split("/").pop();
|
|
846
|
+
queueBlock('<p class="rte-imported-unsupported" data-source="' + esc(fileName) +
|
|
847
|
+
'"><em>[Unsupported image: ' + esc(fileName) +
|
|
848
|
+
" — Windows metafiles have no browser-renderable form]</em></p>");
|
|
849
|
+
return "";
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
queueBlock(diagramHtml(node));
|
|
853
|
+
return "";
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// SmartArt: the drawing references word/diagrams/dataN.xml through
|
|
857
|
+
// r:dm. Rendered as a labelled list -- the layout cannot be reproduced,
|
|
858
|
+
// but every word of it survives and stays editable.
|
|
859
|
+
function diagramHtml(node) {
|
|
860
|
+
var all = node.getElementsByTagName("*");
|
|
861
|
+
for (var i = 0; i < all.length; i++) {
|
|
862
|
+
if (all[i].localName !== "relIds") continue;
|
|
863
|
+
var id = all[i].getAttributeNS(R, "dm") || all[i].getAttribute("r:dm");
|
|
864
|
+
var rel = id ? ctx.rels[id] : null;
|
|
865
|
+
if (!rel) continue;
|
|
866
|
+
var path = rel.target.replace(/^\.\//, "");
|
|
867
|
+
var key = path.indexOf("word/") === 0 ? path : "word/" + path;
|
|
868
|
+
var nodes = ctx.diagrams[key];
|
|
869
|
+
if (!nodes || !nodes.length) continue;
|
|
870
|
+
var items = "";
|
|
871
|
+
for (var n = 0; n < nodes.length; n++) items += "<li>" + esc(nodes[n]) + "</li>";
|
|
872
|
+
return '<div class="rte-imported-smartart" style="' + SMARTART_STYLE + '"><ul>' + items + "</ul></div>";
|
|
873
|
+
}
|
|
874
|
+
return "";
|
|
875
|
+
}
|
|
876
|
+
function els(parent, ln) { return parent.getElementsByTagNameNS(W, ln); }
|
|
877
|
+
function firstChildEl(parent, ln) {
|
|
878
|
+
for (var i = 0; i < parent.childNodes.length; i++) {
|
|
879
|
+
var c = parent.childNodes[i];
|
|
880
|
+
if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === W) return c;
|
|
881
|
+
}
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
function runText(r) {
|
|
885
|
+
var t = "", kids = r.childNodes;
|
|
886
|
+
for (var i = 0; i < kids.length; i++) {
|
|
887
|
+
var c = kids[i];
|
|
888
|
+
if (c.nodeType !== 1) continue;
|
|
889
|
+
// Deleted text is stored as w:delText, never w:t — read only w:t
|
|
890
|
+
// and every tracked deletion imports as an empty run.
|
|
891
|
+
if (c.localName === "t" || c.localName === "delText") t += c.textContent || "";
|
|
892
|
+
else if (c.localName === "tab") t += "\t";
|
|
893
|
+
else if (c.localName === "br" || c.localName === "cr") t += "\n";
|
|
894
|
+
}
|
|
895
|
+
return t;
|
|
896
|
+
}
|
|
897
|
+
function runHtml(r) {
|
|
898
|
+
// A run can carry a picture instead of text; without this every
|
|
899
|
+
// image in the document is silently dropped on import.
|
|
900
|
+
var pics = "";
|
|
901
|
+
for (var d = 0; d < r.childNodes.length; d++) {
|
|
902
|
+
var ch = r.childNodes[d];
|
|
903
|
+
if (ch.nodeType === 1 && (ch.localName === "drawing" || ch.localName === "pict")) {
|
|
904
|
+
pics += imageHtml(ch);
|
|
905
|
+
}
|
|
906
|
+
// A run holding a footnote reference has no text of its own.
|
|
907
|
+
if (ch.nodeType === 1 && ch.localName === "footnoteReference") {
|
|
908
|
+
var fid = ch.getAttributeNS(W, "id") || ch.getAttribute("w:id");
|
|
909
|
+
var body = fid != null ? ctx.footnotes[String(fid)] : null;
|
|
910
|
+
if (body) pics += footnoteMarker(body);
|
|
911
|
+
}
|
|
912
|
+
// Endnotes render through the same notes machinery: Word puts
|
|
913
|
+
// both at the end of the document, and the distinction is not
|
|
914
|
+
// one HTML has a separate representation for.
|
|
915
|
+
if (ch.nodeType === 1 && ch.localName === "endnoteReference") {
|
|
916
|
+
var eid = ch.getAttributeNS(W, "id") || ch.getAttribute("w:id");
|
|
917
|
+
var ebody = eid != null ? ctx.endnotes[String(eid)] : null;
|
|
918
|
+
if (ebody) pics += footnoteMarker(ebody);
|
|
919
|
+
}
|
|
920
|
+
// TEXT BOXES. Their content is nested several levels below the
|
|
921
|
+
// run (w:pict > v:shape > v:textbox > w:txbxContent, or the
|
|
922
|
+
// DrawingML mc:AlternateContent equivalent), so a reader that
|
|
923
|
+
// only looks at a run's DIRECT children drops every word inside
|
|
924
|
+
// them -- silently, with no sign anything is missing.
|
|
925
|
+
if (ch.nodeType === 1 && (ch.localName === "pict" || ch.localName === "drawing" ||
|
|
926
|
+
ch.localName === "object" || ch.localName === "AlternateContent")) {
|
|
927
|
+
collectTextBoxes(ch);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
var txt = esc(runText(r));
|
|
931
|
+
if (!txt) return pics;
|
|
932
|
+
txt = txt.replace(/\n/g, "<br>");
|
|
933
|
+
var rpr = firstChildEl(r, "rPr");
|
|
934
|
+
if (rpr) {
|
|
935
|
+
if (firstChildEl(rpr, "b")) txt = "<strong>" + txt + "</strong>";
|
|
936
|
+
if (firstChildEl(rpr, "i")) txt = "<em>" + txt + "</em>";
|
|
937
|
+
if (firstChildEl(rpr, "u")) txt = "<u>" + txt + "</u>";
|
|
938
|
+
if (firstChildEl(rpr, "strike")) txt = "<s>" + txt + "</s>";
|
|
939
|
+
var vert = firstChildEl(rpr, "vertAlign");
|
|
940
|
+
if (vert) {
|
|
941
|
+
var va = vert.getAttributeNS(W, "val") || vert.getAttribute("w:val") || "";
|
|
942
|
+
if (va === "superscript") txt = "<sup>" + txt + "</sup>";
|
|
943
|
+
else if (va === "subscript") txt = "<sub>" + txt + "</sub>";
|
|
944
|
+
}
|
|
945
|
+
// Character colour and size. w:sz is in HALF-points, so 24 = 12pt;
|
|
946
|
+
// forgetting the halving makes every imported document twice the
|
|
947
|
+
// size it should be.
|
|
948
|
+
var css = "";
|
|
949
|
+
var col = firstChildEl(rpr, "color");
|
|
950
|
+
if (col) {
|
|
951
|
+
var cv = col.getAttributeNS(W, "val") || col.getAttribute("w:val") || "";
|
|
952
|
+
if (cv && /^[0-9A-Fa-f]{6}$/.test(cv)) css += "color:#" + cv + ";";
|
|
953
|
+
}
|
|
954
|
+
var hl = firstChildEl(rpr, "highlight");
|
|
955
|
+
if (hl) {
|
|
956
|
+
var hv = hl.getAttributeNS(W, "val") || hl.getAttribute("w:val") || "";
|
|
957
|
+
if (hv && hv !== "none") css += "background-color:" + hv + ";";
|
|
958
|
+
}
|
|
959
|
+
var sz = firstChildEl(rpr, "sz");
|
|
960
|
+
if (sz) {
|
|
961
|
+
var sv = parseInt(sz.getAttributeNS(W, "val") || sz.getAttribute("w:val") || "", 10);
|
|
962
|
+
if (sv > 0) css += "font-size:" + (sv / 2) + "pt;";
|
|
963
|
+
}
|
|
964
|
+
if (css) txt = '<span style="' + css + '">' + txt + "</span>";
|
|
965
|
+
}
|
|
966
|
+
return pics + txt;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Block-level content discovered while rendering a paragraph: text
|
|
970
|
+
// boxes, SmartArt outlines, and placeholders for images the browser
|
|
971
|
+
// cannot render. None of them may be emitted inline inside the
|
|
972
|
+
// paragraph that referenced them -- a <div> inside a <p> is invalid and
|
|
973
|
+
// browsers silently close the paragraph around it -- so each is
|
|
974
|
+
// buffered here, already wrapped, and flushed straight after.
|
|
975
|
+
var pendingBlocks = [];
|
|
976
|
+
function queueBlock(html) {
|
|
977
|
+
// Dedup on the whole string: an mc:AlternateContent block carries
|
|
978
|
+
// the same graphic twice, once per rendering flavour.
|
|
979
|
+
if (html && pendingBlocks.indexOf(html) < 0) pendingBlocks.push(html);
|
|
980
|
+
}
|
|
981
|
+
function collectTextBoxes(node) {
|
|
982
|
+
var boxes = node.getElementsByTagName("*");
|
|
983
|
+
for (var i = 0; i < boxes.length; i++) {
|
|
984
|
+
if (boxes[i].localName !== "txbxContent") continue;
|
|
985
|
+
var inner = "";
|
|
986
|
+
var kids = boxes[i].childNodes;
|
|
987
|
+
for (var k = 0; k < kids.length; k++) {
|
|
988
|
+
var c = kids[k];
|
|
989
|
+
if (c.nodeType !== 1) continue;
|
|
990
|
+
if (c.localName === "p") {
|
|
991
|
+
var t = paraInner(c);
|
|
992
|
+
if (t) inner += "<p>" + t + "</p>";
|
|
993
|
+
} else if (c.localName === "tbl") {
|
|
994
|
+
inner += tableHtml(c);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (inner) queueBlock('<div class="rte-imported-textbox" style="' + TEXTBOX_STYLE + '">' + inner + "</div>");
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function flushBlocks() {
|
|
1002
|
+
if (!pendingBlocks.length) return "";
|
|
1003
|
+
var html = pendingBlocks.join("");
|
|
1004
|
+
pendingBlocks = [];
|
|
1005
|
+
return html;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Emit a marker in the shape footnotes.js already understands, and stash
|
|
1009
|
+
// the body so the notes section can be built at the end. The plugin
|
|
1010
|
+
// renumbers on load, so the number written here is provisional.
|
|
1011
|
+
var importedNotes = [];
|
|
1012
|
+
// OMML -> the editor's own inline-math span, so an imported equation is
|
|
1013
|
+
// editable in the math dialog and renders through whatever renderer the
|
|
1014
|
+
// host page loads. The visible text is the TeX itself: that is what a
|
|
1015
|
+
// page with no renderer shows, and it is readable rather than blank.
|
|
1016
|
+
function mathHtml(node) {
|
|
1017
|
+
var tex = ommlToTex(node);
|
|
1018
|
+
if (!tex) return "";
|
|
1019
|
+
var display = node.localName === "oMathPara";
|
|
1020
|
+
return '<span class="rte-math-inline' + (display ? " rte-math-display" : "") +
|
|
1021
|
+
'" data-tex="' + esc(tex) + '">' + esc(tex) + "</span>";
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function footnoteMarker(text) {
|
|
1025
|
+
var id = "fnimp" + (importedNotes.length + 1);
|
|
1026
|
+
importedNotes.push({ id: id, text: text });
|
|
1027
|
+
var n = importedNotes.length;
|
|
1028
|
+
return '<sup class="rte-fn-ref" data-fn-id="' + id + '" data-fn-number="' + n +
|
|
1029
|
+
'" id="fnref-' + id + '" contenteditable="false">' + n + "</sup>";
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function footnotesSection() {
|
|
1033
|
+
if (!importedNotes.length) return "";
|
|
1034
|
+
var items = "";
|
|
1035
|
+
for (var i = 0; i < importedNotes.length; i++) {
|
|
1036
|
+
var f = importedNotes[i];
|
|
1037
|
+
items += '<li class="rte-fn-note" data-fn-id="' + f.id + '" data-fn-number="' + (i + 1) +
|
|
1038
|
+
'" id="fn-' + f.id + '">' + esc(f.text) +
|
|
1039
|
+
'<a class="rte-fn-back" data-fn-id="' + f.id + '" href="#fnref-' + f.id +
|
|
1040
|
+
'" contenteditable="false" title="Back to reference">↩</a></li>';
|
|
1041
|
+
}
|
|
1042
|
+
return '<section class="rte-footnotes"><h2 class="rte-fn-title">Footnotes</h2>' +
|
|
1043
|
+
'<ol class="rte-fn-list" data-numbering="decimal">' + items + "</ol></section>";
|
|
1044
|
+
}
|
|
1045
|
+
// Runs inside a w:ins / w:del wrapper, rendered with the review markup
|
|
1046
|
+
// that the DOCX *export* already round-trips (<ins data-author>/<del>).
|
|
1047
|
+
function revisionHtml(node, tag) {
|
|
1048
|
+
var inner = "";
|
|
1049
|
+
for (var i = 0; i < node.childNodes.length; i++) {
|
|
1050
|
+
var c = node.childNodes[i];
|
|
1051
|
+
if (c.nodeType !== 1) continue;
|
|
1052
|
+
if (c.localName === "r") inner += runHtml(c);
|
|
1053
|
+
else if (c.localName === "ins") inner += revisionHtml(c, "ins");
|
|
1054
|
+
else if (c.localName === "del") inner += revisionHtml(c, "del");
|
|
1055
|
+
}
|
|
1056
|
+
if (!inner) return "";
|
|
1057
|
+
var author = node.getAttributeNS(W, "author") || node.getAttribute("w:author") || "";
|
|
1058
|
+
var date = node.getAttributeNS(W, "date") || node.getAttribute("w:date") || "";
|
|
1059
|
+
return "<" + tag +
|
|
1060
|
+
(author ? ' data-author="' + esc(author) + '"' : "") +
|
|
1061
|
+
(date ? ' data-date="' + esc(date) + '"' : "") +
|
|
1062
|
+
">" + inner + "</" + tag + ">";
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function paraInner(p) {
|
|
1066
|
+
var h = "", kids = p.childNodes;
|
|
1067
|
+
// Word marks a commented RANGE with start/end markers around the runs,
|
|
1068
|
+
// so the wrapper has to be opened and closed as we walk, not derived
|
|
1069
|
+
// from any single element.
|
|
1070
|
+
var openComments = [];
|
|
1071
|
+
function commentOpen(id) {
|
|
1072
|
+
var c = ctx.comments[String(id)];
|
|
1073
|
+
if (!c) return "";
|
|
1074
|
+
openComments.push(id);
|
|
1075
|
+
return '<span data-comment="' + esc(c.text) + '"' +
|
|
1076
|
+
(c.author ? ' data-comment-author="' + esc(c.author) + '"' : "") + ">";
|
|
1077
|
+
}
|
|
1078
|
+
for (var i = 0; i < kids.length; i++) {
|
|
1079
|
+
var c = kids[i];
|
|
1080
|
+
if (c.nodeType === 1 && c.localName === "commentRangeStart") {
|
|
1081
|
+
h += commentOpen(c.getAttributeNS(W, "id") || c.getAttribute("w:id"));
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (c.nodeType === 1 && c.localName === "commentRangeEnd") {
|
|
1085
|
+
if (openComments.length) { openComments.pop(); h += "</span>"; }
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (c.nodeType === 1 && (c.localName === "ins" || c.localName === "del") && c.namespaceURI === W) {
|
|
1089
|
+
h += revisionHtml(c, c.localName);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
// EQUATIONS. OMML is a sibling of the runs, in its own
|
|
1093
|
+
// namespace -- a walker that only recognises w:r skips it and
|
|
1094
|
+
// the equation is gone. m:oMathPara is the display (own-line)
|
|
1095
|
+
// wrapper around one or more m:oMath.
|
|
1096
|
+
if (c.nodeType === 1 && c.namespaceURI === OMML_NS &&
|
|
1097
|
+
(c.localName === "oMath" || c.localName === "oMathPara")) {
|
|
1098
|
+
h += mathHtml(c);
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
if (c.nodeType === 1 && c.localName === "r" && c.namespaceURI === W) h += runHtml(c);
|
|
1102
|
+
else if (c.nodeType === 1 && c.localName === "hyperlink") {
|
|
1103
|
+
var inner = "";
|
|
1104
|
+
for (var j = 0; j < c.childNodes.length; j++) if (c.childNodes[j].localName === "r") inner += runHtml(c.childNodes[j]);
|
|
1105
|
+
// Resolve r:id through the rels part. Previously the run text
|
|
1106
|
+
// was emitted bare and the URL was thrown away, so every link
|
|
1107
|
+
// in an imported document became plain text.
|
|
1108
|
+
var rel = relTarget(c);
|
|
1109
|
+
var anchor = c.getAttributeNS(W, "anchor") || c.getAttribute("w:anchor");
|
|
1110
|
+
var href = rel ? rel.target : (anchor ? "#" + anchor : null);
|
|
1111
|
+
h += href ? ('<a href="' + esc(href) + '">' + (inner || esc(href)) + "</a>") : inner;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
// A comment range left open at the end of the paragraph would leak an
|
|
1115
|
+
// unbalanced <span> into the document.
|
|
1116
|
+
while (openComments.length) { openComments.pop(); h += "</span>"; }
|
|
1117
|
+
return h;
|
|
1118
|
+
}
|
|
1119
|
+
function paraStyle(p) {
|
|
1120
|
+
var ppr = firstChildEl(p, "pPr"); if (!ppr) return {};
|
|
1121
|
+
var info = {};
|
|
1122
|
+
var ps = firstChildEl(ppr, "pStyle");
|
|
1123
|
+
if (ps) {
|
|
1124
|
+
var v = ps.getAttributeNS(W, "val") || ps.getAttribute("w:val") || "";
|
|
1125
|
+
var m = /heading(\d)/i.exec(v);
|
|
1126
|
+
if (m) info.heading = Math.min(6, parseInt(m[1], 10));
|
|
1127
|
+
// docxexport.js writes blockquotes as the "Quote" paragraph style;
|
|
1128
|
+
// without this they came back as plain <p> and the round-trip lost
|
|
1129
|
+
// every blockquote. Match Quote / IntenseQuote / BlockQuote.
|
|
1130
|
+
else if (/^(intense)?quote$|blockquote/i.test(v.replace(/\s+/g, ""))) info.quote = true;
|
|
1131
|
+
}
|
|
1132
|
+
var numPr = firstChildEl(ppr, "numPr");
|
|
1133
|
+
if (numPr) {
|
|
1134
|
+
info.list = true;
|
|
1135
|
+
var ilvl = firstChildEl(numPr, "ilvl");
|
|
1136
|
+
var numId = firstChildEl(numPr, "numId");
|
|
1137
|
+
info.level = parseInt(ilvl ? (ilvl.getAttributeNS(W, "val") || ilvl.getAttribute("w:val") || "0") : "0", 10) || 0;
|
|
1138
|
+
var nid = numId ? (numId.getAttributeNS(W, "val") || numId.getAttribute("w:val") || "") : "";
|
|
1139
|
+
var levels = ctx.numbering[nid];
|
|
1140
|
+
var fmt = levels ? levels[String(info.level)] : null;
|
|
1141
|
+
// Word marks bullets with numFmt="bullet"; anything else is an
|
|
1142
|
+
// ordered list. Defaulting to bullet when numbering.xml is absent
|
|
1143
|
+
// matches the old behaviour rather than inventing <ol>s.
|
|
1144
|
+
info.ordered = !!fmt && fmt !== "bullet" && fmt !== "none";
|
|
1145
|
+
}
|
|
1146
|
+
var jc = firstChildEl(ppr, "jc");
|
|
1147
|
+
if (jc) {
|
|
1148
|
+
var a = jc.getAttributeNS(W, "val") || jc.getAttribute("w:val") || "";
|
|
1149
|
+
if (a === "center" || a === "right" || a === "both") {
|
|
1150
|
+
info.align = (a === "both") ? "justify" : a;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return info;
|
|
1154
|
+
}
|
|
1155
|
+
var body = els(doc, "body")[0];
|
|
1156
|
+
if (!body) return "";
|
|
1157
|
+
var out = [];
|
|
1158
|
+
// Stack of open lists so w:ilvl produces real nesting instead of a flat
|
|
1159
|
+
// run of <li>s. liOpen tracks whether an <li> is still open at each
|
|
1160
|
+
// depth, because a nested list must live INSIDE its parent <li> --
|
|
1161
|
+
// <ol><li>a</li><ol>...</ol></ol> is invalid HTML, and shipping invalid
|
|
1162
|
+
// markup out of an importer undermines the whole clean-output promise.
|
|
1163
|
+
var listStack = [];
|
|
1164
|
+
var liOpen = [];
|
|
1165
|
+
function closeListsTo(depth) {
|
|
1166
|
+
while (listStack.length > depth) {
|
|
1167
|
+
var d = listStack.length - 1;
|
|
1168
|
+
if (liOpen[d]) { out.push("</li>"); liOpen[d] = false; }
|
|
1169
|
+
out.push("</" + listStack.pop() + ">");
|
|
1170
|
+
liOpen.pop();
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
for (var i = 0; i < body.childNodes.length; i++) {
|
|
1174
|
+
var node = body.childNodes[i];
|
|
1175
|
+
if (node.nodeType !== 1) continue;
|
|
1176
|
+
// A display equation can sit at body level rather than inside a
|
|
1177
|
+
// w:p, so the "wordprocessingml namespace only" filter below would
|
|
1178
|
+
// drop it.
|
|
1179
|
+
if (node.namespaceURI === OMML_NS &&
|
|
1180
|
+
(node.localName === "oMathPara" || node.localName === "oMath")) {
|
|
1181
|
+
closeListsTo(0);
|
|
1182
|
+
var mh = mathHtml(node);
|
|
1183
|
+
if (mh) out.push("<p>" + mh + "</p>");
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (node.namespaceURI !== W) continue;
|
|
1187
|
+
if (node.localName === "p") {
|
|
1188
|
+
var st = paraStyle(node);
|
|
1189
|
+
var inner = paraInner(node);
|
|
1190
|
+
if (st.list) {
|
|
1191
|
+
var want = (st.level || 0) + 1;
|
|
1192
|
+
var tag = st.ordered ? "ol" : "ul";
|
|
1193
|
+
// Come back up to this depth FIRST. Checking the type before
|
|
1194
|
+
// this ran compared against the wrong level, so a bullet
|
|
1195
|
+
// following a deeper numbered item silently joined the <ol>.
|
|
1196
|
+
if (listStack.length > want) closeListsTo(want);
|
|
1197
|
+
// Now at this depth: a different list type means a new list.
|
|
1198
|
+
if (listStack.length === want && listStack[want - 1] !== tag) closeListsTo(want - 1);
|
|
1199
|
+
// Going deeper leaves the parent <li> open so the sublist
|
|
1200
|
+
// nests inside it.
|
|
1201
|
+
while (listStack.length < want) {
|
|
1202
|
+
out.push("<" + tag + ">");
|
|
1203
|
+
listStack.push(tag);
|
|
1204
|
+
liOpen.push(false);
|
|
1205
|
+
}
|
|
1206
|
+
if (liOpen[want - 1]) { out.push("</li>"); liOpen[want - 1] = false; }
|
|
1207
|
+
out.push("<li>" + (inner || ""));
|
|
1208
|
+
liOpen[want - 1] = true;
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
closeListsTo(0);
|
|
1212
|
+
if (st.heading) out.push("<h" + st.heading + ">" + (inner || "") + "</h" + st.heading + ">");
|
|
1213
|
+
else if (st.quote) out.push("<blockquote>" + (inner || "<br>") + "</blockquote>");
|
|
1214
|
+
else {
|
|
1215
|
+
var style = st.align ? ' style="text-align:' + st.align + '"' : "";
|
|
1216
|
+
// A paragraph that contained ONLY a text box has no text of
|
|
1217
|
+
// its own; emitting an empty <p> before the box adds a blank
|
|
1218
|
+
// line that was never in the document.
|
|
1219
|
+
var boxes = flushBlocks();
|
|
1220
|
+
if (inner || !boxes) out.push("<p" + style + ">" + (inner || "<br>") + "</p>");
|
|
1221
|
+
if (boxes) out.push(boxes);
|
|
1222
|
+
}
|
|
1223
|
+
} else if (node.localName === "tbl") {
|
|
1224
|
+
closeListsTo(0);
|
|
1225
|
+
out.push(tableHtml(node));
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
closeListsTo(0);
|
|
1229
|
+
// A text box hanging off the last paragraph, or off a heading/list item,
|
|
1230
|
+
// still has to land somewhere.
|
|
1231
|
+
var trailing = flushBlocks();
|
|
1232
|
+
if (trailing) out.push(trailing);
|
|
1233
|
+
// The notes section goes last, after every marker has been collected.
|
|
1234
|
+
var notes = footnotesSection();
|
|
1235
|
+
if (notes) out.push(notes);
|
|
1236
|
+
return out.join("\n");
|
|
1237
|
+
|
|
1238
|
+
function tableHtml(tbl) {
|
|
1239
|
+
var rows = [], kids = tbl.childNodes;
|
|
1240
|
+
for (var i = 0; i < kids.length; i++) {
|
|
1241
|
+
var tr = kids[i];
|
|
1242
|
+
if (tr.nodeType !== 1 || tr.localName !== "tr") continue;
|
|
1243
|
+
// docxexport.js marks a header row with <w:trPr><w:tblHeader/>.
|
|
1244
|
+
// Without honouring it, every cell imported as <td> and the table
|
|
1245
|
+
// lost its header semantics on round-trip - which then fails the
|
|
1246
|
+
// accessibility checker's table-missing-header rule.
|
|
1247
|
+
var trPr = firstChildEl(tr, "trPr");
|
|
1248
|
+
var isHeaderRow = !!(trPr && firstChildEl(trPr, "tblHeader"));
|
|
1249
|
+
var cellTag = isHeaderRow ? "th" : "td";
|
|
1250
|
+
var cells = [];
|
|
1251
|
+
for (var j = 0; j < tr.childNodes.length; j++) {
|
|
1252
|
+
var tc = tr.childNodes[j];
|
|
1253
|
+
if (tc.nodeType !== 1 || tc.localName !== "tc") continue;
|
|
1254
|
+
var cellHtml = "";
|
|
1255
|
+
for (var k = 0; k < tc.childNodes.length; k++) {
|
|
1256
|
+
if (tc.childNodes[k].localName === "p") cellHtml += "<p>" + (paraInner(tc.childNodes[k]) || "<br>") + "</p>";
|
|
1257
|
+
}
|
|
1258
|
+
cells.push("<" + cellTag + ">" + cellHtml + "</" + cellTag + ">");
|
|
1259
|
+
}
|
|
1260
|
+
rows.push("<tr>" + cells.join("") + "</tr>");
|
|
1261
|
+
}
|
|
1262
|
+
return "<table>" + rows.join("") + "</table>";
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function notify(msg) {
|
|
1267
|
+
try {
|
|
1268
|
+
if (editor && typeof editor.createDialog === "function") {
|
|
1269
|
+
var d = editor.createDialog((editor.getLangText && editor.getLangText("importtitle")) || "Import document", "rte-dialog-import");
|
|
1270
|
+
var w = d.ownerDocument.createElement("div");
|
|
1271
|
+
w.style.cssText = "padding:16px;max-width:420px;font:13px -apple-system,Segoe UI,sans-serif;line-height:1.5";
|
|
1272
|
+
w.textContent = msg;
|
|
1273
|
+
d.appendChild(w);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
} catch (e) {}
|
|
1277
|
+
if (window.console) console.warn("documentimport:", msg);
|
|
1278
|
+
}
|
|
1279
|
+
}
|