@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.
@@ -522,11 +522,33 @@ function RTE_Plugin_AccessibilityChecker() {
522
522
  var tables = tag === "table" ? [node] : (node.querySelectorAll ? node.querySelectorAll("table") : []);
523
523
  for (var tableIndex = 0; tableIndex < tables.length; tableIndex++) {
524
524
  var table = tables[tableIndex];
525
- if (!table.querySelector("th")) {
525
+ var tableRole = String(table.getAttribute("role") || "").toLowerCase();
526
+ var isLayoutTable = (tableRole === "presentation" || tableRole === "none");
527
+
528
+ // 2026-08-30 A layout table MUST NOT have headers, so demanding
529
+ // them here produced an error the author could never clear —
530
+ // which is how a checker teaches people to ignore it. Presentation
531
+ // tables are held to the opposite rule instead: they must not
532
+ // carry data-table semantics, because <th>/scope/<caption> make a
533
+ // screen reader announce relationships that do not exist
534
+ // (WCAG 1.3.1). See layouttable.js.
535
+ if (isLayoutTable) {
536
+ var semantic = table.querySelector("th, [scope], caption") || table.getAttribute("summary");
537
+ if (semantic) {
538
+ issues.push({
539
+ code: "layout-table-semantics",
540
+ severity: "error",
541
+ message: "Layout table still carries data-table markup (header cells, scope or a caption). A screen reader will announce relationships this table does not have.",
542
+ path: path + ".table[" + tableIndex + "]",
543
+ _target: table
544
+ });
545
+ }
546
+ }
547
+ else if (!table.querySelector("th")) {
526
548
  issues.push({
527
549
  code: "table-missing-header",
528
550
  severity: "warning",
529
- message: "Table has no header cells. Promote the first row to headers when it describes the columns.",
551
+ message: "Table has no header cells. Promote the first row to headers, or mark it as a layout table when it only positions content.",
530
552
  path: path + ".table[" + tableIndex + "]",
531
553
  _target: table
532
554
  });
@@ -557,7 +579,13 @@ function RTE_Plugin_AccessibilityChecker() {
557
579
  target.parentNode.replaceChild(replacement, target);
558
580
  }
559
581
  else if (issue.code === "table-missing-header") {
560
- promoteFirstTableRow(target);
582
+ // Two valid outcomes, and only the author knows which: the table
583
+ // describes data (give it headers) or only positions it (say so).
584
+ if (options && options.markLayout) markTableAsLayout(target);
585
+ else promoteFirstTableRow(target);
586
+ }
587
+ else if (issue.code === "layout-table-semantics") {
588
+ markTableAsLayout(target);
561
589
  }
562
590
  else if (issue.code === "language-of-parts") {
563
591
  markLanguageRuns(target, options && options.lang ? options.lang : issue._lang, issue._script);
@@ -642,6 +670,35 @@ function RTE_Plugin_AccessibilityChecker() {
642
670
  }
643
671
  }
644
672
 
673
+ // Delegates to layouttable.js when it is loaded so there is ONE definition of
674
+ // what "layout table" means; the fallback keeps the repair working in builds
675
+ // where that plugin was left out of the bundle.
676
+ function markTableAsLayout(table) {
677
+ if (!table) return;
678
+ if (editor && typeof editor.setTableLayoutMode === "function") {
679
+ try { editor.setTableLayoutMode("layout", table); return; } catch (e) {}
680
+ }
681
+ table.setAttribute("role", "presentation");
682
+ table.removeAttribute("summary");
683
+ var caption = table.querySelector("caption");
684
+ if (caption && caption.parentNode === table) caption.parentNode.removeChild(caption);
685
+ var cells = table.querySelectorAll("th,[scope]");
686
+ for (var i = 0; i < cells.length; i++) {
687
+ var cell = cells[i];
688
+ // Tables nest — do not strip the headers of a data table that
689
+ // happens to sit inside a cell of this one.
690
+ if (cell.closest && cell.closest("table") !== table) continue;
691
+ cell.removeAttribute("scope");
692
+ cell.removeAttribute("headers");
693
+ if (String(cell.nodeName || "").toLowerCase() !== "th") continue;
694
+ var td = table.ownerDocument.createElement("td");
695
+ while (cell.firstChild) td.appendChild(cell.firstChild);
696
+ copyAttributes(cell, td);
697
+ td.removeAttribute("scope");
698
+ cell.parentNode.replaceChild(td, cell);
699
+ }
700
+ }
701
+
645
702
  function copyAttributes(source, target) {
646
703
  if (!source || !target || !source.attributes) return;
647
704
  for (var index = 0; index < source.attributes.length; index++) {
@@ -875,7 +932,9 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
875
932
  var existing = document.querySelectorAll("link[rel=stylesheet]");
876
933
  for (var i = 0; i < existing.length; i++) {
877
934
  var href = existing[i].getAttribute("href") || "";
878
- if (/(?:^|\/)aitoolkit\.css(?:\?.*)?$/i.test(href)) return;
935
+ // Matches the minified variant too - a host page that links either one
936
+ // must not get a second copy injected.
937
+ if (/(?:^|\/)aitoolkit(?:\.min)?\.css(?:\?.*)?$/i.test(href)) return;
879
938
  }
880
939
  // Locate our own script tag (aitoolkit.js or all_plugins.js) so we
881
940
  // can resolve aitoolkit.css next to it. Matches a custom mount path.
@@ -889,14 +948,30 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
889
948
  }
890
949
  }
891
950
  var srcAttr = ourScript ? ourScript.getAttribute("src") : "";
892
- var cssHref = srcAttr
893
- ? srcAttr.replace(/[^/]+$/, "aitoolkit.css")
894
- : (((window.RTE_DefaultConfig && window.RTE_DefaultConfig.url_base) || "/richtexteditor") + "/plugins/aitoolkit.css");
895
- if (cssHref.indexOf("?") < 0) cssHref += "?v=20260703a";
951
+ // Prefer the minified stylesheet. aitoolkit.css is 537 KB unminified - larger
952
+ // than the obfuscated rte.js - and every page that loads the AI toolkit was
953
+ // paying for it. The readable .css is still shipped and still the one to edit
954
+ // or override; this only changes what gets fetched by default.
955
+ function resolve(name) {
956
+ return srcAttr
957
+ ? srcAttr.replace(/[^/]+$/, name)
958
+ : (((window.RTE_DefaultConfig && window.RTE_DefaultConfig.url_base) || "/richtexteditor") + "/plugins/" + name);
959
+ }
960
+ function bust(href) { return href.indexOf("?") < 0 ? href + "?v=20260830a" : href; }
961
+
896
962
  var link = document.createElement("link");
897
963
  link.rel = "stylesheet";
898
- link.href = cssHref;
964
+ link.href = bust(resolve("aitoolkit.min.css"));
899
965
  link.setAttribute("data-rte-auto-injected", "aitoolkit");
966
+ // .min.css is newer than .css. An install that predates it - an unpacked older
967
+ // zip, a tier the mirror missed - would otherwise lose ALL AI toolkit styling
968
+ // silently, which is a far worse failure than shipping the larger file. Fall
969
+ // back to the readable stylesheet if the minified one is not there.
970
+ link.onerror = function () {
971
+ if (link.getAttribute("data-rte-css-fallback")) return;
972
+ link.setAttribute("data-rte-css-fallback", "1");
973
+ link.href = bust(resolve("aitoolkit.css"));
974
+ };
900
975
  (document.head || document.documentElement).appendChild(link);
901
976
  })();
902
977
 
@@ -23295,1269 +23370,1285 @@ function RTE_Plugin_Dictation() {
23295
23370
  }
23296
23371
  }
23297
23372
 
23298
- if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
23299
-
23300
- // 2026-06-09 Document import — the natural pair to Export-to-Word/Markdown/PDF.
23301
- // Opens a local file and loads it into the editor. Library-free for the common
23302
- // formats: Markdown (.md/.markdown via the core fromMarkdown engine), HTML
23303
- // (.html/.htm), plain text (.txt), and Word's HTML-based format (.doc, which
23304
- // Word saves as MSO-flavored HTML) — Word junk (mso-* styles, o:/w: tags,
23305
- // conditional comments) is stripped the same way paste-from-Word is.
23306
- //
23307
- // .docx IS parsed library-free: it is a ZIP of XML, and the ZIP central
23308
- // directory is read directly while DecompressionStream("deflate-raw") handles
23309
- // the inflate. No dependency, no server round-trip.
23310
- // (This comment used to say .docx was not parseable library-free. That was true
23311
- // when it was written and stopped being true when the ZIP reader landed below;
23312
- // it then misled a reader into reporting the feature as missing. Kept explicit
23313
- // so it does not happen again.)
23314
- //
23315
- // 2026-07-28 fidelity pass — what a naive converter silently loses, and what
23316
- // this now preserves:
23317
- // - HYPERLINK URLS. w:hyperlink carries only an r:id; the address lives in
23318
- // word/_rels/document.xml.rels. Without resolving it every link imports as
23319
- // plain text with the URL discarded.
23320
- // - IMAGES. w:drawing -> a:blip r:embed -> word/media/*, inlined as data URIs
23321
- // (set config.documentImportImages = false to skip, for image-heavy files).
23322
- // - ORDERED vs BULLET lists and NESTING. document.xml stores every list the
23323
- // same way; only word/numbering.xml says which is which, so a naive reader
23324
- // turns numbered lists into bullets. w:ilvl gives real nesting, placed
23325
- // inside the parent <li> so the markup is valid.
23326
- // - Paragraph alignment (w:jc).
23327
- // Supply config.documentImportResolver(file) to override any type (e.g. to run
23328
- // a server-side converter for formats this does not cover).
23329
- //
23330
- // API:
23331
- // editor.openImportDialog(options?) -> file picker, then import
23332
- // editor.importFile(file, options?) -> import a File object (Promise)
23333
- // editor.htmlFromImportText(text, kind) -> convert text to HTML (kind: md|html|txt|doc)
23334
- // Command: exec_command "importdocument" opens the picker. Slash: "/import".
23335
- // Config:
23336
- // config.documentImport = false // disable
23337
- // config.documentImportMode = "replace" | "insert" // default "replace"
23338
- // config.documentImportAccept = ".md,.markdown,.html,.htm,.txt,.doc,.docx"
23339
- // config.documentImportResolver = function(file){ return htmlOrPromise; }
23340
- RTE_DefaultConfig.plugin_documentimport = RTE_Plugin_DocumentImport;
23341
- if (typeof RTE_DefaultConfig.documentImport === "undefined") RTE_DefaultConfig.documentImport = true;
23342
-
23343
- function RTE_Plugin_DocumentImport() {
23344
- var obj = this;
23345
- var config, editor;
23346
-
23347
- obj.PluginName = "DocumentImport";
23348
-
23349
- obj.InitConfig = function (argconfig) { config = argconfig; };
23350
-
23351
- obj.InitEditor = function (argeditor) {
23352
- editor = argeditor;
23353
- if (config.documentImport === false) return;
23354
-
23355
- editor.htmlFromImportText = function (text, kind) { return toHtml(String(text == null ? "" : text), kind); };
23356
- editor.importFile = function (file, options) { return importFile(file, options || {}); };
23357
- editor.openImportDialog = function (options) { return openPicker(options || {}); };
23358
-
23359
- editor.attachEvent("exec_command_importdocument", function (state) {
23360
- state.returnValue = true;
23361
- state.stopBubble = true;
23362
- openPicker({});
23363
- });
23364
-
23365
- if (editor.slashCommands && typeof editor.slashCommands.register === "function") {
23366
- try {
23367
- editor.slashCommands.register({
23368
- id: "import-document",
23369
- title: "Import document",
23370
- description: "Open a Markdown, HTML, text, or Word (.doc) file into the editor",
23371
- keywords: ["import", "open", "file", "word", "markdown", "upload"],
23372
- action: function () { openPicker({}); }
23373
- });
23374
- } catch (e) {}
23375
- }
23376
- };
23377
-
23378
- function kindFromName(name) {
23379
- var n = String(name || "").toLowerCase();
23380
- if (/\.(md|markdown|mdown|mkd)$/.test(n)) return "md";
23381
- if (/\.html?$/.test(n)) return "html";
23382
- if (/\.doc$/.test(n)) return "doc";
23383
- if (/\.docx$/.test(n)) return "docx";
23384
- if (/\.txt$/.test(n)) return "txt";
23385
- return "txt";
23386
- }
23387
-
23388
- function esc(s) {
23389
- return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
23390
- }
23391
-
23392
- // A Word text box and a SmartArt graphic are BOXES on the page. Importing
23393
- // them as bare <div>s keeps the words but loses the thing that made them
23394
- // read as separate from the body text, so the import looks lossier than it
23395
- // is. Styled inline rather than by class because this is saved content: it
23396
- // has to survive wherever the host renders it, with no stylesheet of ours
23397
- // loaded. (Found by check-css-contract.mjs -- the classes were emitted with
23398
- // no rule anywhere.)
23399
- var TEXTBOX_STYLE = "border:1px solid #b9c2cf;border-radius:4px;padding:10px 14px;margin:12px 0;background:#fbfcfe";
23400
- var SMARTART_STYLE = "border:1px solid #b9c2cf;border-radius:4px;padding:10px 14px 10px 6px;margin:12px 0;background:#f7faff";
23401
-
23402
- // Strip Word/MSO debris from .doc HTML (same intent as paste-from-Word).
23403
- function cleanWordHtml(html) {
23404
- var h = String(html || "");
23405
- // body only, drop head/xml/style islands and conditional comments
23406
- var bodyMatch = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(h);
23407
- if (bodyMatch) h = bodyMatch[1];
23408
- h = h.replace(/<!--\[if[\s\S]*?\[endif\]-->/gi, "");
23409
- h = h.replace(/<!--[\s\S]*?-->/g, "");
23410
- h = h.replace(/<\/?(o:p|o:|w:|xml|style|meta|link|title|head)[^>]*>/gi, "");
23411
- h = h.replace(/<\\?\?xml[^>]*>/gi, "");
23412
- // strip mso-* declarations + empty style/class/lang attrs
23413
- h = h.replace(/\sstyle="[^"]*"/gi, function (m) {
23414
- var cleaned = m.replace(/mso-[^;"]*;?/gi, "").replace(/style="\s*;*\s*"/i, "");
23415
- return /style="\s*"/.test(cleaned) || cleaned === ' style=""' ? "" : cleaned;
23416
- });
23417
- h = h.replace(/\sclass="Mso[^"]*"/gi, "");
23418
- h = h.replace(/\s(lang|xmlns(:\w+)?)="[^"]*"/gi, "");
23419
- return h;
23420
- }
23421
-
23422
- function toHtml(text, kind) {
23423
- switch (kind) {
23424
- case "md":
23425
- if (editor && typeof editor.fromMarkdown === "function") return editor.fromMarkdown(text, { apply: false });
23426
- return "<p>" + esc(text).replace(/\n{2,}/g, "</p><p>").replace(/\n/g, "<br>") + "</p>";
23427
- case "html":
23428
- return text;
23429
- case "doc":
23430
- return cleanWordHtml(text);
23431
- case "txt":
23432
- default:
23433
- var paras = String(text).replace(/\r\n?/g, "\n").split(/\n{2,}/);
23434
- return paras.map(function (p) {
23435
- return "<p>" + esc(p).replace(/\n/g, "<br>") + "</p>";
23436
- }).join("");
23437
- }
23438
- }
23439
-
23440
- function applyHtml(html, mode) {
23441
- if (html == null) return false;
23442
- if (mode === "insert") {
23443
- if (typeof editor.insertHTML === "function") { editor.insertHTML(html); return true; }
23444
- }
23445
- if (typeof editor.setHTMLCode === "function") { editor.setHTMLCode(html); return true; }
23446
- return false;
23447
- }
23448
-
23449
- function importFile(file, options) {
23450
- var mode = options.mode || config.documentImportMode || "replace";
23451
- return new Promise(function (resolve) {
23452
- if (!file) { resolve(false); return; }
23453
- var kind = kindFromName(file.name);
23454
-
23455
- // BYOK resolver wins for any type (lets a host handle .docx etc.).
23456
- if (typeof config.documentImportResolver === "function") {
23457
- try {
23458
- var r = config.documentImportResolver(file);
23459
- Promise.resolve(r).then(function (html) {
23460
- if (typeof html === "string") { resolve(applyHtml(html, mode)); }
23461
- else { builtin(); }
23462
- }, function () { builtin(); });
23463
- return;
23464
- } catch (e) { builtin(); return; }
23465
- }
23466
- builtin();
23467
-
23468
- function builtin() {
23469
- if (kind === "docx") {
23470
- // Library-free .docx: unzip word/document.xml (native
23471
- // DecompressionStream) + transform WordprocessingML -> HTML.
23472
- if (!docxSupported()) {
23473
- notify("This browser can't unpack .docx (no DecompressionStream). Save as .doc/.html/.md, or wire config.documentImportResolver.");
23474
- resolve(false);
23475
- return;
23476
- }
23477
- readDocx(file).then(function (html) {
23478
- if (html != null) resolve(applyHtml(html, mode));
23479
- else { notify("Could not read this .docx file."); resolve(false); }
23480
- }, function () { notify("Could not read this .docx file."); resolve(false); });
23481
- return;
23482
- }
23483
- if (typeof FileReader === "undefined") { resolve(false); return; }
23484
- var reader = new FileReader();
23485
- reader.onload = function () {
23486
- var html = toHtml(reader.result, kind);
23487
- resolve(applyHtml(html, mode));
23488
- };
23489
- reader.onerror = function () { resolve(false); };
23490
- reader.readAsText(file);
23491
- }
23492
- });
23493
- }
23494
-
23495
- function openPicker(options) {
23496
- try {
23497
- var input = document.createElement("input");
23498
- input.type = "file";
23499
- input.accept = options.accept || config.documentImportAccept || ".md,.markdown,.html,.htm,.txt,.doc,.docx";
23500
- input.style.position = "fixed";
23501
- input.style.left = "-9999px";
23502
- input.addEventListener("change", function () {
23503
- var f = input.files && input.files[0];
23504
- if (f) importFile(f, options);
23505
- setTimeout(function () { try { document.body.removeChild(input); } catch (e) {} }, 0);
23506
- });
23507
- document.body.appendChild(input);
23508
- input.click();
23509
- return true;
23510
- } catch (e) {
23511
- if (window.console) console.error("documentimport: openPicker failed", e);
23512
- return false;
23513
- }
23514
- }
23515
-
23516
- // ---- library-free .docx (ZIP of XML) ----
23517
- function docxSupported() {
23518
- return typeof DecompressionStream === "function" && typeof DOMParser === "function" &&
23519
- (typeof Response === "function" || typeof Blob === "function");
23520
- }
23521
-
23522
- // Locate an entry via the ZIP central directory (reliable comp sizes/offsets).
23523
- function findZipEntry(bytes, wantName) {
23524
- var dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
23525
- // End Of Central Directory: signature 0x06054b50, scan from the end.
23526
- var eocd = -1;
23527
- for (var i = bytes.length - 22; i >= 0 && i >= bytes.length - 22 - 65536; i--) {
23528
- if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
23529
- }
23530
- if (eocd < 0) return null;
23531
- var cdOffset = dv.getUint32(eocd + 16, true);
23532
- var cdCount = dv.getUint16(eocd + 10, true);
23533
- var p = cdOffset;
23534
- for (var n = 0; n < cdCount; n++) {
23535
- if (dv.getUint32(p, true) !== 0x02014b50) break;
23536
- var method = dv.getUint16(p + 10, true);
23537
- var compSize = dv.getUint32(p + 20, true);
23538
- var nameLen = dv.getUint16(p + 28, true);
23539
- var extraLen = dv.getUint16(p + 30, true);
23540
- var commentLen = dv.getUint16(p + 32, true);
23541
- var localOff = dv.getUint32(p + 42, true);
23542
- // Normalize separators: the ZIP spec mandates "/", but some Windows
23543
- // tools (.NET ZipFile on older runtimes) emit "\".
23544
- var name = utf8(bytes.subarray(p + 46, p + 46 + nameLen)).replace(/\\/g, "/");
23545
- if (name === wantName) {
23546
- var lh = new DataView(bytes.buffer, bytes.byteOffset + localOff, 30);
23547
- var lNameLen = lh.getUint16(26, true);
23548
- var lExtraLen = lh.getUint16(28, true);
23549
- var dataStart = localOff + 30 + lNameLen + lExtraLen;
23550
- return { method: method, data: bytes.subarray(dataStart, dataStart + compSize) };
23551
- }
23552
- p += 46 + nameLen + extraLen + commentLen;
23553
- }
23554
- return null;
23555
- }
23556
-
23557
- function utf8(u8) {
23558
- try { return new TextDecoder("utf-8").decode(u8); } catch (e) {
23559
- var s = ""; for (var i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]); return s;
23560
- }
23561
- }
23562
-
23563
- function inflateRaw(u8) {
23564
- // method 0 = stored, 8 = deflate (raw).
23565
- var ds = new DecompressionStream("deflate-raw");
23566
- var blob = new Blob([u8]);
23567
- return new Response(blob.stream().pipeThrough(ds)).arrayBuffer().then(function (ab) {
23568
- return new Uint8Array(ab);
23569
- });
23570
- }
23571
-
23572
- // Every entry whose name matches, decompressed. Needed because a faithful
23573
- // conversion is not just document.xml: the hyperlink URLs live in the rels
23574
- // part, whether a list is bulleted or numbered lives in numbering.xml, and
23575
- // the images live in word/media.
23576
- function listZipEntries(bytes) {
23577
- var dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
23578
- var eocd = -1;
23579
- for (var i = bytes.length - 22; i >= 0 && i >= bytes.length - 22 - 65536; i--) {
23580
- if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
23581
- }
23582
- if (eocd < 0) return [];
23583
- var cdOffset = dv.getUint32(eocd + 16, true);
23584
- var cdCount = dv.getUint16(eocd + 10, true);
23585
- var p = cdOffset, out = [];
23586
- for (var n = 0; n < cdCount; n++) {
23587
- if (dv.getUint32(p, true) !== 0x02014b50) break;
23588
- var method = dv.getUint16(p + 10, true);
23589
- var compSize = dv.getUint32(p + 20, true);
23590
- var nameLen = dv.getUint16(p + 28, true);
23591
- var extraLen = dv.getUint16(p + 30, true);
23592
- var commentLen = dv.getUint16(p + 32, true);
23593
- var localOff = dv.getUint32(p + 42, true);
23594
- var name = utf8(bytes.subarray(p + 46, p + 46 + nameLen)).replace(/\\/g, "/");
23595
- var lh = new DataView(bytes.buffer, bytes.byteOffset + localOff, 30);
23596
- var dataStart = localOff + 30 + lh.getUint16(26, true) + lh.getUint16(28, true);
23597
- out.push({ name: name, method: method, data: bytes.subarray(dataStart, dataStart + compSize) });
23598
- p += 46 + nameLen + extraLen + commentLen;
23599
- }
23600
- return out;
23601
- }
23602
-
23603
- function entryBytes(entry) {
23604
- return entry.method === 0 ? Promise.resolve(entry.data) : inflateRaw(entry.data);
23605
- }
23606
-
23607
- function mimeForExt(name) {
23608
- var e = (name.split(".").pop() || "").toLowerCase();
23609
- if (e === "png") return "image/png";
23610
- if (e === "jpg" || e === "jpeg") return "image/jpeg";
23611
- if (e === "gif") return "image/gif";
23612
- if (e === "bmp") return "image/bmp";
23613
- if (e === "webp") return "image/webp";
23614
- if (e === "svg") return "image/svg+xml";
23615
- return null; // wmf/emf and friends have no browser-renderable form
23616
- }
23617
-
23618
- function bytesToDataUri(u8, mime) {
23619
- var CHUNK = 0x8000, s = "";
23620
- for (var i = 0; i < u8.length; i += CHUNK) {
23621
- s += String.fromCharCode.apply(null, u8.subarray(i, i + CHUNK));
23622
- }
23623
- return "data:" + mime + ";base64," + btoa(s);
23624
- }
23625
-
23626
- function readDocx(file) {
23627
- var bufPromise = file.arrayBuffer ? file.arrayBuffer() : new Promise(function (res, rej) {
23628
- var r = new FileReader(); r.onload = function () { res(r.result); }; r.onerror = rej; r.readAsArrayBuffer(file);
23629
- });
23630
- return bufPromise.then(function (ab) {
23631
- var bytes = new Uint8Array(ab);
23632
- var entries = listZipEntries(bytes);
23633
- var byName = {};
23634
- for (var i = 0; i < entries.length; i++) byName[entries[i].name] = entries[i];
23635
- if (!byName["word/document.xml"]) return null;
23636
-
23637
- var ctx = { rels: {}, numbering: {}, media: {}, footnotes: {}, endnotes: {}, comments: {}, diagrams: {}, vectorNames: {} };
23638
-
23639
- // Images are opt-in: a document full of photographs would otherwise
23640
- // inline megabytes of base64 into the editor.
23641
- var wantMedia = config.documentImportImages !== false;
23642
-
23643
- var jobs = [entryBytes(byName["word/document.xml"]).then(function (b) { ctx.documentXml = utf8(b); })];
23644
-
23645
- if (byName["word/_rels/document.xml.rels"]) {
23646
- jobs.push(entryBytes(byName["word/_rels/document.xml.rels"]).then(function (b) {
23647
- ctx.rels = parseRels(utf8(b));
23648
- }));
23649
- }
23650
- if (byName["word/numbering.xml"]) {
23651
- jobs.push(entryBytes(byName["word/numbering.xml"]).then(function (b) {
23652
- ctx.numbering = parseNumbering(utf8(b));
23653
- }));
23654
- }
23655
- // Footnotes live in their OWN part; document.xml only carries
23656
- // w:footnoteReference with an id. Skip this and a footnoted contract
23657
- // imports with every citation silently deleted.
23658
- if (byName["word/footnotes.xml"]) {
23659
- jobs.push(entryBytes(byName["word/footnotes.xml"]).then(function (b) {
23660
- ctx.footnotes = parseFootnotes(utf8(b), "footnote");
23661
- }));
23662
- }
23663
- // Comment bodies live in their own part too, keyed by id; the
23664
- // document only marks the commented RANGE.
23665
- // Endnotes are structurally identical to footnotes, in their own part.
23666
- if (byName["word/endnotes.xml"]) {
23667
- jobs.push(entryBytes(byName["word/endnotes.xml"]).then(function (b) {
23668
- ctx.endnotes = parseFootnotes(utf8(b), "endnote");
23669
- }));
23670
- }
23671
- if (byName["word/comments.xml"]) {
23672
- jobs.push(entryBytes(byName["word/comments.xml"]).then(function (b) {
23673
- ctx.comments = parseComments(utf8(b));
23674
- }));
23675
- }
23676
- // SmartArt. The drawing in document.xml holds only relationship ids;
23677
- // every word of the diagram lives in word/diagrams/dataN.xml, in the
23678
- // DrawingML *diagram* namespace. A reader that looks for a:blip finds
23679
- // nothing and drops the whole graphic, so an org chart or a process
23680
- // flow imports as empty space.
23681
- for (var dg = 0; dg < entries.length; dg++) {
23682
- (function (ent) {
23683
- if (!/^word\/diagrams\/data\d*\.xml$/.test(ent.name)) return;
23684
- jobs.push(entryBytes(ent).then(function (b) {
23685
- try { ctx.diagrams[ent.name] = parseDiagram(utf8(b)); } catch (e) {}
23686
- }));
23687
- })(entries[dg]);
23688
- }
23689
- if (wantMedia) {
23690
- for (var n = 0; n < entries.length; n++) {
23691
- (function (ent) {
23692
- if (ent.name.indexOf("word/media/") !== 0) return;
23693
- var mime = mimeForExt(ent.name);
23694
- if (!mime) return;
23695
- jobs.push(entryBytes(ent).then(function (b) {
23696
- try { ctx.media[ent.name] = bytesToDataUri(b, mime); } catch (e) {}
23697
- }));
23698
- })(entries[n]);
23699
- }
23700
- }
23701
-
23702
- return Promise.all(jobs).then(function () { return docxXmlToHtml(ctx.documentXml, ctx); });
23703
- });
23704
- }
23705
-
23706
- // r:id -> target. Without this, every hyperlink in the document survives as
23707
- // plain text with its URL silently discarded.
23708
- function parseRels(xml) {
23709
- var map = {};
23710
- try {
23711
- var d = new DOMParser().parseFromString(xml, "application/xml");
23712
- var rs = d.getElementsByTagName("*");
23713
- for (var i = 0; i < rs.length; i++) {
23714
- if (rs[i].localName !== "Relationship") continue;
23715
- var id = rs[i].getAttribute("Id");
23716
- var target = rs[i].getAttribute("Target");
23717
- var mode = rs[i].getAttribute("TargetMode");
23718
- if (id && target) map[id] = { target: target, external: mode === "External" };
23719
- }
23720
- } catch (e) {}
23721
- return map;
23722
- }
23723
-
23724
- // w:id -> plain text of the footnote body.
23725
- // Word reserves ids 0 and -1 for the separator rules drawn above footnotes;
23726
- // those carry type="separator"/"continuationSeparator" and are not content.
23727
- // tagName is "footnote" or "endnote": the two parts share a schema but NOT
23728
- // an element name, so parsing endnotes.xml with "footnote" silently returns
23729
- // nothing at all.
23730
- function parseFootnotes(xml, tagName) {
23731
- var out = {};
23732
- try {
23733
- var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
23734
- var d = new DOMParser().parseFromString(xml, "application/xml");
23735
- var notes = d.getElementsByTagNameNS(W, tagName || "footnote");
23736
- for (var i = 0; i < notes.length; i++) {
23737
- var n = notes[i];
23738
- var type = n.getAttributeNS(W, "type") || n.getAttribute("w:type") || "";
23739
- if (type) continue; // separator, not a real note
23740
- var id = n.getAttributeNS(W, "id") || n.getAttribute("w:id");
23741
- if (id == null) continue;
23742
- // Body text only; the leading footnote-reference mark inside the
23743
- // note is Word's own numbering and would duplicate ours.
23744
- var txt = (n.textContent || "").replace(/\s+/g, " ").trim();
23745
- if (txt) out[String(id)] = txt;
23746
- }
23747
- } catch (e) {}
23748
- return out;
23749
- }
23750
-
23751
- // w:id -> { text, author }
23752
- function parseComments(xml) {
23753
- var out = {};
23754
- try {
23755
- var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
23756
- var d = new DOMParser().parseFromString(xml, "application/xml");
23757
- var cs = d.getElementsByTagNameNS(W, "comment");
23758
- for (var i = 0; i < cs.length; i++) {
23759
- var c = cs[i];
23760
- var id = c.getAttributeNS(W, "id") || c.getAttribute("w:id");
23761
- if (id == null) continue;
23762
- out[String(id)] = {
23763
- text: (c.textContent || "").replace(/\s+/g, " ").trim(),
23764
- author: c.getAttributeNS(W, "author") || c.getAttribute("w:author") || ""
23765
- };
23766
- }
23767
- } catch (e) {}
23768
- return out;
23769
- }
23770
-
23771
- // word/diagrams/dataN.xml -> the diagram's text, one entry per node, in
23772
- // document order.
23773
- //
23774
- // SmartArt has no browser equivalent -- it is a laid-out graphic driven by a
23775
- // layout algorithm -- but its CONTENT is ordinary text, and text is what the
23776
- // reader actually needs. An org chart that imports as an outline is usable; an
23777
- // org chart that imports as nothing is a hole in the document.
23778
- //
23779
- // Nodes with presentation-only roles (`dgm:prSet` with no text, and the
23780
- // ptType "pres"/"parTrans"/"sibTrans" connector points) carry no content and
23781
- // would otherwise contribute blank list items.
23782
- function parseDiagram(xml) {
23783
- var out = [];
23784
- try {
23785
- var DGM = "http://schemas.openxmlformats.org/drawingml/2006/diagram";
23786
- var d = new DOMParser().parseFromString(xml, "application/xml");
23787
- var pts = d.getElementsByTagNameNS(DGM, "pt");
23788
- for (var i = 0; i < pts.length; i++) {
23789
- var type = pts[i].getAttribute("type") || "";
23790
- if (type === "pres" || type === "parTrans" || type === "sibTrans") continue;
23791
- var t = pts[i].getElementsByTagNameNS(DGM, "t")[0];
23792
- if (!t) continue;
23793
- var text = (t.textContent || "").replace(/\s+/g, " ").trim();
23794
- if (text) out.push(text);
23795
- }
23796
- } catch (e) {}
23797
- return out;
23798
- }
23799
-
23800
- // numId -> { level -> "bullet" | "decimal" | ... }. Word stores every list
23801
- // the same way in document.xml; only numbering.xml says whether it renders
23802
- // as bullets or numbers, which is why a naive converter turns every ordered
23803
- // list into a <ul>.
23804
- function parseNumbering(xml) {
23805
- var out = {};
23806
- try {
23807
- var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
23808
- var d = new DOMParser().parseFromString(xml, "application/xml");
23809
- function val(el) { return el ? (el.getAttributeNS(W, "val") || el.getAttribute("w:val") || "") : ""; }
23810
-
23811
- // abstractNumId -> { ilvl -> numFmt }
23812
- var abstracts = {};
23813
- var aNums = d.getElementsByTagNameNS(W, "abstractNum");
23814
- for (var i = 0; i < aNums.length; i++) {
23815
- var aid = aNums[i].getAttributeNS(W, "abstractNumId") || aNums[i].getAttribute("w:abstractNumId");
23816
- var levels = {};
23817
- var lvls = aNums[i].getElementsByTagNameNS(W, "lvl");
23818
- for (var j = 0; j < lvls.length; j++) {
23819
- var ilvl = lvls[j].getAttributeNS(W, "ilvl") || lvls[j].getAttribute("w:ilvl") || "0";
23820
- var fmt = val(lvls[j].getElementsByTagNameNS(W, "numFmt")[0]);
23821
- levels[ilvl] = fmt || "decimal";
23822
- }
23823
- if (aid != null) abstracts[aid] = levels;
23824
- }
23825
- // num -> abstractNumId
23826
- var nums = d.getElementsByTagNameNS(W, "num");
23827
- for (var k = 0; k < nums.length; k++) {
23828
- var nid = nums[k].getAttributeNS(W, "numId") || nums[k].getAttribute("w:numId");
23829
- var ref = nums[k].getElementsByTagNameNS(W, "abstractNumId")[0];
23830
- var aref = val(ref);
23831
- if (nid != null && abstracts[aref]) out[nid] = abstracts[aref];
23832
- }
23833
- } catch (e) {}
23834
- return out;
23835
- }
23836
-
23837
- // ---------------------------------------------------------------------
23838
- // OMML (Office Math Markup) -> LaTeX.
23839
- //
23840
- // Word does not store equations as text, MathML or an image: it stores an
23841
- // <m:oMath> tree in its OWN namespace, interleaved between the runs of a
23842
- // paragraph. A converter that walks w:r elements never sees it, so every
23843
- // equation in the document disappears without a trace -- and because the
23844
- // surrounding sentence still imports, nothing looks broken.
23845
- //
23846
- // The target is the editor's own inline-math format
23847
- // (<span class="rte-math-inline" data-tex="...">), so imported equations
23848
- // are editable and re-render through whichever renderer the host page
23849
- // loads, exactly like one typed in the math dialog.
23850
- //
23851
- // This covers the constructs Word's equation editor actually produces.
23852
- // Anything unrecognised degrades to its literal text rather than vanishing
23853
- // -- a wrong-looking formula is recoverable, a missing one is not.
23854
- var OMML_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math";
23855
-
23856
- // Word writes operators as Unicode, but a TeX renderer wants command names.
23857
- // Passing "∑" through untouched produces a glyph that KaTeX cannot size or
23858
- // place limits on.
23859
- var MATH_SYMBOLS = {
23860
- "∑": "\\sum", "∏": "\\prod", "∐": "\\coprod",
23861
- "∫": "\\int", "∬": "\\iint", "∭": "\\iiint", "∮": "\\oint",
23862
- "⋃": "\\bigcup", "⋂": "\\bigcap",
23863
- "±": "\\pm", "∓": "\\mp", "×": "\\times", "÷": "\\div",
23864
- "−": "-", "≠": "\\neq", "≤": "\\leq", "≥": "\\geq",
23865
- "≈": "\\approx", "≡": "\\equiv", "∝": "\\propto",
23866
- "∞": "\\infty", "∂": "\\partial", "∇": "\\nabla",
23867
- "√": "\\sqrt", "∠": "\\angle", "⋅": "\\cdot", "…": "\\dots",
23868
- "→": "\\to", "←": "\\leftarrow", "⇒": "\\Rightarrow", "⇔": "\\Leftrightarrow",
23869
- "∈": "\\in", "∉": "\\notin", "⊂": "\\subset", "⊆": "\\subseteq",
23870
- "∪": "\\cup", "∩": "\\cap", "∅": "\\emptyset",
23871
- "∀": "\\forall", "∃": "\\exists", "¬": "\\neg",
23872
- "∧": "\\land", "∨": "\\lor", "∴": "\\therefore",
23873
- "α": "\\alpha", "β": "\\beta", "γ": "\\gamma", "δ": "\\delta",
23874
- "ε": "\\epsilon", "ζ": "\\zeta", "η": "\\eta", "θ": "\\theta",
23875
- "ι": "\\iota", "κ": "\\kappa", "λ": "\\lambda", "μ": "\\mu",
23876
- "ν": "\\nu", "ξ": "\\xi", "π": "\\pi", "ρ": "\\rho",
23877
- "σ": "\\sigma", "τ": "\\tau", "υ": "\\upsilon", "φ": "\\varphi",
23878
- "χ": "\\chi", "ψ": "\\psi", "ω": "\\omega",
23879
- "Γ": "\\Gamma", "Δ": "\\Delta", "Θ": "\\Theta", "Λ": "\\Lambda",
23880
- "Ξ": "\\Xi", "Π": "\\Pi", "Σ": "\\Sigma", "Φ": "\\Phi",
23881
- "Ψ": "\\Psi", "Ω": "\\Omega"
23882
- };
23883
- // Accent character -> TeX accent command (m:acc carries the mark as a chr).
23884
- var MATH_ACCENTS = {
23885
- "̂": "\\hat", "̃": "\\tilde", "̄": "\\bar", "̅": "\\overline",
23886
- "̆": "\\breve", "̇": "\\dot", "̈": "\\ddot", "̊": "\\mathring",
23887
- "̌": "\\check", "→": "\\vec", "⃗": "\\vec", "́": "\\acute", "̀": "\\grave"
23888
- };
23889
-
23890
- function ommlToTex(node) {
23891
- var M = OMML_NS;
23892
- function attrVal(parent, ln) {
23893
- // Properties sit in a child element whose only content is m:val.
23894
- for (var i = 0; i < parent.childNodes.length; i++) {
23895
- var c = parent.childNodes[i];
23896
- if (c.nodeType === 1 && c.localName === ln) {
23897
- return c.getAttributeNS(M, "val") || c.getAttribute("m:val") || "";
23898
- }
23899
- }
23900
- return "";
23901
- }
23902
- function propOf(el, propName, ln) {
23903
- var p = child(el, propName);
23904
- return p ? attrVal(p, ln) : "";
23905
- }
23906
- function child(el, ln) {
23907
- for (var i = 0; i < el.childNodes.length; i++) {
23908
- var c = el.childNodes[i];
23909
- if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === M) return c;
23910
- }
23911
- return null;
23912
- }
23913
- function children(el, ln) {
23914
- var out = [];
23915
- for (var i = 0; i < el.childNodes.length; i++) {
23916
- var c = el.childNodes[i];
23917
- if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === M) out.push(c);
23918
- }
23919
- return out;
23920
- }
23921
- // An argument slot: braces so multi-character content binds correctly.
23922
- // "x+1" without them turns \frac{x+1}{2} into \fracx+12.
23923
- function arg(el) {
23924
- return "{" + (el ? walk(el).trim() : "") + "}";
23925
- }
23926
- // A slot where braces are optional. Emitting them unconditionally is
23927
- // correct but produces "{x}^{2}" and "{{i}^{2}}" -- valid TeX that is
23928
- // unreadable the moment the user opens the equation in the math dialog.
23929
- // A lone letter, digit or command needs none.
23930
- function atom(el) {
23931
- var t = el ? walk(el).trim() : "";
23932
- if (/^[A-Za-z0-9]$/.test(t) || /^\\[A-Za-z]+$/.test(t)) return t;
23933
- return "{" + t + "}";
23934
- }
23935
- function mapText(s) {
23936
- var out = "";
23937
- for (var i = 0; i < s.length; i++) {
23938
- var ch = s.charAt(i);
23939
- var sym = MATH_SYMBOLS[ch];
23940
- if (sym) { out += sym + " "; continue; }
23941
- // TeX metacharacters inside literal math text.
23942
- if ("#$%&_{}".indexOf(ch) >= 0) { out += "\\" + ch; continue; }
23943
- if (ch === "\\") { out += "\\backslash "; continue; }
23944
- out += ch;
23945
- }
23946
- return out;
23947
- }
23948
- function walk(el) {
23949
- var tex = "";
23950
- for (var i = 0; i < el.childNodes.length; i++) {
23951
- var c = el.childNodes[i];
23952
- if (c.nodeType !== 1) continue;
23953
- if (c.namespaceURI !== M) { tex += mapText(c.textContent || ""); continue; }
23954
- tex += convert(c);
23955
- }
23956
- return tex;
23957
- }
23958
- function convert(el) {
23959
- var ln = el.localName;
23960
- switch (ln) {
23961
- case "oMath":
23962
- case "oMathPara":
23963
- case "e":
23964
- case "num":
23965
- case "den":
23966
- case "sub":
23967
- case "sup":
23968
- case "deg":
23969
- case "lim":
23970
- case "fName":
23971
- return walk(el);
23972
- // Properties describe how to render, not what to render; emitting
23973
- // their text would leak stray characters into the formula.
23974
- case "rPr": case "ctrlPr": case "fPr": case "radPr": case "naryPr":
23975
- case "dPr": case "sSupPr": case "sSubPr": case "sSubSupPr": case "accPr":
23976
- case "barPr": case "funcPr": case "mPr": case "limLowPr": case "limUppPr":
23977
- case "groupChrPr": case "mcPr": case "eqArrPr": case "boxPr": case "borderBoxPr":
23978
- case "phantPr": case "sPrePr":
23979
- return "";
23980
- case "r":
23981
- return mapText(el.textContent || "");
23982
- case "t":
23983
- return mapText(el.textContent || "");
23984
- case "f": {
23985
- // A "no bar" fraction is Word's binomial/stacked form.
23986
- var type = propOf(el, "fPr", "type");
23987
- var n = arg(child(el, "num")), d = arg(child(el, "den"));
23988
- if (type === "noBar") return "\\binom" + n + d;
23989
- if (type === "skw" || type === "lin") return n + "/" + d;
23990
- return "\\frac" + n + d;
23991
- }
23992
- case "sSup": return atom(child(el, "e")) + "^" + arg(child(el, "sup"));
23993
- case "sSub": return atom(child(el, "e")) + "_" + arg(child(el, "sub"));
23994
- case "sSubSup": return atom(child(el, "e")) + "_" + arg(child(el, "sub")) + "^" + arg(child(el, "sup"));
23995
- case "sPre": return "{}_" + arg(child(el, "sub")) + "^" + arg(child(el, "sup")) + arg(child(el, "e"));
23996
- case "rad": {
23997
- // degHide means a plain square root; otherwise it is \sqrt[n]{}.
23998
- var hide = propOf(el, "radPr", "degHide");
23999
- var deg = child(el, "deg");
24000
- var body = arg(child(el, "e"));
24001
- if (hide === "1" || hide === "on" || hide === "true" || !deg || !(deg.textContent || "").trim()) return "\\sqrt" + body;
24002
- return "\\sqrt[" + walk(deg) + "]" + body;
24003
- }
24004
- case "nary": {
24005
- // The operator itself is an ATTRIBUTE (m:chr), not text. Miss
24006
- // it and every summation imports as a bare limit pair.
24007
- var chr = propOf(el, "naryPr", "chr") || "∫";
24008
- var op = MATH_SYMBOLS[chr] || mapText(chr);
24009
- var subHide = propOf(el, "naryPr", "subHide");
24010
- var supHide = propOf(el, "naryPr", "supHide");
24011
- var s = op;
24012
- if (subHide !== "1" && subHide !== "on" && child(el, "sub")) s += "_" + arg(child(el, "sub"));
24013
- if (supHide !== "1" && supHide !== "on" && child(el, "sup")) s += "^" + arg(child(el, "sup"));
24014
- // The integrand takes no braces: \sum_{i=1}^{n} i^{2}, not
24015
- // \sum_{i=1}^{n} {i^{2}}.
24016
- var nbody = child(el, "e");
24017
- return s + " " + (nbody ? walk(nbody).trim() : "");
24018
- }
24019
- case "d": {
24020
- // \left/\right so the fences grow with their content.
24021
- var beg = propOf(el, "dPr", "begChr");
24022
- var end = propOf(el, "dPr", "endChr");
24023
- var sep = propOf(el, "dPr", "sepChr") || "|";
24024
- if (beg === "") beg = "(";
24025
- if (end === "") end = ")";
24026
- function fence(ch, side) {
24027
- if (!ch) return side === "l" ? "\\left." : "\\right.";
24028
- if (ch === "{" || ch === "}") return "\\" + ch;
24029
- if (ch === "|") return "|";
24030
- if (ch === "‖") return "\\|";
24031
- // THREE different codepoints render as an angle
24032
- // bracket and are indistinguishable on screen: the
24033
- // mathematical pair (U+27E8/9), the CJK pair
24034
- // (U+3008/9) and the deprecated pair (U+2329/A) that
24035
- // several generators still emit. Comparing against a
24036
- // literal in the source would silently match only one
24037
- // of them, so compare by codepoint.
24038
- var cp = ch.charCodeAt(0);
24039
- if (cp === 0x27E8 || cp === 0x3008 || cp === 0x2329) return "\\langle";
24040
- if (cp === 0x27E9 || cp === 0x3009 || cp === 0x232A) return "\\rangle";
24041
- if (ch === "⌊") return "\\lfloor";
24042
- if (ch === "⌋") return "\\rfloor";
24043
- if (ch === "⌈") return "\\lceil";
24044
- if (ch === "⌉") return "\\rceil";
24045
- return ch;
24046
- }
24047
- var parts = children(el, "e");
24048
- var inner = [];
24049
- for (var q = 0; q < parts.length; q++) inner.push(walk(parts[q]));
24050
- return "\\left" + fence(beg, "l") + inner.join(" " + fence(sep, "m") + " ") + "\\right" + fence(end, "r");
24051
- }
24052
- case "func": {
24053
- // Word stores "sin"/"log" as ordinary text; without
24054
- // \operatorname TeX renders it as s*i*n in italics.
24055
- var name = walk(child(el, "fName")).trim();
24056
- 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)$/;
24057
- var head = known.test(name) ? "\\" + name : (name ? "\\operatorname{" + name + "}" : "");
24058
- return head + arg(child(el, "e"));
24059
- }
24060
- case "limLow": {
24061
- var base = walk(child(el, "e")).trim();
24062
- if (base === "lim" || base === "\\lim") return "\\lim_" + arg(child(el, "lim"));
24063
- return "\\underset" + arg(child(el, "lim")) + "{" + base + "}";
24064
- }
24065
- case "limUpp":
24066
- return "\\overset" + arg(child(el, "lim")) + arg(child(el, "e"));
24067
- case "acc": {
24068
- var mark = propOf(el, "accPr", "chr") || "̂";
24069
- var cmd = MATH_ACCENTS[mark] || "\\hat";
24070
- return cmd + arg(child(el, "e"));
24071
- }
24072
- case "bar":
24073
- return (propOf(el, "barPr", "pos") === "bot" ? "\\underline" : "\\overline") + arg(child(el, "e"));
24074
- case "groupChr": {
24075
- var g = propOf(el, "groupChrPr", "chr");
24076
- if (g === "⏟") return "\\underbrace" + arg(child(el, "e"));
24077
- return "\\overbrace" + arg(child(el, "e"));
24078
- }
24079
- case "borderBox":
24080
- return "\\boxed" + arg(child(el, "e"));
24081
- case "box":
24082
- case "phant":
24083
- return walk(el);
24084
- case "m": {
24085
- // Matrix: rows are m:mr, cells are m:e inside them.
24086
- var rows = children(el, "mr"), lines = [];
24087
- for (var r = 0; r < rows.length; r++) {
24088
- var cells = children(rows[r], "e"), cs = [];
24089
- for (var cix = 0; cix < cells.length; cix++) cs.push(walk(cells[cix]));
24090
- lines.push(cs.join(" & "));
24091
- }
24092
- return "\\begin{matrix}" + lines.join(" \\\\ ") + "\\end{matrix}";
24093
- }
24094
- case "eqArr": {
24095
- var eqs = children(el, "e"), rowsOut = [];
24096
- for (var e2 = 0; e2 < eqs.length; e2++) rowsOut.push(walk(eqs[e2]));
24097
- return "\\begin{aligned}" + rowsOut.join(" \\\\ ") + "\\end{aligned}";
24098
- }
24099
- default:
24100
- // Unknown construct: keep the content rather than losing it.
24101
- return walk(el);
24102
- }
24103
- }
24104
- try {
24105
- return convert(node).replace(/\s+/g, " ").trim();
24106
- } catch (e) { return (node.textContent || "").trim(); }
24107
- }
24108
-
24109
- function docxXmlToHtml(xml, ctx) {
24110
- ctx = ctx || { rels: {}, numbering: {}, media: {} };
24111
- if (!ctx.diagrams) ctx.diagrams = {};
24112
- var doc = new DOMParser().parseFromString(xml, "application/xml");
24113
- var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
24114
- var R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
24115
-
24116
- function relTarget(el) {
24117
- if (!el) return null;
24118
- var id = el.getAttributeNS(R, "id") || el.getAttribute("r:id") ||
24119
- el.getAttributeNS(R, "embed") || el.getAttribute("r:embed");
24120
- if (!id) return null;
24121
- return ctx.rels[id] || null;
24122
- }
24123
-
24124
- // <w:drawing>/<w:pict> -> <img> from word/media, inlined as a data URI.
24125
- function imageHtml(node) {
24126
- var blips = node.getElementsByTagName("*");
24127
- for (var i = 0; i < blips.length; i++) {
24128
- if (blips[i].localName !== "blip" && blips[i].localName !== "imagedata") continue;
24129
- var rel = relTarget(blips[i]);
24130
- if (!rel) continue;
24131
- // Targets are relative to word/: "media/image1.png".
24132
- var path = rel.target.replace(/^\.\//, "");
24133
- var key = path.indexOf("word/") === 0 ? path : "word/" + path;
24134
- var uri = ctx.media[key];
24135
- if (uri) return '<img src="' + uri + '" alt="">';
24136
- if (rel.external) return '<img src="' + esc(rel.target) + '" alt="">';
24137
- // A vector metafile (WMF/EMF) has no browser-renderable form, so
24138
- // there is no data URI for it. Dropping it silently leaves the
24139
- // reader with no idea a figure was ever there -- and no filename
24140
- // to go looking for in the original.
24141
- if (/\.(wmf|emf|emz|wmz)$/i.test(path)) {
24142
- var fileName = path.split("/").pop();
24143
- queueBlock('<p class="rte-imported-unsupported" data-source="' + esc(fileName) +
24144
- '"><em>[Unsupported image: ' + esc(fileName) +
24145
- " — Windows metafiles have no browser-renderable form]</em></p>");
24146
- return "";
24147
- }
24148
- }
24149
- queueBlock(diagramHtml(node));
24150
- return "";
24151
- }
24152
-
24153
- // SmartArt: the drawing references word/diagrams/dataN.xml through
24154
- // r:dm. Rendered as a labelled list -- the layout cannot be reproduced,
24155
- // but every word of it survives and stays editable.
24156
- function diagramHtml(node) {
24157
- var all = node.getElementsByTagName("*");
24158
- for (var i = 0; i < all.length; i++) {
24159
- if (all[i].localName !== "relIds") continue;
24160
- var id = all[i].getAttributeNS(R, "dm") || all[i].getAttribute("r:dm");
24161
- var rel = id ? ctx.rels[id] : null;
24162
- if (!rel) continue;
24163
- var path = rel.target.replace(/^\.\//, "");
24164
- var key = path.indexOf("word/") === 0 ? path : "word/" + path;
24165
- var nodes = ctx.diagrams[key];
24166
- if (!nodes || !nodes.length) continue;
24167
- var items = "";
24168
- for (var n = 0; n < nodes.length; n++) items += "<li>" + esc(nodes[n]) + "</li>";
24169
- return '<div class="rte-imported-smartart" style="' + SMARTART_STYLE + '"><ul>' + items + "</ul></div>";
24170
- }
24171
- return "";
24172
- }
24173
- function els(parent, ln) { return parent.getElementsByTagNameNS(W, ln); }
24174
- function firstChildEl(parent, ln) {
24175
- for (var i = 0; i < parent.childNodes.length; i++) {
24176
- var c = parent.childNodes[i];
24177
- if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === W) return c;
24178
- }
24179
- return null;
24180
- }
24181
- function runText(r) {
24182
- var t = "", kids = r.childNodes;
24183
- for (var i = 0; i < kids.length; i++) {
24184
- var c = kids[i];
24185
- if (c.nodeType !== 1) continue;
24186
- // Deleted text is stored as w:delText, never w:t — read only w:t
24187
- // and every tracked deletion imports as an empty run.
24188
- if (c.localName === "t" || c.localName === "delText") t += c.textContent || "";
24189
- else if (c.localName === "tab") t += "\t";
24190
- else if (c.localName === "br" || c.localName === "cr") t += "\n";
24191
- }
24192
- return t;
24193
- }
24194
- function runHtml(r) {
24195
- // A run can carry a picture instead of text; without this every
24196
- // image in the document is silently dropped on import.
24197
- var pics = "";
24198
- for (var d = 0; d < r.childNodes.length; d++) {
24199
- var ch = r.childNodes[d];
24200
- if (ch.nodeType === 1 && (ch.localName === "drawing" || ch.localName === "pict")) {
24201
- pics += imageHtml(ch);
24202
- }
24203
- // A run holding a footnote reference has no text of its own.
24204
- if (ch.nodeType === 1 && ch.localName === "footnoteReference") {
24205
- var fid = ch.getAttributeNS(W, "id") || ch.getAttribute("w:id");
24206
- var body = fid != null ? ctx.footnotes[String(fid)] : null;
24207
- if (body) pics += footnoteMarker(body);
24208
- }
24209
- // Endnotes render through the same notes machinery: Word puts
24210
- // both at the end of the document, and the distinction is not
24211
- // one HTML has a separate representation for.
24212
- if (ch.nodeType === 1 && ch.localName === "endnoteReference") {
24213
- var eid = ch.getAttributeNS(W, "id") || ch.getAttribute("w:id");
24214
- var ebody = eid != null ? ctx.endnotes[String(eid)] : null;
24215
- if (ebody) pics += footnoteMarker(ebody);
24216
- }
24217
- // TEXT BOXES. Their content is nested several levels below the
24218
- // run (w:pict > v:shape > v:textbox > w:txbxContent, or the
24219
- // DrawingML mc:AlternateContent equivalent), so a reader that
24220
- // only looks at a run's DIRECT children drops every word inside
24221
- // them -- silently, with no sign anything is missing.
24222
- if (ch.nodeType === 1 && (ch.localName === "pict" || ch.localName === "drawing" ||
24223
- ch.localName === "object" || ch.localName === "AlternateContent")) {
24224
- collectTextBoxes(ch);
24225
- }
24226
- }
24227
- var txt = esc(runText(r));
24228
- if (!txt) return pics;
24229
- txt = txt.replace(/\n/g, "<br>");
24230
- var rpr = firstChildEl(r, "rPr");
24231
- if (rpr) {
24232
- if (firstChildEl(rpr, "b")) txt = "<strong>" + txt + "</strong>";
24233
- if (firstChildEl(rpr, "i")) txt = "<em>" + txt + "</em>";
24234
- if (firstChildEl(rpr, "u")) txt = "<u>" + txt + "</u>";
24235
- if (firstChildEl(rpr, "strike")) txt = "<s>" + txt + "</s>";
24236
- var vert = firstChildEl(rpr, "vertAlign");
24237
- if (vert) {
24238
- var va = vert.getAttributeNS(W, "val") || vert.getAttribute("w:val") || "";
24239
- if (va === "superscript") txt = "<sup>" + txt + "</sup>";
24240
- else if (va === "subscript") txt = "<sub>" + txt + "</sub>";
24241
- }
24242
- // Character colour and size. w:sz is in HALF-points, so 24 = 12pt;
24243
- // forgetting the halving makes every imported document twice the
24244
- // size it should be.
24245
- var css = "";
24246
- var col = firstChildEl(rpr, "color");
24247
- if (col) {
24248
- var cv = col.getAttributeNS(W, "val") || col.getAttribute("w:val") || "";
24249
- if (cv && /^[0-9A-Fa-f]{6}$/.test(cv)) css += "color:#" + cv + ";";
24250
- }
24251
- var hl = firstChildEl(rpr, "highlight");
24252
- if (hl) {
24253
- var hv = hl.getAttributeNS(W, "val") || hl.getAttribute("w:val") || "";
24254
- if (hv && hv !== "none") css += "background-color:" + hv + ";";
24255
- }
24256
- var sz = firstChildEl(rpr, "sz");
24257
- if (sz) {
24258
- var sv = parseInt(sz.getAttributeNS(W, "val") || sz.getAttribute("w:val") || "", 10);
24259
- if (sv > 0) css += "font-size:" + (sv / 2) + "pt;";
24260
- }
24261
- if (css) txt = '<span style="' + css + '">' + txt + "</span>";
24262
- }
24263
- return pics + txt;
24264
- }
24265
-
24266
- // Block-level content discovered while rendering a paragraph: text
24267
- // boxes, SmartArt outlines, and placeholders for images the browser
24268
- // cannot render. None of them may be emitted inline inside the
24269
- // paragraph that referenced them -- a <div> inside a <p> is invalid and
24270
- // browsers silently close the paragraph around it -- so each is
24271
- // buffered here, already wrapped, and flushed straight after.
24272
- var pendingBlocks = [];
24273
- function queueBlock(html) {
24274
- // Dedup on the whole string: an mc:AlternateContent block carries
24275
- // the same graphic twice, once per rendering flavour.
24276
- if (html && pendingBlocks.indexOf(html) < 0) pendingBlocks.push(html);
24277
- }
24278
- function collectTextBoxes(node) {
24279
- var boxes = node.getElementsByTagName("*");
24280
- for (var i = 0; i < boxes.length; i++) {
24281
- if (boxes[i].localName !== "txbxContent") continue;
24282
- var inner = "";
24283
- var kids = boxes[i].childNodes;
24284
- for (var k = 0; k < kids.length; k++) {
24285
- var c = kids[k];
24286
- if (c.nodeType !== 1) continue;
24287
- if (c.localName === "p") {
24288
- var t = paraInner(c);
24289
- if (t) inner += "<p>" + t + "</p>";
24290
- } else if (c.localName === "tbl") {
24291
- inner += tableHtml(c);
24292
- }
24293
- }
24294
- if (inner) queueBlock('<div class="rte-imported-textbox" style="' + TEXTBOX_STYLE + '">' + inner + "</div>");
24295
- }
24296
- }
24297
-
24298
- function flushBlocks() {
24299
- if (!pendingBlocks.length) return "";
24300
- var html = pendingBlocks.join("");
24301
- pendingBlocks = [];
24302
- return html;
24303
- }
24304
-
24305
- // Emit a marker in the shape footnotes.js already understands, and stash
24306
- // the body so the notes section can be built at the end. The plugin
24307
- // renumbers on load, so the number written here is provisional.
24308
- var importedNotes = [];
24309
- // OMML -> the editor's own inline-math span, so an imported equation is
24310
- // editable in the math dialog and renders through whatever renderer the
24311
- // host page loads. The visible text is the TeX itself: that is what a
24312
- // page with no renderer shows, and it is readable rather than blank.
24313
- function mathHtml(node) {
24314
- var tex = ommlToTex(node);
24315
- if (!tex) return "";
24316
- var display = node.localName === "oMathPara";
24317
- return '<span class="rte-math-inline' + (display ? " rte-math-display" : "") +
24318
- '" data-tex="' + esc(tex) + '">' + esc(tex) + "</span>";
24319
- }
24320
-
24321
- function footnoteMarker(text) {
24322
- var id = "fnimp" + (importedNotes.length + 1);
24323
- importedNotes.push({ id: id, text: text });
24324
- var n = importedNotes.length;
24325
- return '<sup class="rte-fn-ref" data-fn-id="' + id + '" data-fn-number="' + n +
24326
- '" id="fnref-' + id + '" contenteditable="false">' + n + "</sup>";
24327
- }
24328
-
24329
- function footnotesSection() {
24330
- if (!importedNotes.length) return "";
24331
- var items = "";
24332
- for (var i = 0; i < importedNotes.length; i++) {
24333
- var f = importedNotes[i];
24334
- items += '<li class="rte-fn-note" data-fn-id="' + f.id + '" data-fn-number="' + (i + 1) +
24335
- '" id="fn-' + f.id + '">' + esc(f.text) +
24336
- '<a class="rte-fn-back" data-fn-id="' + f.id + '" href="#fnref-' + f.id +
24337
- '" contenteditable="false" title="Back to reference">↩</a></li>';
24338
- }
24339
- return '<section class="rte-footnotes"><h2 class="rte-fn-title">Footnotes</h2>' +
24340
- '<ol class="rte-fn-list" data-numbering="decimal">' + items + "</ol></section>";
24341
- }
24342
- // Runs inside a w:ins / w:del wrapper, rendered with the review markup
24343
- // that the DOCX *export* already round-trips (<ins data-author>/<del>).
24344
- function revisionHtml(node, tag) {
24345
- var inner = "";
24346
- for (var i = 0; i < node.childNodes.length; i++) {
24347
- var c = node.childNodes[i];
24348
- if (c.nodeType !== 1) continue;
24349
- if (c.localName === "r") inner += runHtml(c);
24350
- else if (c.localName === "ins") inner += revisionHtml(c, "ins");
24351
- else if (c.localName === "del") inner += revisionHtml(c, "del");
24352
- }
24353
- if (!inner) return "";
24354
- var author = node.getAttributeNS(W, "author") || node.getAttribute("w:author") || "";
24355
- var date = node.getAttributeNS(W, "date") || node.getAttribute("w:date") || "";
24356
- return "<" + tag +
24357
- (author ? ' data-author="' + esc(author) + '"' : "") +
24358
- (date ? ' data-date="' + esc(date) + '"' : "") +
24359
- ">" + inner + "</" + tag + ">";
24360
- }
24361
-
24362
- function paraInner(p) {
24363
- var h = "", kids = p.childNodes;
24364
- // Word marks a commented RANGE with start/end markers around the runs,
24365
- // so the wrapper has to be opened and closed as we walk, not derived
24366
- // from any single element.
24367
- var openComments = [];
24368
- function commentOpen(id) {
24369
- var c = ctx.comments[String(id)];
24370
- if (!c) return "";
24371
- openComments.push(id);
24372
- return '<span data-comment="' + esc(c.text) + '"' +
24373
- (c.author ? ' data-comment-author="' + esc(c.author) + '"' : "") + ">";
24374
- }
24375
- for (var i = 0; i < kids.length; i++) {
24376
- var c = kids[i];
24377
- if (c.nodeType === 1 && c.localName === "commentRangeStart") {
24378
- h += commentOpen(c.getAttributeNS(W, "id") || c.getAttribute("w:id"));
24379
- continue;
24380
- }
24381
- if (c.nodeType === 1 && c.localName === "commentRangeEnd") {
24382
- if (openComments.length) { openComments.pop(); h += "</span>"; }
24383
- continue;
24384
- }
24385
- if (c.nodeType === 1 && (c.localName === "ins" || c.localName === "del") && c.namespaceURI === W) {
24386
- h += revisionHtml(c, c.localName);
24387
- continue;
24388
- }
24389
- // EQUATIONS. OMML is a sibling of the runs, in its own
24390
- // namespace -- a walker that only recognises w:r skips it and
24391
- // the equation is gone. m:oMathPara is the display (own-line)
24392
- // wrapper around one or more m:oMath.
24393
- if (c.nodeType === 1 && c.namespaceURI === OMML_NS &&
24394
- (c.localName === "oMath" || c.localName === "oMathPara")) {
24395
- h += mathHtml(c);
24396
- continue;
24397
- }
24398
- if (c.nodeType === 1 && c.localName === "r" && c.namespaceURI === W) h += runHtml(c);
24399
- else if (c.nodeType === 1 && c.localName === "hyperlink") {
24400
- var inner = "";
24401
- for (var j = 0; j < c.childNodes.length; j++) if (c.childNodes[j].localName === "r") inner += runHtml(c.childNodes[j]);
24402
- // Resolve r:id through the rels part. Previously the run text
24403
- // was emitted bare and the URL was thrown away, so every link
24404
- // in an imported document became plain text.
24405
- var rel = relTarget(c);
24406
- var anchor = c.getAttributeNS(W, "anchor") || c.getAttribute("w:anchor");
24407
- var href = rel ? rel.target : (anchor ? "#" + anchor : null);
24408
- h += href ? ('<a href="' + esc(href) + '">' + (inner || esc(href)) + "</a>") : inner;
24409
- }
24410
- }
24411
- // A comment range left open at the end of the paragraph would leak an
24412
- // unbalanced <span> into the document.
24413
- while (openComments.length) { openComments.pop(); h += "</span>"; }
24414
- return h;
24415
- }
24416
- function paraStyle(p) {
24417
- var ppr = firstChildEl(p, "pPr"); if (!ppr) return {};
24418
- var info = {};
24419
- var ps = firstChildEl(ppr, "pStyle");
24420
- if (ps) { var v = ps.getAttributeNS(W, "val") || ps.getAttribute("w:val") || ""; var m = /heading(\d)/i.exec(v); if (m) info.heading = Math.min(6, parseInt(m[1], 10)); }
24421
- var numPr = firstChildEl(ppr, "numPr");
24422
- if (numPr) {
24423
- info.list = true;
24424
- var ilvl = firstChildEl(numPr, "ilvl");
24425
- var numId = firstChildEl(numPr, "numId");
24426
- info.level = parseInt(ilvl ? (ilvl.getAttributeNS(W, "val") || ilvl.getAttribute("w:val") || "0") : "0", 10) || 0;
24427
- var nid = numId ? (numId.getAttributeNS(W, "val") || numId.getAttribute("w:val") || "") : "";
24428
- var levels = ctx.numbering[nid];
24429
- var fmt = levels ? levels[String(info.level)] : null;
24430
- // Word marks bullets with numFmt="bullet"; anything else is an
24431
- // ordered list. Defaulting to bullet when numbering.xml is absent
24432
- // matches the old behaviour rather than inventing <ol>s.
24433
- info.ordered = !!fmt && fmt !== "bullet" && fmt !== "none";
24434
- }
24435
- var jc = firstChildEl(ppr, "jc");
24436
- if (jc) {
24437
- var a = jc.getAttributeNS(W, "val") || jc.getAttribute("w:val") || "";
24438
- if (a === "center" || a === "right" || a === "both") {
24439
- info.align = (a === "both") ? "justify" : a;
24440
- }
24441
- }
24442
- return info;
24443
- }
24444
- var body = els(doc, "body")[0];
24445
- if (!body) return "";
24446
- var out = [];
24447
- // Stack of open lists so w:ilvl produces real nesting instead of a flat
24448
- // run of <li>s. liOpen tracks whether an <li> is still open at each
24449
- // depth, because a nested list must live INSIDE its parent <li> --
24450
- // <ol><li>a</li><ol>...</ol></ol> is invalid HTML, and shipping invalid
24451
- // markup out of an importer undermines the whole clean-output promise.
24452
- var listStack = [];
24453
- var liOpen = [];
24454
- function closeListsTo(depth) {
24455
- while (listStack.length > depth) {
24456
- var d = listStack.length - 1;
24457
- if (liOpen[d]) { out.push("</li>"); liOpen[d] = false; }
24458
- out.push("</" + listStack.pop() + ">");
24459
- liOpen.pop();
24460
- }
24461
- }
24462
- for (var i = 0; i < body.childNodes.length; i++) {
24463
- var node = body.childNodes[i];
24464
- if (node.nodeType !== 1) continue;
24465
- // A display equation can sit at body level rather than inside a
24466
- // w:p, so the "wordprocessingml namespace only" filter below would
24467
- // drop it.
24468
- if (node.namespaceURI === OMML_NS &&
24469
- (node.localName === "oMathPara" || node.localName === "oMath")) {
24470
- closeListsTo(0);
24471
- var mh = mathHtml(node);
24472
- if (mh) out.push("<p>" + mh + "</p>");
24473
- continue;
24474
- }
24475
- if (node.namespaceURI !== W) continue;
24476
- if (node.localName === "p") {
24477
- var st = paraStyle(node);
24478
- var inner = paraInner(node);
24479
- if (st.list) {
24480
- var want = (st.level || 0) + 1;
24481
- var tag = st.ordered ? "ol" : "ul";
24482
- // Come back up to this depth FIRST. Checking the type before
24483
- // this ran compared against the wrong level, so a bullet
24484
- // following a deeper numbered item silently joined the <ol>.
24485
- if (listStack.length > want) closeListsTo(want);
24486
- // Now at this depth: a different list type means a new list.
24487
- if (listStack.length === want && listStack[want - 1] !== tag) closeListsTo(want - 1);
24488
- // Going deeper leaves the parent <li> open so the sublist
24489
- // nests inside it.
24490
- while (listStack.length < want) {
24491
- out.push("<" + tag + ">");
24492
- listStack.push(tag);
24493
- liOpen.push(false);
24494
- }
24495
- if (liOpen[want - 1]) { out.push("</li>"); liOpen[want - 1] = false; }
24496
- out.push("<li>" + (inner || ""));
24497
- liOpen[want - 1] = true;
24498
- continue;
24499
- }
24500
- closeListsTo(0);
24501
- if (st.heading) out.push("<h" + st.heading + ">" + (inner || "") + "</h" + st.heading + ">");
24502
- else {
24503
- var style = st.align ? ' style="text-align:' + st.align + '"' : "";
24504
- // A paragraph that contained ONLY a text box has no text of
24505
- // its own; emitting an empty <p> before the box adds a blank
24506
- // line that was never in the document.
24507
- var boxes = flushBlocks();
24508
- if (inner || !boxes) out.push("<p" + style + ">" + (inner || "<br>") + "</p>");
24509
- if (boxes) out.push(boxes);
24510
- }
24511
- } else if (node.localName === "tbl") {
24512
- closeListsTo(0);
24513
- out.push(tableHtml(node));
24514
- }
24515
- }
24516
- closeListsTo(0);
24517
- // A text box hanging off the last paragraph, or off a heading/list item,
24518
- // still has to land somewhere.
24519
- var trailing = flushBlocks();
24520
- if (trailing) out.push(trailing);
24521
- // The notes section goes last, after every marker has been collected.
24522
- var notes = footnotesSection();
24523
- if (notes) out.push(notes);
24524
- return out.join("\n");
24525
-
24526
- function tableHtml(tbl) {
24527
- var rows = [], kids = tbl.childNodes;
24528
- for (var i = 0; i < kids.length; i++) {
24529
- var tr = kids[i];
24530
- if (tr.nodeType !== 1 || tr.localName !== "tr") continue;
24531
- var cells = [];
24532
- for (var j = 0; j < tr.childNodes.length; j++) {
24533
- var tc = tr.childNodes[j];
24534
- if (tc.nodeType !== 1 || tc.localName !== "tc") continue;
24535
- var cellHtml = "";
24536
- for (var k = 0; k < tc.childNodes.length; k++) {
24537
- if (tc.childNodes[k].localName === "p") cellHtml += "<p>" + (paraInner(tc.childNodes[k]) || "<br>") + "</p>";
24538
- }
24539
- cells.push("<td>" + cellHtml + "</td>");
24540
- }
24541
- rows.push("<tr>" + cells.join("") + "</tr>");
24542
- }
24543
- return "<table>" + rows.join("") + "</table>";
24544
- }
24545
- }
24546
-
24547
- function notify(msg) {
24548
- try {
24549
- if (editor && typeof editor.createDialog === "function") {
24550
- var d = editor.createDialog((editor.getLangText && editor.getLangText("importtitle")) || "Import document", "rte-dialog-import");
24551
- var w = d.ownerDocument.createElement("div");
24552
- w.style.cssText = "padding:16px;max-width:420px;font:13px -apple-system,Segoe UI,sans-serif;line-height:1.5";
24553
- w.textContent = msg;
24554
- d.appendChild(w);
24555
- return;
24556
- }
24557
- } catch (e) {}
24558
- if (window.console) console.warn("documentimport:", msg);
24559
- }
24560
- }
23373
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
23374
+
23375
+ // 2026-06-09 Document import — the natural pair to Export-to-Word/Markdown/PDF.
23376
+ // Opens a local file and loads it into the editor. Library-free for the common
23377
+ // formats: Markdown (.md/.markdown via the core fromMarkdown engine), HTML
23378
+ // (.html/.htm), plain text (.txt), and Word's HTML-based format (.doc, which
23379
+ // Word saves as MSO-flavored HTML) — Word junk (mso-* styles, o:/w: tags,
23380
+ // conditional comments) is stripped the same way paste-from-Word is.
23381
+ //
23382
+ // .docx IS parsed library-free: it is a ZIP of XML, and the ZIP central
23383
+ // directory is read directly while DecompressionStream("deflate-raw") handles
23384
+ // the inflate. No dependency, no server round-trip.
23385
+ // (This comment used to say .docx was not parseable library-free. That was true
23386
+ // when it was written and stopped being true when the ZIP reader landed below;
23387
+ // it then misled a reader into reporting the feature as missing. Kept explicit
23388
+ // so it does not happen again.)
23389
+ //
23390
+ // 2026-07-28 fidelity pass — what a naive converter silently loses, and what
23391
+ // this now preserves:
23392
+ // - HYPERLINK URLS. w:hyperlink carries only an r:id; the address lives in
23393
+ // word/_rels/document.xml.rels. Without resolving it every link imports as
23394
+ // plain text with the URL discarded.
23395
+ // - IMAGES. w:drawing -> a:blip r:embed -> word/media/*, inlined as data URIs
23396
+ // (set config.documentImportImages = false to skip, for image-heavy files).
23397
+ // - ORDERED vs BULLET lists and NESTING. document.xml stores every list the
23398
+ // same way; only word/numbering.xml says which is which, so a naive reader
23399
+ // turns numbered lists into bullets. w:ilvl gives real nesting, placed
23400
+ // inside the parent <li> so the markup is valid.
23401
+ // - Paragraph alignment (w:jc).
23402
+ // Supply config.documentImportResolver(file) to override any type (e.g. to run
23403
+ // a server-side converter for formats this does not cover).
23404
+ //
23405
+ // API:
23406
+ // editor.openImportDialog(options?) -> file picker, then import
23407
+ // editor.importFile(file, options?) -> import a File object (Promise)
23408
+ // editor.htmlFromImportText(text, kind) -> convert text to HTML (kind: md|html|txt|doc)
23409
+ // Command: exec_command "importdocument" opens the picker. Slash: "/import".
23410
+ // Config:
23411
+ // config.documentImport = false // disable
23412
+ // config.documentImportMode = "replace" | "insert" // default "replace"
23413
+ // config.documentImportAccept = ".md,.markdown,.html,.htm,.txt,.doc,.docx"
23414
+ // config.documentImportResolver = function(file){ return htmlOrPromise; }
23415
+ RTE_DefaultConfig.plugin_documentimport = RTE_Plugin_DocumentImport;
23416
+ if (typeof RTE_DefaultConfig.documentImport === "undefined") RTE_DefaultConfig.documentImport = true;
23417
+
23418
+ function RTE_Plugin_DocumentImport() {
23419
+ var obj = this;
23420
+ var config, editor;
23421
+
23422
+ obj.PluginName = "DocumentImport";
23423
+
23424
+ obj.InitConfig = function (argconfig) { config = argconfig; };
23425
+
23426
+ obj.InitEditor = function (argeditor) {
23427
+ editor = argeditor;
23428
+ if (config.documentImport === false) return;
23429
+
23430
+ editor.htmlFromImportText = function (text, kind) { return toHtml(String(text == null ? "" : text), kind); };
23431
+ editor.importFile = function (file, options) { return importFile(file, options || {}); };
23432
+ editor.openImportDialog = function (options) { return openPicker(options || {}); };
23433
+
23434
+ editor.attachEvent("exec_command_importdocument", function (state) {
23435
+ state.returnValue = true;
23436
+ state.stopBubble = true;
23437
+ openPicker({});
23438
+ });
23439
+
23440
+ if (editor.slashCommands && typeof editor.slashCommands.register === "function") {
23441
+ try {
23442
+ editor.slashCommands.register({
23443
+ id: "import-document",
23444
+ title: "Import document",
23445
+ description: "Open a Markdown, HTML, text, or Word (.doc) file into the editor",
23446
+ keywords: ["import", "open", "file", "word", "markdown", "upload"],
23447
+ action: function () { openPicker({}); }
23448
+ });
23449
+ } catch (e) {}
23450
+ }
23451
+ };
23452
+
23453
+ function kindFromName(name) {
23454
+ var n = String(name || "").toLowerCase();
23455
+ if (/\.(md|markdown|mdown|mkd)$/.test(n)) return "md";
23456
+ if (/\.html?$/.test(n)) return "html";
23457
+ if (/\.doc$/.test(n)) return "doc";
23458
+ if (/\.docx$/.test(n)) return "docx";
23459
+ if (/\.txt$/.test(n)) return "txt";
23460
+ return "txt";
23461
+ }
23462
+
23463
+ function esc(s) {
23464
+ return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
23465
+ }
23466
+
23467
+ // A Word text box and a SmartArt graphic are BOXES on the page. Importing
23468
+ // them as bare <div>s keeps the words but loses the thing that made them
23469
+ // read as separate from the body text, so the import looks lossier than it
23470
+ // is. Styled inline rather than by class because this is saved content: it
23471
+ // has to survive wherever the host renders it, with no stylesheet of ours
23472
+ // loaded. (Found by check-css-contract.mjs -- the classes were emitted with
23473
+ // no rule anywhere.)
23474
+ var TEXTBOX_STYLE = "border:1px solid #b9c2cf;border-radius:4px;padding:10px 14px;margin:12px 0;background:#fbfcfe";
23475
+ var SMARTART_STYLE = "border:1px solid #b9c2cf;border-radius:4px;padding:10px 14px 10px 6px;margin:12px 0;background:#f7faff";
23476
+
23477
+ // Strip Word/MSO debris from .doc HTML (same intent as paste-from-Word).
23478
+ function cleanWordHtml(html) {
23479
+ var h = String(html || "");
23480
+ // body only, drop head/xml/style islands and conditional comments
23481
+ var bodyMatch = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(h);
23482
+ if (bodyMatch) h = bodyMatch[1];
23483
+ h = h.replace(/<!--\[if[\s\S]*?\[endif\]-->/gi, "");
23484
+ h = h.replace(/<!--[\s\S]*?-->/g, "");
23485
+ h = h.replace(/<\/?(o:p|o:|w:|xml|style|meta|link|title|head)[^>]*>/gi, "");
23486
+ h = h.replace(/<\\?\?xml[^>]*>/gi, "");
23487
+ // strip mso-* declarations + empty style/class/lang attrs
23488
+ h = h.replace(/\sstyle="[^"]*"/gi, function (m) {
23489
+ var cleaned = m.replace(/mso-[^;"]*;?/gi, "").replace(/style="\s*;*\s*"/i, "");
23490
+ return /style="\s*"/.test(cleaned) || cleaned === ' style=""' ? "" : cleaned;
23491
+ });
23492
+ h = h.replace(/\sclass="Mso[^"]*"/gi, "");
23493
+ h = h.replace(/\s(lang|xmlns(:\w+)?)="[^"]*"/gi, "");
23494
+ return h;
23495
+ }
23496
+
23497
+ function toHtml(text, kind) {
23498
+ switch (kind) {
23499
+ case "md":
23500
+ if (editor && typeof editor.fromMarkdown === "function") return editor.fromMarkdown(text, { apply: false });
23501
+ return "<p>" + esc(text).replace(/\n{2,}/g, "</p><p>").replace(/\n/g, "<br>") + "</p>";
23502
+ case "html":
23503
+ return text;
23504
+ case "doc":
23505
+ return cleanWordHtml(text);
23506
+ case "txt":
23507
+ default:
23508
+ var paras = String(text).replace(/\r\n?/g, "\n").split(/\n{2,}/);
23509
+ return paras.map(function (p) {
23510
+ return "<p>" + esc(p).replace(/\n/g, "<br>") + "</p>";
23511
+ }).join("");
23512
+ }
23513
+ }
23514
+
23515
+ function applyHtml(html, mode) {
23516
+ if (html == null) return false;
23517
+ if (mode === "insert") {
23518
+ if (typeof editor.insertHTML === "function") { editor.insertHTML(html); return true; }
23519
+ }
23520
+ if (typeof editor.setHTMLCode === "function") { editor.setHTMLCode(html); return true; }
23521
+ return false;
23522
+ }
23523
+
23524
+ function importFile(file, options) {
23525
+ var mode = options.mode || config.documentImportMode || "replace";
23526
+ return new Promise(function (resolve) {
23527
+ if (!file) { resolve(false); return; }
23528
+ var kind = kindFromName(file.name);
23529
+
23530
+ // BYOK resolver wins for any type (lets a host handle .docx etc.).
23531
+ if (typeof config.documentImportResolver === "function") {
23532
+ try {
23533
+ var r = config.documentImportResolver(file);
23534
+ Promise.resolve(r).then(function (html) {
23535
+ if (typeof html === "string") { resolve(applyHtml(html, mode)); }
23536
+ else { builtin(); }
23537
+ }, function () { builtin(); });
23538
+ return;
23539
+ } catch (e) { builtin(); return; }
23540
+ }
23541
+ builtin();
23542
+
23543
+ function builtin() {
23544
+ if (kind === "docx") {
23545
+ // Library-free .docx: unzip word/document.xml (native
23546
+ // DecompressionStream) + transform WordprocessingML -> HTML.
23547
+ if (!docxSupported()) {
23548
+ notify("This browser can't unpack .docx (no DecompressionStream). Save as .doc/.html/.md, or wire config.documentImportResolver.");
23549
+ resolve(false);
23550
+ return;
23551
+ }
23552
+ readDocx(file).then(function (html) {
23553
+ if (html != null) resolve(applyHtml(html, mode));
23554
+ else { notify("Could not read this .docx file."); resolve(false); }
23555
+ }, function () { notify("Could not read this .docx file."); resolve(false); });
23556
+ return;
23557
+ }
23558
+ if (typeof FileReader === "undefined") { resolve(false); return; }
23559
+ var reader = new FileReader();
23560
+ reader.onload = function () {
23561
+ var html = toHtml(reader.result, kind);
23562
+ resolve(applyHtml(html, mode));
23563
+ };
23564
+ reader.onerror = function () { resolve(false); };
23565
+ reader.readAsText(file);
23566
+ }
23567
+ });
23568
+ }
23569
+
23570
+ function openPicker(options) {
23571
+ try {
23572
+ var input = document.createElement("input");
23573
+ input.type = "file";
23574
+ input.accept = options.accept || config.documentImportAccept || ".md,.markdown,.html,.htm,.txt,.doc,.docx";
23575
+ input.style.position = "fixed";
23576
+ input.style.left = "-9999px";
23577
+ input.addEventListener("change", function () {
23578
+ var f = input.files && input.files[0];
23579
+ if (f) importFile(f, options);
23580
+ setTimeout(function () { try { document.body.removeChild(input); } catch (e) {} }, 0);
23581
+ });
23582
+ document.body.appendChild(input);
23583
+ input.click();
23584
+ return true;
23585
+ } catch (e) {
23586
+ if (window.console) console.error("documentimport: openPicker failed", e);
23587
+ return false;
23588
+ }
23589
+ }
23590
+
23591
+ // ---- library-free .docx (ZIP of XML) ----
23592
+ function docxSupported() {
23593
+ return typeof DecompressionStream === "function" && typeof DOMParser === "function" &&
23594
+ (typeof Response === "function" || typeof Blob === "function");
23595
+ }
23596
+
23597
+ // Locate an entry via the ZIP central directory (reliable comp sizes/offsets).
23598
+ function findZipEntry(bytes, wantName) {
23599
+ var dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
23600
+ // End Of Central Directory: signature 0x06054b50, scan from the end.
23601
+ var eocd = -1;
23602
+ for (var i = bytes.length - 22; i >= 0 && i >= bytes.length - 22 - 65536; i--) {
23603
+ if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
23604
+ }
23605
+ if (eocd < 0) return null;
23606
+ var cdOffset = dv.getUint32(eocd + 16, true);
23607
+ var cdCount = dv.getUint16(eocd + 10, true);
23608
+ var p = cdOffset;
23609
+ for (var n = 0; n < cdCount; n++) {
23610
+ if (dv.getUint32(p, true) !== 0x02014b50) break;
23611
+ var method = dv.getUint16(p + 10, true);
23612
+ var compSize = dv.getUint32(p + 20, true);
23613
+ var nameLen = dv.getUint16(p + 28, true);
23614
+ var extraLen = dv.getUint16(p + 30, true);
23615
+ var commentLen = dv.getUint16(p + 32, true);
23616
+ var localOff = dv.getUint32(p + 42, true);
23617
+ // Normalize separators: the ZIP spec mandates "/", but some Windows
23618
+ // tools (.NET ZipFile on older runtimes) emit "\".
23619
+ var name = utf8(bytes.subarray(p + 46, p + 46 + nameLen)).replace(/\\/g, "/");
23620
+ if (name === wantName) {
23621
+ var lh = new DataView(bytes.buffer, bytes.byteOffset + localOff, 30);
23622
+ var lNameLen = lh.getUint16(26, true);
23623
+ var lExtraLen = lh.getUint16(28, true);
23624
+ var dataStart = localOff + 30 + lNameLen + lExtraLen;
23625
+ return { method: method, data: bytes.subarray(dataStart, dataStart + compSize) };
23626
+ }
23627
+ p += 46 + nameLen + extraLen + commentLen;
23628
+ }
23629
+ return null;
23630
+ }
23631
+
23632
+ function utf8(u8) {
23633
+ try { return new TextDecoder("utf-8").decode(u8); } catch (e) {
23634
+ var s = ""; for (var i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]); return s;
23635
+ }
23636
+ }
23637
+
23638
+ function inflateRaw(u8) {
23639
+ // method 0 = stored, 8 = deflate (raw).
23640
+ var ds = new DecompressionStream("deflate-raw");
23641
+ var blob = new Blob([u8]);
23642
+ return new Response(blob.stream().pipeThrough(ds)).arrayBuffer().then(function (ab) {
23643
+ return new Uint8Array(ab);
23644
+ });
23645
+ }
23646
+
23647
+ // Every entry whose name matches, decompressed. Needed because a faithful
23648
+ // conversion is not just document.xml: the hyperlink URLs live in the rels
23649
+ // part, whether a list is bulleted or numbered lives in numbering.xml, and
23650
+ // the images live in word/media.
23651
+ function listZipEntries(bytes) {
23652
+ var dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
23653
+ var eocd = -1;
23654
+ for (var i = bytes.length - 22; i >= 0 && i >= bytes.length - 22 - 65536; i--) {
23655
+ if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; }
23656
+ }
23657
+ if (eocd < 0) return [];
23658
+ var cdOffset = dv.getUint32(eocd + 16, true);
23659
+ var cdCount = dv.getUint16(eocd + 10, true);
23660
+ var p = cdOffset, out = [];
23661
+ for (var n = 0; n < cdCount; n++) {
23662
+ if (dv.getUint32(p, true) !== 0x02014b50) break;
23663
+ var method = dv.getUint16(p + 10, true);
23664
+ var compSize = dv.getUint32(p + 20, true);
23665
+ var nameLen = dv.getUint16(p + 28, true);
23666
+ var extraLen = dv.getUint16(p + 30, true);
23667
+ var commentLen = dv.getUint16(p + 32, true);
23668
+ var localOff = dv.getUint32(p + 42, true);
23669
+ var name = utf8(bytes.subarray(p + 46, p + 46 + nameLen)).replace(/\\/g, "/");
23670
+ var lh = new DataView(bytes.buffer, bytes.byteOffset + localOff, 30);
23671
+ var dataStart = localOff + 30 + lh.getUint16(26, true) + lh.getUint16(28, true);
23672
+ out.push({ name: name, method: method, data: bytes.subarray(dataStart, dataStart + compSize) });
23673
+ p += 46 + nameLen + extraLen + commentLen;
23674
+ }
23675
+ return out;
23676
+ }
23677
+
23678
+ function entryBytes(entry) {
23679
+ return entry.method === 0 ? Promise.resolve(entry.data) : inflateRaw(entry.data);
23680
+ }
23681
+
23682
+ function mimeForExt(name) {
23683
+ var e = (name.split(".").pop() || "").toLowerCase();
23684
+ if (e === "png") return "image/png";
23685
+ if (e === "jpg" || e === "jpeg") return "image/jpeg";
23686
+ if (e === "gif") return "image/gif";
23687
+ if (e === "bmp") return "image/bmp";
23688
+ if (e === "webp") return "image/webp";
23689
+ if (e === "svg") return "image/svg+xml";
23690
+ return null; // wmf/emf and friends have no browser-renderable form
23691
+ }
23692
+
23693
+ function bytesToDataUri(u8, mime) {
23694
+ var CHUNK = 0x8000, s = "";
23695
+ for (var i = 0; i < u8.length; i += CHUNK) {
23696
+ s += String.fromCharCode.apply(null, u8.subarray(i, i + CHUNK));
23697
+ }
23698
+ return "data:" + mime + ";base64," + btoa(s);
23699
+ }
23700
+
23701
+ function readDocx(file) {
23702
+ var bufPromise = file.arrayBuffer ? file.arrayBuffer() : new Promise(function (res, rej) {
23703
+ var r = new FileReader(); r.onload = function () { res(r.result); }; r.onerror = rej; r.readAsArrayBuffer(file);
23704
+ });
23705
+ return bufPromise.then(function (ab) {
23706
+ var bytes = new Uint8Array(ab);
23707
+ var entries = listZipEntries(bytes);
23708
+ var byName = {};
23709
+ for (var i = 0; i < entries.length; i++) byName[entries[i].name] = entries[i];
23710
+ if (!byName["word/document.xml"]) return null;
23711
+
23712
+ var ctx = { rels: {}, numbering: {}, media: {}, footnotes: {}, endnotes: {}, comments: {}, diagrams: {}, vectorNames: {} };
23713
+
23714
+ // Images are opt-in: a document full of photographs would otherwise
23715
+ // inline megabytes of base64 into the editor.
23716
+ var wantMedia = config.documentImportImages !== false;
23717
+
23718
+ var jobs = [entryBytes(byName["word/document.xml"]).then(function (b) { ctx.documentXml = utf8(b); })];
23719
+
23720
+ if (byName["word/_rels/document.xml.rels"]) {
23721
+ jobs.push(entryBytes(byName["word/_rels/document.xml.rels"]).then(function (b) {
23722
+ ctx.rels = parseRels(utf8(b));
23723
+ }));
23724
+ }
23725
+ if (byName["word/numbering.xml"]) {
23726
+ jobs.push(entryBytes(byName["word/numbering.xml"]).then(function (b) {
23727
+ ctx.numbering = parseNumbering(utf8(b));
23728
+ }));
23729
+ }
23730
+ // Footnotes live in their OWN part; document.xml only carries
23731
+ // w:footnoteReference with an id. Skip this and a footnoted contract
23732
+ // imports with every citation silently deleted.
23733
+ if (byName["word/footnotes.xml"]) {
23734
+ jobs.push(entryBytes(byName["word/footnotes.xml"]).then(function (b) {
23735
+ ctx.footnotes = parseFootnotes(utf8(b), "footnote");
23736
+ }));
23737
+ }
23738
+ // Comment bodies live in their own part too, keyed by id; the
23739
+ // document only marks the commented RANGE.
23740
+ // Endnotes are structurally identical to footnotes, in their own part.
23741
+ if (byName["word/endnotes.xml"]) {
23742
+ jobs.push(entryBytes(byName["word/endnotes.xml"]).then(function (b) {
23743
+ ctx.endnotes = parseFootnotes(utf8(b), "endnote");
23744
+ }));
23745
+ }
23746
+ if (byName["word/comments.xml"]) {
23747
+ jobs.push(entryBytes(byName["word/comments.xml"]).then(function (b) {
23748
+ ctx.comments = parseComments(utf8(b));
23749
+ }));
23750
+ }
23751
+ // SmartArt. The drawing in document.xml holds only relationship ids;
23752
+ // every word of the diagram lives in word/diagrams/dataN.xml, in the
23753
+ // DrawingML *diagram* namespace. A reader that looks for a:blip finds
23754
+ // nothing and drops the whole graphic, so an org chart or a process
23755
+ // flow imports as empty space.
23756
+ for (var dg = 0; dg < entries.length; dg++) {
23757
+ (function (ent) {
23758
+ if (!/^word\/diagrams\/data\d*\.xml$/.test(ent.name)) return;
23759
+ jobs.push(entryBytes(ent).then(function (b) {
23760
+ try { ctx.diagrams[ent.name] = parseDiagram(utf8(b)); } catch (e) {}
23761
+ }));
23762
+ })(entries[dg]);
23763
+ }
23764
+ if (wantMedia) {
23765
+ for (var n = 0; n < entries.length; n++) {
23766
+ (function (ent) {
23767
+ if (ent.name.indexOf("word/media/") !== 0) return;
23768
+ var mime = mimeForExt(ent.name);
23769
+ if (!mime) return;
23770
+ jobs.push(entryBytes(ent).then(function (b) {
23771
+ try { ctx.media[ent.name] = bytesToDataUri(b, mime); } catch (e) {}
23772
+ }));
23773
+ })(entries[n]);
23774
+ }
23775
+ }
23776
+
23777
+ return Promise.all(jobs).then(function () { return docxXmlToHtml(ctx.documentXml, ctx); });
23778
+ });
23779
+ }
23780
+
23781
+ // r:id -> target. Without this, every hyperlink in the document survives as
23782
+ // plain text with its URL silently discarded.
23783
+ function parseRels(xml) {
23784
+ var map = {};
23785
+ try {
23786
+ var d = new DOMParser().parseFromString(xml, "application/xml");
23787
+ var rs = d.getElementsByTagName("*");
23788
+ for (var i = 0; i < rs.length; i++) {
23789
+ if (rs[i].localName !== "Relationship") continue;
23790
+ var id = rs[i].getAttribute("Id");
23791
+ var target = rs[i].getAttribute("Target");
23792
+ var mode = rs[i].getAttribute("TargetMode");
23793
+ if (id && target) map[id] = { target: target, external: mode === "External" };
23794
+ }
23795
+ } catch (e) {}
23796
+ return map;
23797
+ }
23798
+
23799
+ // w:id -> plain text of the footnote body.
23800
+ // Word reserves ids 0 and -1 for the separator rules drawn above footnotes;
23801
+ // those carry type="separator"/"continuationSeparator" and are not content.
23802
+ // tagName is "footnote" or "endnote": the two parts share a schema but NOT
23803
+ // an element name, so parsing endnotes.xml with "footnote" silently returns
23804
+ // nothing at all.
23805
+ function parseFootnotes(xml, tagName) {
23806
+ var out = {};
23807
+ try {
23808
+ var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
23809
+ var d = new DOMParser().parseFromString(xml, "application/xml");
23810
+ var notes = d.getElementsByTagNameNS(W, tagName || "footnote");
23811
+ for (var i = 0; i < notes.length; i++) {
23812
+ var n = notes[i];
23813
+ var type = n.getAttributeNS(W, "type") || n.getAttribute("w:type") || "";
23814
+ if (type) continue; // separator, not a real note
23815
+ var id = n.getAttributeNS(W, "id") || n.getAttribute("w:id");
23816
+ if (id == null) continue;
23817
+ // Body text only; the leading footnote-reference mark inside the
23818
+ // note is Word's own numbering and would duplicate ours.
23819
+ var txt = (n.textContent || "").replace(/\s+/g, " ").trim();
23820
+ if (txt) out[String(id)] = txt;
23821
+ }
23822
+ } catch (e) {}
23823
+ return out;
23824
+ }
23825
+
23826
+ // w:id -> { text, author }
23827
+ function parseComments(xml) {
23828
+ var out = {};
23829
+ try {
23830
+ var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
23831
+ var d = new DOMParser().parseFromString(xml, "application/xml");
23832
+ var cs = d.getElementsByTagNameNS(W, "comment");
23833
+ for (var i = 0; i < cs.length; i++) {
23834
+ var c = cs[i];
23835
+ var id = c.getAttributeNS(W, "id") || c.getAttribute("w:id");
23836
+ if (id == null) continue;
23837
+ out[String(id)] = {
23838
+ text: (c.textContent || "").replace(/\s+/g, " ").trim(),
23839
+ author: c.getAttributeNS(W, "author") || c.getAttribute("w:author") || ""
23840
+ };
23841
+ }
23842
+ } catch (e) {}
23843
+ return out;
23844
+ }
23845
+
23846
+ // word/diagrams/dataN.xml -> the diagram's text, one entry per node, in
23847
+ // document order.
23848
+ //
23849
+ // SmartArt has no browser equivalent -- it is a laid-out graphic driven by a
23850
+ // layout algorithm -- but its CONTENT is ordinary text, and text is what the
23851
+ // reader actually needs. An org chart that imports as an outline is usable; an
23852
+ // org chart that imports as nothing is a hole in the document.
23853
+ //
23854
+ // Nodes with presentation-only roles (`dgm:prSet` with no text, and the
23855
+ // ptType "pres"/"parTrans"/"sibTrans" connector points) carry no content and
23856
+ // would otherwise contribute blank list items.
23857
+ function parseDiagram(xml) {
23858
+ var out = [];
23859
+ try {
23860
+ var DGM = "http://schemas.openxmlformats.org/drawingml/2006/diagram";
23861
+ var d = new DOMParser().parseFromString(xml, "application/xml");
23862
+ var pts = d.getElementsByTagNameNS(DGM, "pt");
23863
+ for (var i = 0; i < pts.length; i++) {
23864
+ var type = pts[i].getAttribute("type") || "";
23865
+ if (type === "pres" || type === "parTrans" || type === "sibTrans") continue;
23866
+ var t = pts[i].getElementsByTagNameNS(DGM, "t")[0];
23867
+ if (!t) continue;
23868
+ var text = (t.textContent || "").replace(/\s+/g, " ").trim();
23869
+ if (text) out.push(text);
23870
+ }
23871
+ } catch (e) {}
23872
+ return out;
23873
+ }
23874
+
23875
+ // numId -> { level -> "bullet" | "decimal" | ... }. Word stores every list
23876
+ // the same way in document.xml; only numbering.xml says whether it renders
23877
+ // as bullets or numbers, which is why a naive converter turns every ordered
23878
+ // list into a <ul>.
23879
+ function parseNumbering(xml) {
23880
+ var out = {};
23881
+ try {
23882
+ var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
23883
+ var d = new DOMParser().parseFromString(xml, "application/xml");
23884
+ function val(el) { return el ? (el.getAttributeNS(W, "val") || el.getAttribute("w:val") || "") : ""; }
23885
+
23886
+ // abstractNumId -> { ilvl -> numFmt }
23887
+ var abstracts = {};
23888
+ var aNums = d.getElementsByTagNameNS(W, "abstractNum");
23889
+ for (var i = 0; i < aNums.length; i++) {
23890
+ var aid = aNums[i].getAttributeNS(W, "abstractNumId") || aNums[i].getAttribute("w:abstractNumId");
23891
+ var levels = {};
23892
+ var lvls = aNums[i].getElementsByTagNameNS(W, "lvl");
23893
+ for (var j = 0; j < lvls.length; j++) {
23894
+ var ilvl = lvls[j].getAttributeNS(W, "ilvl") || lvls[j].getAttribute("w:ilvl") || "0";
23895
+ var fmt = val(lvls[j].getElementsByTagNameNS(W, "numFmt")[0]);
23896
+ levels[ilvl] = fmt || "decimal";
23897
+ }
23898
+ if (aid != null) abstracts[aid] = levels;
23899
+ }
23900
+ // num -> abstractNumId
23901
+ var nums = d.getElementsByTagNameNS(W, "num");
23902
+ for (var k = 0; k < nums.length; k++) {
23903
+ var nid = nums[k].getAttributeNS(W, "numId") || nums[k].getAttribute("w:numId");
23904
+ var ref = nums[k].getElementsByTagNameNS(W, "abstractNumId")[0];
23905
+ var aref = val(ref);
23906
+ if (nid != null && abstracts[aref]) out[nid] = abstracts[aref];
23907
+ }
23908
+ } catch (e) {}
23909
+ return out;
23910
+ }
23911
+
23912
+ // ---------------------------------------------------------------------
23913
+ // OMML (Office Math Markup) -> LaTeX.
23914
+ //
23915
+ // Word does not store equations as text, MathML or an image: it stores an
23916
+ // <m:oMath> tree in its OWN namespace, interleaved between the runs of a
23917
+ // paragraph. A converter that walks w:r elements never sees it, so every
23918
+ // equation in the document disappears without a trace -- and because the
23919
+ // surrounding sentence still imports, nothing looks broken.
23920
+ //
23921
+ // The target is the editor's own inline-math format
23922
+ // (<span class="rte-math-inline" data-tex="...">), so imported equations
23923
+ // are editable and re-render through whichever renderer the host page
23924
+ // loads, exactly like one typed in the math dialog.
23925
+ //
23926
+ // This covers the constructs Word's equation editor actually produces.
23927
+ // Anything unrecognised degrades to its literal text rather than vanishing
23928
+ // -- a wrong-looking formula is recoverable, a missing one is not.
23929
+ var OMML_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math";
23930
+
23931
+ // Word writes operators as Unicode, but a TeX renderer wants command names.
23932
+ // Passing "∑" through untouched produces a glyph that KaTeX cannot size or
23933
+ // place limits on.
23934
+ var MATH_SYMBOLS = {
23935
+ "∑": "\\sum", "∏": "\\prod", "∐": "\\coprod",
23936
+ "∫": "\\int", "∬": "\\iint", "∭": "\\iiint", "∮": "\\oint",
23937
+ "⋃": "\\bigcup", "⋂": "\\bigcap",
23938
+ "±": "\\pm", "∓": "\\mp", "×": "\\times", "÷": "\\div",
23939
+ "−": "-", "≠": "\\neq", "≤": "\\leq", "≥": "\\geq",
23940
+ "≈": "\\approx", "≡": "\\equiv", "∝": "\\propto",
23941
+ "∞": "\\infty", "∂": "\\partial", "∇": "\\nabla",
23942
+ "√": "\\sqrt", "∠": "\\angle", "⋅": "\\cdot", "…": "\\dots",
23943
+ "→": "\\to", "←": "\\leftarrow", "⇒": "\\Rightarrow", "⇔": "\\Leftrightarrow",
23944
+ "∈": "\\in", "∉": "\\notin", "⊂": "\\subset", "⊆": "\\subseteq",
23945
+ "∪": "\\cup", "∩": "\\cap", "∅": "\\emptyset",
23946
+ "∀": "\\forall", "∃": "\\exists", "¬": "\\neg",
23947
+ "∧": "\\land", "∨": "\\lor", "∴": "\\therefore",
23948
+ "α": "\\alpha", "β": "\\beta", "γ": "\\gamma", "δ": "\\delta",
23949
+ "ε": "\\epsilon", "ζ": "\\zeta", "η": "\\eta", "θ": "\\theta",
23950
+ "ι": "\\iota", "κ": "\\kappa", "λ": "\\lambda", "μ": "\\mu",
23951
+ "ν": "\\nu", "ξ": "\\xi", "π": "\\pi", "ρ": "\\rho",
23952
+ "σ": "\\sigma", "τ": "\\tau", "υ": "\\upsilon", "φ": "\\varphi",
23953
+ "χ": "\\chi", "ψ": "\\psi", "ω": "\\omega",
23954
+ "Γ": "\\Gamma", "Δ": "\\Delta", "Θ": "\\Theta", "Λ": "\\Lambda",
23955
+ "Ξ": "\\Xi", "Π": "\\Pi", "Σ": "\\Sigma", "Φ": "\\Phi",
23956
+ "Ψ": "\\Psi", "Ω": "\\Omega"
23957
+ };
23958
+ // Accent character -> TeX accent command (m:acc carries the mark as a chr).
23959
+ var MATH_ACCENTS = {
23960
+ "̂": "\\hat", "̃": "\\tilde", "̄": "\\bar", "̅": "\\overline",
23961
+ "̆": "\\breve", "̇": "\\dot", "̈": "\\ddot", "̊": "\\mathring",
23962
+ "̌": "\\check", "→": "\\vec", "⃗": "\\vec", "́": "\\acute", "̀": "\\grave"
23963
+ };
23964
+
23965
+ function ommlToTex(node) {
23966
+ var M = OMML_NS;
23967
+ function attrVal(parent, ln) {
23968
+ // Properties sit in a child element whose only content is m:val.
23969
+ for (var i = 0; i < parent.childNodes.length; i++) {
23970
+ var c = parent.childNodes[i];
23971
+ if (c.nodeType === 1 && c.localName === ln) {
23972
+ return c.getAttributeNS(M, "val") || c.getAttribute("m:val") || "";
23973
+ }
23974
+ }
23975
+ return "";
23976
+ }
23977
+ function propOf(el, propName, ln) {
23978
+ var p = child(el, propName);
23979
+ return p ? attrVal(p, ln) : "";
23980
+ }
23981
+ function child(el, ln) {
23982
+ for (var i = 0; i < el.childNodes.length; i++) {
23983
+ var c = el.childNodes[i];
23984
+ if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === M) return c;
23985
+ }
23986
+ return null;
23987
+ }
23988
+ function children(el, ln) {
23989
+ var out = [];
23990
+ for (var i = 0; i < el.childNodes.length; i++) {
23991
+ var c = el.childNodes[i];
23992
+ if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === M) out.push(c);
23993
+ }
23994
+ return out;
23995
+ }
23996
+ // An argument slot: braces so multi-character content binds correctly.
23997
+ // "x+1" without them turns \frac{x+1}{2} into \fracx+12.
23998
+ function arg(el) {
23999
+ return "{" + (el ? walk(el).trim() : "") + "}";
24000
+ }
24001
+ // A slot where braces are optional. Emitting them unconditionally is
24002
+ // correct but produces "{x}^{2}" and "{{i}^{2}}" -- valid TeX that is
24003
+ // unreadable the moment the user opens the equation in the math dialog.
24004
+ // A lone letter, digit or command needs none.
24005
+ function atom(el) {
24006
+ var t = el ? walk(el).trim() : "";
24007
+ if (/^[A-Za-z0-9]$/.test(t) || /^\\[A-Za-z]+$/.test(t)) return t;
24008
+ return "{" + t + "}";
24009
+ }
24010
+ function mapText(s) {
24011
+ var out = "";
24012
+ for (var i = 0; i < s.length; i++) {
24013
+ var ch = s.charAt(i);
24014
+ var sym = MATH_SYMBOLS[ch];
24015
+ if (sym) { out += sym + " "; continue; }
24016
+ // TeX metacharacters inside literal math text.
24017
+ if ("#$%&_{}".indexOf(ch) >= 0) { out += "\\" + ch; continue; }
24018
+ if (ch === "\\") { out += "\\backslash "; continue; }
24019
+ out += ch;
24020
+ }
24021
+ return out;
24022
+ }
24023
+ function walk(el) {
24024
+ var tex = "";
24025
+ for (var i = 0; i < el.childNodes.length; i++) {
24026
+ var c = el.childNodes[i];
24027
+ if (c.nodeType !== 1) continue;
24028
+ if (c.namespaceURI !== M) { tex += mapText(c.textContent || ""); continue; }
24029
+ tex += convert(c);
24030
+ }
24031
+ return tex;
24032
+ }
24033
+ function convert(el) {
24034
+ var ln = el.localName;
24035
+ switch (ln) {
24036
+ case "oMath":
24037
+ case "oMathPara":
24038
+ case "e":
24039
+ case "num":
24040
+ case "den":
24041
+ case "sub":
24042
+ case "sup":
24043
+ case "deg":
24044
+ case "lim":
24045
+ case "fName":
24046
+ return walk(el);
24047
+ // Properties describe how to render, not what to render; emitting
24048
+ // their text would leak stray characters into the formula.
24049
+ case "rPr": case "ctrlPr": case "fPr": case "radPr": case "naryPr":
24050
+ case "dPr": case "sSupPr": case "sSubPr": case "sSubSupPr": case "accPr":
24051
+ case "barPr": case "funcPr": case "mPr": case "limLowPr": case "limUppPr":
24052
+ case "groupChrPr": case "mcPr": case "eqArrPr": case "boxPr": case "borderBoxPr":
24053
+ case "phantPr": case "sPrePr":
24054
+ return "";
24055
+ case "r":
24056
+ return mapText(el.textContent || "");
24057
+ case "t":
24058
+ return mapText(el.textContent || "");
24059
+ case "f": {
24060
+ // A "no bar" fraction is Word's binomial/stacked form.
24061
+ var type = propOf(el, "fPr", "type");
24062
+ var n = arg(child(el, "num")), d = arg(child(el, "den"));
24063
+ if (type === "noBar") return "\\binom" + n + d;
24064
+ if (type === "skw" || type === "lin") return n + "/" + d;
24065
+ return "\\frac" + n + d;
24066
+ }
24067
+ case "sSup": return atom(child(el, "e")) + "^" + arg(child(el, "sup"));
24068
+ case "sSub": return atom(child(el, "e")) + "_" + arg(child(el, "sub"));
24069
+ case "sSubSup": return atom(child(el, "e")) + "_" + arg(child(el, "sub")) + "^" + arg(child(el, "sup"));
24070
+ case "sPre": return "{}_" + arg(child(el, "sub")) + "^" + arg(child(el, "sup")) + arg(child(el, "e"));
24071
+ case "rad": {
24072
+ // degHide means a plain square root; otherwise it is \sqrt[n]{}.
24073
+ var hide = propOf(el, "radPr", "degHide");
24074
+ var deg = child(el, "deg");
24075
+ var body = arg(child(el, "e"));
24076
+ if (hide === "1" || hide === "on" || hide === "true" || !deg || !(deg.textContent || "").trim()) return "\\sqrt" + body;
24077
+ return "\\sqrt[" + walk(deg) + "]" + body;
24078
+ }
24079
+ case "nary": {
24080
+ // The operator itself is an ATTRIBUTE (m:chr), not text. Miss
24081
+ // it and every summation imports as a bare limit pair.
24082
+ var chr = propOf(el, "naryPr", "chr") || "∫";
24083
+ var op = MATH_SYMBOLS[chr] || mapText(chr);
24084
+ var subHide = propOf(el, "naryPr", "subHide");
24085
+ var supHide = propOf(el, "naryPr", "supHide");
24086
+ var s = op;
24087
+ if (subHide !== "1" && subHide !== "on" && child(el, "sub")) s += "_" + arg(child(el, "sub"));
24088
+ if (supHide !== "1" && supHide !== "on" && child(el, "sup")) s += "^" + arg(child(el, "sup"));
24089
+ // The integrand takes no braces: \sum_{i=1}^{n} i^{2}, not
24090
+ // \sum_{i=1}^{n} {i^{2}}.
24091
+ var nbody = child(el, "e");
24092
+ return s + " " + (nbody ? walk(nbody).trim() : "");
24093
+ }
24094
+ case "d": {
24095
+ // \left/\right so the fences grow with their content.
24096
+ var beg = propOf(el, "dPr", "begChr");
24097
+ var end = propOf(el, "dPr", "endChr");
24098
+ var sep = propOf(el, "dPr", "sepChr") || "|";
24099
+ if (beg === "") beg = "(";
24100
+ if (end === "") end = ")";
24101
+ function fence(ch, side) {
24102
+ if (!ch) return side === "l" ? "\\left." : "\\right.";
24103
+ if (ch === "{" || ch === "}") return "\\" + ch;
24104
+ if (ch === "|") return "|";
24105
+ if (ch === "‖") return "\\|";
24106
+ // THREE different codepoints render as an angle
24107
+ // bracket and are indistinguishable on screen: the
24108
+ // mathematical pair (U+27E8/9), the CJK pair
24109
+ // (U+3008/9) and the deprecated pair (U+2329/A) that
24110
+ // several generators still emit. Comparing against a
24111
+ // literal in the source would silently match only one
24112
+ // of them, so compare by codepoint.
24113
+ var cp = ch.charCodeAt(0);
24114
+ if (cp === 0x27E8 || cp === 0x3008 || cp === 0x2329) return "\\langle";
24115
+ if (cp === 0x27E9 || cp === 0x3009 || cp === 0x232A) return "\\rangle";
24116
+ if (ch === "⌊") return "\\lfloor";
24117
+ if (ch === "⌋") return "\\rfloor";
24118
+ if (ch === "⌈") return "\\lceil";
24119
+ if (ch === "⌉") return "\\rceil";
24120
+ return ch;
24121
+ }
24122
+ var parts = children(el, "e");
24123
+ var inner = [];
24124
+ for (var q = 0; q < parts.length; q++) inner.push(walk(parts[q]));
24125
+ return "\\left" + fence(beg, "l") + inner.join(" " + fence(sep, "m") + " ") + "\\right" + fence(end, "r");
24126
+ }
24127
+ case "func": {
24128
+ // Word stores "sin"/"log" as ordinary text; without
24129
+ // \operatorname TeX renders it as s*i*n in italics.
24130
+ var name = walk(child(el, "fName")).trim();
24131
+ 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)$/;
24132
+ var head = known.test(name) ? "\\" + name : (name ? "\\operatorname{" + name + "}" : "");
24133
+ return head + arg(child(el, "e"));
24134
+ }
24135
+ case "limLow": {
24136
+ var base = walk(child(el, "e")).trim();
24137
+ if (base === "lim" || base === "\\lim") return "\\lim_" + arg(child(el, "lim"));
24138
+ return "\\underset" + arg(child(el, "lim")) + "{" + base + "}";
24139
+ }
24140
+ case "limUpp":
24141
+ return "\\overset" + arg(child(el, "lim")) + arg(child(el, "e"));
24142
+ case "acc": {
24143
+ var mark = propOf(el, "accPr", "chr") || "̂";
24144
+ var cmd = MATH_ACCENTS[mark] || "\\hat";
24145
+ return cmd + arg(child(el, "e"));
24146
+ }
24147
+ case "bar":
24148
+ return (propOf(el, "barPr", "pos") === "bot" ? "\\underline" : "\\overline") + arg(child(el, "e"));
24149
+ case "groupChr": {
24150
+ var g = propOf(el, "groupChrPr", "chr");
24151
+ if (g === "⏟") return "\\underbrace" + arg(child(el, "e"));
24152
+ return "\\overbrace" + arg(child(el, "e"));
24153
+ }
24154
+ case "borderBox":
24155
+ return "\\boxed" + arg(child(el, "e"));
24156
+ case "box":
24157
+ case "phant":
24158
+ return walk(el);
24159
+ case "m": {
24160
+ // Matrix: rows are m:mr, cells are m:e inside them.
24161
+ var rows = children(el, "mr"), lines = [];
24162
+ for (var r = 0; r < rows.length; r++) {
24163
+ var cells = children(rows[r], "e"), cs = [];
24164
+ for (var cix = 0; cix < cells.length; cix++) cs.push(walk(cells[cix]));
24165
+ lines.push(cs.join(" & "));
24166
+ }
24167
+ return "\\begin{matrix}" + lines.join(" \\\\ ") + "\\end{matrix}";
24168
+ }
24169
+ case "eqArr": {
24170
+ var eqs = children(el, "e"), rowsOut = [];
24171
+ for (var e2 = 0; e2 < eqs.length; e2++) rowsOut.push(walk(eqs[e2]));
24172
+ return "\\begin{aligned}" + rowsOut.join(" \\\\ ") + "\\end{aligned}";
24173
+ }
24174
+ default:
24175
+ // Unknown construct: keep the content rather than losing it.
24176
+ return walk(el);
24177
+ }
24178
+ }
24179
+ try {
24180
+ return convert(node).replace(/\s+/g, " ").trim();
24181
+ } catch (e) { return (node.textContent || "").trim(); }
24182
+ }
24183
+
24184
+ function docxXmlToHtml(xml, ctx) {
24185
+ ctx = ctx || { rels: {}, numbering: {}, media: {} };
24186
+ if (!ctx.diagrams) ctx.diagrams = {};
24187
+ var doc = new DOMParser().parseFromString(xml, "application/xml");
24188
+ var W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
24189
+ var R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
24190
+
24191
+ function relTarget(el) {
24192
+ if (!el) return null;
24193
+ var id = el.getAttributeNS(R, "id") || el.getAttribute("r:id") ||
24194
+ el.getAttributeNS(R, "embed") || el.getAttribute("r:embed");
24195
+ if (!id) return null;
24196
+ return ctx.rels[id] || null;
24197
+ }
24198
+
24199
+ // <w:drawing>/<w:pict> -> <img> from word/media, inlined as a data URI.
24200
+ function imageHtml(node) {
24201
+ var blips = node.getElementsByTagName("*");
24202
+ for (var i = 0; i < blips.length; i++) {
24203
+ if (blips[i].localName !== "blip" && blips[i].localName !== "imagedata") continue;
24204
+ var rel = relTarget(blips[i]);
24205
+ if (!rel) continue;
24206
+ // Targets are relative to word/: "media/image1.png".
24207
+ var path = rel.target.replace(/^\.\//, "");
24208
+ var key = path.indexOf("word/") === 0 ? path : "word/" + path;
24209
+ var uri = ctx.media[key];
24210
+ if (uri) return '<img src="' + uri + '" alt="">';
24211
+ if (rel.external) return '<img src="' + esc(rel.target) + '" alt="">';
24212
+ // A vector metafile (WMF/EMF) has no browser-renderable form, so
24213
+ // there is no data URI for it. Dropping it silently leaves the
24214
+ // reader with no idea a figure was ever there -- and no filename
24215
+ // to go looking for in the original.
24216
+ if (/\.(wmf|emf|emz|wmz)$/i.test(path)) {
24217
+ var fileName = path.split("/").pop();
24218
+ queueBlock('<p class="rte-imported-unsupported" data-source="' + esc(fileName) +
24219
+ '"><em>[Unsupported image: ' + esc(fileName) +
24220
+ " — Windows metafiles have no browser-renderable form]</em></p>");
24221
+ return "";
24222
+ }
24223
+ }
24224
+ queueBlock(diagramHtml(node));
24225
+ return "";
24226
+ }
24227
+
24228
+ // SmartArt: the drawing references word/diagrams/dataN.xml through
24229
+ // r:dm. Rendered as a labelled list -- the layout cannot be reproduced,
24230
+ // but every word of it survives and stays editable.
24231
+ function diagramHtml(node) {
24232
+ var all = node.getElementsByTagName("*");
24233
+ for (var i = 0; i < all.length; i++) {
24234
+ if (all[i].localName !== "relIds") continue;
24235
+ var id = all[i].getAttributeNS(R, "dm") || all[i].getAttribute("r:dm");
24236
+ var rel = id ? ctx.rels[id] : null;
24237
+ if (!rel) continue;
24238
+ var path = rel.target.replace(/^\.\//, "");
24239
+ var key = path.indexOf("word/") === 0 ? path : "word/" + path;
24240
+ var nodes = ctx.diagrams[key];
24241
+ if (!nodes || !nodes.length) continue;
24242
+ var items = "";
24243
+ for (var n = 0; n < nodes.length; n++) items += "<li>" + esc(nodes[n]) + "</li>";
24244
+ return '<div class="rte-imported-smartart" style="' + SMARTART_STYLE + '"><ul>' + items + "</ul></div>";
24245
+ }
24246
+ return "";
24247
+ }
24248
+ function els(parent, ln) { return parent.getElementsByTagNameNS(W, ln); }
24249
+ function firstChildEl(parent, ln) {
24250
+ for (var i = 0; i < parent.childNodes.length; i++) {
24251
+ var c = parent.childNodes[i];
24252
+ if (c.nodeType === 1 && c.localName === ln && c.namespaceURI === W) return c;
24253
+ }
24254
+ return null;
24255
+ }
24256
+ function runText(r) {
24257
+ var t = "", kids = r.childNodes;
24258
+ for (var i = 0; i < kids.length; i++) {
24259
+ var c = kids[i];
24260
+ if (c.nodeType !== 1) continue;
24261
+ // Deleted text is stored as w:delText, never w:t — read only w:t
24262
+ // and every tracked deletion imports as an empty run.
24263
+ if (c.localName === "t" || c.localName === "delText") t += c.textContent || "";
24264
+ else if (c.localName === "tab") t += "\t";
24265
+ else if (c.localName === "br" || c.localName === "cr") t += "\n";
24266
+ }
24267
+ return t;
24268
+ }
24269
+ function runHtml(r) {
24270
+ // A run can carry a picture instead of text; without this every
24271
+ // image in the document is silently dropped on import.
24272
+ var pics = "";
24273
+ for (var d = 0; d < r.childNodes.length; d++) {
24274
+ var ch = r.childNodes[d];
24275
+ if (ch.nodeType === 1 && (ch.localName === "drawing" || ch.localName === "pict")) {
24276
+ pics += imageHtml(ch);
24277
+ }
24278
+ // A run holding a footnote reference has no text of its own.
24279
+ if (ch.nodeType === 1 && ch.localName === "footnoteReference") {
24280
+ var fid = ch.getAttributeNS(W, "id") || ch.getAttribute("w:id");
24281
+ var body = fid != null ? ctx.footnotes[String(fid)] : null;
24282
+ if (body) pics += footnoteMarker(body);
24283
+ }
24284
+ // Endnotes render through the same notes machinery: Word puts
24285
+ // both at the end of the document, and the distinction is not
24286
+ // one HTML has a separate representation for.
24287
+ if (ch.nodeType === 1 && ch.localName === "endnoteReference") {
24288
+ var eid = ch.getAttributeNS(W, "id") || ch.getAttribute("w:id");
24289
+ var ebody = eid != null ? ctx.endnotes[String(eid)] : null;
24290
+ if (ebody) pics += footnoteMarker(ebody);
24291
+ }
24292
+ // TEXT BOXES. Their content is nested several levels below the
24293
+ // run (w:pict > v:shape > v:textbox > w:txbxContent, or the
24294
+ // DrawingML mc:AlternateContent equivalent), so a reader that
24295
+ // only looks at a run's DIRECT children drops every word inside
24296
+ // them -- silently, with no sign anything is missing.
24297
+ if (ch.nodeType === 1 && (ch.localName === "pict" || ch.localName === "drawing" ||
24298
+ ch.localName === "object" || ch.localName === "AlternateContent")) {
24299
+ collectTextBoxes(ch);
24300
+ }
24301
+ }
24302
+ var txt = esc(runText(r));
24303
+ if (!txt) return pics;
24304
+ txt = txt.replace(/\n/g, "<br>");
24305
+ var rpr = firstChildEl(r, "rPr");
24306
+ if (rpr) {
24307
+ if (firstChildEl(rpr, "b")) txt = "<strong>" + txt + "</strong>";
24308
+ if (firstChildEl(rpr, "i")) txt = "<em>" + txt + "</em>";
24309
+ if (firstChildEl(rpr, "u")) txt = "<u>" + txt + "</u>";
24310
+ if (firstChildEl(rpr, "strike")) txt = "<s>" + txt + "</s>";
24311
+ var vert = firstChildEl(rpr, "vertAlign");
24312
+ if (vert) {
24313
+ var va = vert.getAttributeNS(W, "val") || vert.getAttribute("w:val") || "";
24314
+ if (va === "superscript") txt = "<sup>" + txt + "</sup>";
24315
+ else if (va === "subscript") txt = "<sub>" + txt + "</sub>";
24316
+ }
24317
+ // Character colour and size. w:sz is in HALF-points, so 24 = 12pt;
24318
+ // forgetting the halving makes every imported document twice the
24319
+ // size it should be.
24320
+ var css = "";
24321
+ var col = firstChildEl(rpr, "color");
24322
+ if (col) {
24323
+ var cv = col.getAttributeNS(W, "val") || col.getAttribute("w:val") || "";
24324
+ if (cv && /^[0-9A-Fa-f]{6}$/.test(cv)) css += "color:#" + cv + ";";
24325
+ }
24326
+ var hl = firstChildEl(rpr, "highlight");
24327
+ if (hl) {
24328
+ var hv = hl.getAttributeNS(W, "val") || hl.getAttribute("w:val") || "";
24329
+ if (hv && hv !== "none") css += "background-color:" + hv + ";";
24330
+ }
24331
+ var sz = firstChildEl(rpr, "sz");
24332
+ if (sz) {
24333
+ var sv = parseInt(sz.getAttributeNS(W, "val") || sz.getAttribute("w:val") || "", 10);
24334
+ if (sv > 0) css += "font-size:" + (sv / 2) + "pt;";
24335
+ }
24336
+ if (css) txt = '<span style="' + css + '">' + txt + "</span>";
24337
+ }
24338
+ return pics + txt;
24339
+ }
24340
+
24341
+ // Block-level content discovered while rendering a paragraph: text
24342
+ // boxes, SmartArt outlines, and placeholders for images the browser
24343
+ // cannot render. None of them may be emitted inline inside the
24344
+ // paragraph that referenced them -- a <div> inside a <p> is invalid and
24345
+ // browsers silently close the paragraph around it -- so each is
24346
+ // buffered here, already wrapped, and flushed straight after.
24347
+ var pendingBlocks = [];
24348
+ function queueBlock(html) {
24349
+ // Dedup on the whole string: an mc:AlternateContent block carries
24350
+ // the same graphic twice, once per rendering flavour.
24351
+ if (html && pendingBlocks.indexOf(html) < 0) pendingBlocks.push(html);
24352
+ }
24353
+ function collectTextBoxes(node) {
24354
+ var boxes = node.getElementsByTagName("*");
24355
+ for (var i = 0; i < boxes.length; i++) {
24356
+ if (boxes[i].localName !== "txbxContent") continue;
24357
+ var inner = "";
24358
+ var kids = boxes[i].childNodes;
24359
+ for (var k = 0; k < kids.length; k++) {
24360
+ var c = kids[k];
24361
+ if (c.nodeType !== 1) continue;
24362
+ if (c.localName === "p") {
24363
+ var t = paraInner(c);
24364
+ if (t) inner += "<p>" + t + "</p>";
24365
+ } else if (c.localName === "tbl") {
24366
+ inner += tableHtml(c);
24367
+ }
24368
+ }
24369
+ if (inner) queueBlock('<div class="rte-imported-textbox" style="' + TEXTBOX_STYLE + '">' + inner + "</div>");
24370
+ }
24371
+ }
24372
+
24373
+ function flushBlocks() {
24374
+ if (!pendingBlocks.length) return "";
24375
+ var html = pendingBlocks.join("");
24376
+ pendingBlocks = [];
24377
+ return html;
24378
+ }
24379
+
24380
+ // Emit a marker in the shape footnotes.js already understands, and stash
24381
+ // the body so the notes section can be built at the end. The plugin
24382
+ // renumbers on load, so the number written here is provisional.
24383
+ var importedNotes = [];
24384
+ // OMML -> the editor's own inline-math span, so an imported equation is
24385
+ // editable in the math dialog and renders through whatever renderer the
24386
+ // host page loads. The visible text is the TeX itself: that is what a
24387
+ // page with no renderer shows, and it is readable rather than blank.
24388
+ function mathHtml(node) {
24389
+ var tex = ommlToTex(node);
24390
+ if (!tex) return "";
24391
+ var display = node.localName === "oMathPara";
24392
+ return '<span class="rte-math-inline' + (display ? " rte-math-display" : "") +
24393
+ '" data-tex="' + esc(tex) + '">' + esc(tex) + "</span>";
24394
+ }
24395
+
24396
+ function footnoteMarker(text) {
24397
+ var id = "fnimp" + (importedNotes.length + 1);
24398
+ importedNotes.push({ id: id, text: text });
24399
+ var n = importedNotes.length;
24400
+ return '<sup class="rte-fn-ref" data-fn-id="' + id + '" data-fn-number="' + n +
24401
+ '" id="fnref-' + id + '" contenteditable="false">' + n + "</sup>";
24402
+ }
24403
+
24404
+ function footnotesSection() {
24405
+ if (!importedNotes.length) return "";
24406
+ var items = "";
24407
+ for (var i = 0; i < importedNotes.length; i++) {
24408
+ var f = importedNotes[i];
24409
+ items += '<li class="rte-fn-note" data-fn-id="' + f.id + '" data-fn-number="' + (i + 1) +
24410
+ '" id="fn-' + f.id + '">' + esc(f.text) +
24411
+ '<a class="rte-fn-back" data-fn-id="' + f.id + '" href="#fnref-' + f.id +
24412
+ '" contenteditable="false" title="Back to reference">↩</a></li>';
24413
+ }
24414
+ return '<section class="rte-footnotes"><h2 class="rte-fn-title">Footnotes</h2>' +
24415
+ '<ol class="rte-fn-list" data-numbering="decimal">' + items + "</ol></section>";
24416
+ }
24417
+ // Runs inside a w:ins / w:del wrapper, rendered with the review markup
24418
+ // that the DOCX *export* already round-trips (<ins data-author>/<del>).
24419
+ function revisionHtml(node, tag) {
24420
+ var inner = "";
24421
+ for (var i = 0; i < node.childNodes.length; i++) {
24422
+ var c = node.childNodes[i];
24423
+ if (c.nodeType !== 1) continue;
24424
+ if (c.localName === "r") inner += runHtml(c);
24425
+ else if (c.localName === "ins") inner += revisionHtml(c, "ins");
24426
+ else if (c.localName === "del") inner += revisionHtml(c, "del");
24427
+ }
24428
+ if (!inner) return "";
24429
+ var author = node.getAttributeNS(W, "author") || node.getAttribute("w:author") || "";
24430
+ var date = node.getAttributeNS(W, "date") || node.getAttribute("w:date") || "";
24431
+ return "<" + tag +
24432
+ (author ? ' data-author="' + esc(author) + '"' : "") +
24433
+ (date ? ' data-date="' + esc(date) + '"' : "") +
24434
+ ">" + inner + "</" + tag + ">";
24435
+ }
24436
+
24437
+ function paraInner(p) {
24438
+ var h = "", kids = p.childNodes;
24439
+ // Word marks a commented RANGE with start/end markers around the runs,
24440
+ // so the wrapper has to be opened and closed as we walk, not derived
24441
+ // from any single element.
24442
+ var openComments = [];
24443
+ function commentOpen(id) {
24444
+ var c = ctx.comments[String(id)];
24445
+ if (!c) return "";
24446
+ openComments.push(id);
24447
+ return '<span data-comment="' + esc(c.text) + '"' +
24448
+ (c.author ? ' data-comment-author="' + esc(c.author) + '"' : "") + ">";
24449
+ }
24450
+ for (var i = 0; i < kids.length; i++) {
24451
+ var c = kids[i];
24452
+ if (c.nodeType === 1 && c.localName === "commentRangeStart") {
24453
+ h += commentOpen(c.getAttributeNS(W, "id") || c.getAttribute("w:id"));
24454
+ continue;
24455
+ }
24456
+ if (c.nodeType === 1 && c.localName === "commentRangeEnd") {
24457
+ if (openComments.length) { openComments.pop(); h += "</span>"; }
24458
+ continue;
24459
+ }
24460
+ if (c.nodeType === 1 && (c.localName === "ins" || c.localName === "del") && c.namespaceURI === W) {
24461
+ h += revisionHtml(c, c.localName);
24462
+ continue;
24463
+ }
24464
+ // EQUATIONS. OMML is a sibling of the runs, in its own
24465
+ // namespace -- a walker that only recognises w:r skips it and
24466
+ // the equation is gone. m:oMathPara is the display (own-line)
24467
+ // wrapper around one or more m:oMath.
24468
+ if (c.nodeType === 1 && c.namespaceURI === OMML_NS &&
24469
+ (c.localName === "oMath" || c.localName === "oMathPara")) {
24470
+ h += mathHtml(c);
24471
+ continue;
24472
+ }
24473
+ if (c.nodeType === 1 && c.localName === "r" && c.namespaceURI === W) h += runHtml(c);
24474
+ else if (c.nodeType === 1 && c.localName === "hyperlink") {
24475
+ var inner = "";
24476
+ for (var j = 0; j < c.childNodes.length; j++) if (c.childNodes[j].localName === "r") inner += runHtml(c.childNodes[j]);
24477
+ // Resolve r:id through the rels part. Previously the run text
24478
+ // was emitted bare and the URL was thrown away, so every link
24479
+ // in an imported document became plain text.
24480
+ var rel = relTarget(c);
24481
+ var anchor = c.getAttributeNS(W, "anchor") || c.getAttribute("w:anchor");
24482
+ var href = rel ? rel.target : (anchor ? "#" + anchor : null);
24483
+ h += href ? ('<a href="' + esc(href) + '">' + (inner || esc(href)) + "</a>") : inner;
24484
+ }
24485
+ }
24486
+ // A comment range left open at the end of the paragraph would leak an
24487
+ // unbalanced <span> into the document.
24488
+ while (openComments.length) { openComments.pop(); h += "</span>"; }
24489
+ return h;
24490
+ }
24491
+ function paraStyle(p) {
24492
+ var ppr = firstChildEl(p, "pPr"); if (!ppr) return {};
24493
+ var info = {};
24494
+ var ps = firstChildEl(ppr, "pStyle");
24495
+ if (ps) {
24496
+ var v = ps.getAttributeNS(W, "val") || ps.getAttribute("w:val") || "";
24497
+ var m = /heading(\d)/i.exec(v);
24498
+ if (m) info.heading = Math.min(6, parseInt(m[1], 10));
24499
+ // docxexport.js writes blockquotes as the "Quote" paragraph style;
24500
+ // without this they came back as plain <p> and the round-trip lost
24501
+ // every blockquote. Match Quote / IntenseQuote / BlockQuote.
24502
+ else if (/^(intense)?quote$|blockquote/i.test(v.replace(/\s+/g, ""))) info.quote = true;
24503
+ }
24504
+ var numPr = firstChildEl(ppr, "numPr");
24505
+ if (numPr) {
24506
+ info.list = true;
24507
+ var ilvl = firstChildEl(numPr, "ilvl");
24508
+ var numId = firstChildEl(numPr, "numId");
24509
+ info.level = parseInt(ilvl ? (ilvl.getAttributeNS(W, "val") || ilvl.getAttribute("w:val") || "0") : "0", 10) || 0;
24510
+ var nid = numId ? (numId.getAttributeNS(W, "val") || numId.getAttribute("w:val") || "") : "";
24511
+ var levels = ctx.numbering[nid];
24512
+ var fmt = levels ? levels[String(info.level)] : null;
24513
+ // Word marks bullets with numFmt="bullet"; anything else is an
24514
+ // ordered list. Defaulting to bullet when numbering.xml is absent
24515
+ // matches the old behaviour rather than inventing <ol>s.
24516
+ info.ordered = !!fmt && fmt !== "bullet" && fmt !== "none";
24517
+ }
24518
+ var jc = firstChildEl(ppr, "jc");
24519
+ if (jc) {
24520
+ var a = jc.getAttributeNS(W, "val") || jc.getAttribute("w:val") || "";
24521
+ if (a === "center" || a === "right" || a === "both") {
24522
+ info.align = (a === "both") ? "justify" : a;
24523
+ }
24524
+ }
24525
+ return info;
24526
+ }
24527
+ var body = els(doc, "body")[0];
24528
+ if (!body) return "";
24529
+ var out = [];
24530
+ // Stack of open lists so w:ilvl produces real nesting instead of a flat
24531
+ // run of <li>s. liOpen tracks whether an <li> is still open at each
24532
+ // depth, because a nested list must live INSIDE its parent <li> --
24533
+ // <ol><li>a</li><ol>...</ol></ol> is invalid HTML, and shipping invalid
24534
+ // markup out of an importer undermines the whole clean-output promise.
24535
+ var listStack = [];
24536
+ var liOpen = [];
24537
+ function closeListsTo(depth) {
24538
+ while (listStack.length > depth) {
24539
+ var d = listStack.length - 1;
24540
+ if (liOpen[d]) { out.push("</li>"); liOpen[d] = false; }
24541
+ out.push("</" + listStack.pop() + ">");
24542
+ liOpen.pop();
24543
+ }
24544
+ }
24545
+ for (var i = 0; i < body.childNodes.length; i++) {
24546
+ var node = body.childNodes[i];
24547
+ if (node.nodeType !== 1) continue;
24548
+ // A display equation can sit at body level rather than inside a
24549
+ // w:p, so the "wordprocessingml namespace only" filter below would
24550
+ // drop it.
24551
+ if (node.namespaceURI === OMML_NS &&
24552
+ (node.localName === "oMathPara" || node.localName === "oMath")) {
24553
+ closeListsTo(0);
24554
+ var mh = mathHtml(node);
24555
+ if (mh) out.push("<p>" + mh + "</p>");
24556
+ continue;
24557
+ }
24558
+ if (node.namespaceURI !== W) continue;
24559
+ if (node.localName === "p") {
24560
+ var st = paraStyle(node);
24561
+ var inner = paraInner(node);
24562
+ if (st.list) {
24563
+ var want = (st.level || 0) + 1;
24564
+ var tag = st.ordered ? "ol" : "ul";
24565
+ // Come back up to this depth FIRST. Checking the type before
24566
+ // this ran compared against the wrong level, so a bullet
24567
+ // following a deeper numbered item silently joined the <ol>.
24568
+ if (listStack.length > want) closeListsTo(want);
24569
+ // Now at this depth: a different list type means a new list.
24570
+ if (listStack.length === want && listStack[want - 1] !== tag) closeListsTo(want - 1);
24571
+ // Going deeper leaves the parent <li> open so the sublist
24572
+ // nests inside it.
24573
+ while (listStack.length < want) {
24574
+ out.push("<" + tag + ">");
24575
+ listStack.push(tag);
24576
+ liOpen.push(false);
24577
+ }
24578
+ if (liOpen[want - 1]) { out.push("</li>"); liOpen[want - 1] = false; }
24579
+ out.push("<li>" + (inner || ""));
24580
+ liOpen[want - 1] = true;
24581
+ continue;
24582
+ }
24583
+ closeListsTo(0);
24584
+ if (st.heading) out.push("<h" + st.heading + ">" + (inner || "") + "</h" + st.heading + ">");
24585
+ else if (st.quote) out.push("<blockquote>" + (inner || "<br>") + "</blockquote>");
24586
+ else {
24587
+ var style = st.align ? ' style="text-align:' + st.align + '"' : "";
24588
+ // A paragraph that contained ONLY a text box has no text of
24589
+ // its own; emitting an empty <p> before the box adds a blank
24590
+ // line that was never in the document.
24591
+ var boxes = flushBlocks();
24592
+ if (inner || !boxes) out.push("<p" + style + ">" + (inner || "<br>") + "</p>");
24593
+ if (boxes) out.push(boxes);
24594
+ }
24595
+ } else if (node.localName === "tbl") {
24596
+ closeListsTo(0);
24597
+ out.push(tableHtml(node));
24598
+ }
24599
+ }
24600
+ closeListsTo(0);
24601
+ // A text box hanging off the last paragraph, or off a heading/list item,
24602
+ // still has to land somewhere.
24603
+ var trailing = flushBlocks();
24604
+ if (trailing) out.push(trailing);
24605
+ // The notes section goes last, after every marker has been collected.
24606
+ var notes = footnotesSection();
24607
+ if (notes) out.push(notes);
24608
+ return out.join("\n");
24609
+
24610
+ function tableHtml(tbl) {
24611
+ var rows = [], kids = tbl.childNodes;
24612
+ for (var i = 0; i < kids.length; i++) {
24613
+ var tr = kids[i];
24614
+ if (tr.nodeType !== 1 || tr.localName !== "tr") continue;
24615
+ // docxexport.js marks a header row with <w:trPr><w:tblHeader/>.
24616
+ // Without honouring it, every cell imported as <td> and the table
24617
+ // lost its header semantics on round-trip - which then fails the
24618
+ // accessibility checker's table-missing-header rule.
24619
+ var trPr = firstChildEl(tr, "trPr");
24620
+ var isHeaderRow = !!(trPr && firstChildEl(trPr, "tblHeader"));
24621
+ var cellTag = isHeaderRow ? "th" : "td";
24622
+ var cells = [];
24623
+ for (var j = 0; j < tr.childNodes.length; j++) {
24624
+ var tc = tr.childNodes[j];
24625
+ if (tc.nodeType !== 1 || tc.localName !== "tc") continue;
24626
+ var cellHtml = "";
24627
+ for (var k = 0; k < tc.childNodes.length; k++) {
24628
+ if (tc.childNodes[k].localName === "p") cellHtml += "<p>" + (paraInner(tc.childNodes[k]) || "<br>") + "</p>";
24629
+ }
24630
+ cells.push("<" + cellTag + ">" + cellHtml + "</" + cellTag + ">");
24631
+ }
24632
+ rows.push("<tr>" + cells.join("") + "</tr>");
24633
+ }
24634
+ return "<table>" + rows.join("") + "</table>";
24635
+ }
24636
+ }
24637
+
24638
+ function notify(msg) {
24639
+ try {
24640
+ if (editor && typeof editor.createDialog === "function") {
24641
+ var d = editor.createDialog((editor.getLangText && editor.getLangText("importtitle")) || "Import document", "rte-dialog-import");
24642
+ var w = d.ownerDocument.createElement("div");
24643
+ w.style.cssText = "padding:16px;max-width:420px;font:13px -apple-system,Segoe UI,sans-serif;line-height:1.5";
24644
+ w.textContent = msg;
24645
+ d.appendChild(w);
24646
+ return;
24647
+ }
24648
+ } catch (e) {}
24649
+ if (window.console) console.warn("documentimport:", msg);
24650
+ }
24651
+ }
24561
24652
 
24562
24653
  if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
24563
24654
 
@@ -31274,6 +31365,390 @@ function RTE_Plugin_KeyboardA11y() {
31274
31365
 
31275
31366
  if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
31276
31367
 
31368
+ // 2026-08-30 Layout tables — say whether a table carries data or only geometry.
31369
+ //
31370
+ // Found by re-sweeping CKEditor 5's feature pages (they ship `layout-tables`);
31371
+ // TinyMCE has no equivalent. Every other editor treats <table> as one thing.
31372
+ // It is two:
31373
+ //
31374
+ // * a DATA table states relationships — this cell is described by that header —
31375
+ // and a screen reader announces them on every cell;
31376
+ // * a LAYOUT table states nothing. Newsletters, signatures and pre-CSS page
31377
+ // furniture use it purely for geometry, and the correct markup is
31378
+ // role="presentation", which tells assistive technology to read the cells as
31379
+ // plain flow content instead of narrating a fake grid.
31380
+ //
31381
+ // Using one where the other is meant is a real WCAG 1.3.1 failure, not a style
31382
+ // preference: a layout table with <th>/scope/<caption> makes a screen reader
31383
+ // announce column headers that describe nothing.
31384
+ //
31385
+ // It also fixes a false alarm we were shipping. `accessibilitychecker.js` raised
31386
+ // `table-missing-header` on EVERY table without a <th>, so an email layout — which
31387
+ // must not have one — produced a permanent error the author could never clear. A
31388
+ // checker that cannot be satisfied teaches people to ignore it, so the rule now
31389
+ // skips presentation tables and instead flags the genuine fault: a layout table
31390
+ // that still carries data-table semantics.
31391
+ //
31392
+ // Design notes:
31393
+ // - role="presentation" is the whole marker. No vendor class is added, so the
31394
+ // exported HTML gains nothing this editor has to own; `role=none` is the ARIA
31395
+ // synonym and is accepted on read, though we always write "presentation"
31396
+ // because Outlook-era mail clients are more likely to have heard of it.
31397
+ // - The dashed outline is injected into the EDITING document only (a <style> in
31398
+ // the iframe head, like formattingmarks.js), never into the content. A layout
31399
+ // table is invisible by design; without an affordance the author cannot see
31400
+ // the thing they are editing.
31401
+ // - Tables nest. Marking the outer table layout must not strip the headers of a
31402
+ // data table sitting inside one of its cells, so every walk is restricted to
31403
+ // cells whose nearest ancestor table is this one.
31404
+ // - Converting <th> to <td> loses information. Which cells were headers is
31405
+ // remembered per table in a WeakMap so an accidental toggle is reversible in
31406
+ // the session, and that memory is deliberately NOT written into the markup —
31407
+ // restoring it is a convenience, not a document property. Reloaded documents
31408
+ // therefore restore nothing; `promoteFirstRow` is offered for that case.
31409
+ RTE_DefaultConfig.plugin_layouttable = RTE_Plugin_LayoutTable;
31410
+
31411
+ // Show a dashed outline around layout tables while editing.
31412
+ if (typeof RTE_DefaultConfig.layoutTableOutline === "undefined") RTE_DefaultConfig.layoutTableOutline = true;
31413
+
31414
+ function RTE_Plugin_LayoutTable() {
31415
+ var obj = this;
31416
+ var config, editor;
31417
+ var headerMemory = (typeof WeakMap === "function") ? new WeakMap() : null;
31418
+
31419
+ obj.PluginName = "LayoutTable";
31420
+
31421
+ obj.InitConfig = function (argconfig) { config = argconfig; };
31422
+
31423
+ obj.InitEditor = function (argeditor) {
31424
+ editor = argeditor;
31425
+
31426
+ editor.attachEvent("exec_command_tablelayoutmode", function (state) {
31427
+ state.returnValue = true;
31428
+ obj.SetMode(tableAtCaret(), "layout");
31429
+ });
31430
+ editor.attachEvent("exec_command_tabledatamode", function (state) {
31431
+ state.returnValue = true;
31432
+ obj.SetMode(tableAtCaret(), "data");
31433
+ });
31434
+ editor.attachEvent("exec_command_toggletablelayout", function (state) {
31435
+ state.returnValue = true;
31436
+ obj.Toggle();
31437
+ });
31438
+ editor.attachEvent("exec_command_insertlayouttable", function (state) {
31439
+ state.returnValue = true;
31440
+ obj.InsertLayoutTable();
31441
+ });
31442
+
31443
+ // Public API.
31444
+ editor.setTableLayoutMode = function (mode, table, options) {
31445
+ return obj.SetMode(table || tableAtCaret(), mode, options);
31446
+ };
31447
+ editor.getTableLayoutMode = function (table) {
31448
+ var t = table || tableAtCaret();
31449
+ return t ? (isLayout(t) ? "layout" : "data") : null;
31450
+ };
31451
+ editor.isLayoutTable = function (table) { return isLayout(table || tableAtCaret()); };
31452
+ editor.toggleTableLayoutMode = function () { return obj.Toggle(); };
31453
+ editor.insertLayoutTable = function (rows, cols, options) {
31454
+ return obj.InsertLayoutTable(rows, cols, options);
31455
+ };
31456
+ editor.listLayoutTables = function () {
31457
+ var ed = getEditable();
31458
+ if (!ed) return [];
31459
+ var out = [], all = ed.getElementsByTagName("table");
31460
+ for (var i = 0; i < all.length; i++) if (isLayout(all[i])) out.push(all[i]);
31461
+ return out;
31462
+ };
31463
+ editor.showLayoutTableOutlines = function (on) {
31464
+ outlineOn = (on !== false);
31465
+ injectStyles(editableDoc());
31466
+ return outlineOn;
31467
+ };
31468
+
31469
+ try {
31470
+ if (config && typeof config.layoutTableOutline !== "undefined") outlineOn = !!config.layoutTableOutline;
31471
+ } catch (e) {}
31472
+ injectStyles(editableDoc());
31473
+ try {
31474
+ editor.attachEvent("editor_ready", function () { injectStyles(editableDoc()); });
31475
+ editor.attachEvent("update_design", function () { injectStyles(editableDoc()); });
31476
+ } catch (e) {}
31477
+ };
31478
+
31479
+ var outlineOn = true;
31480
+
31481
+ // ---------------------------------------------------------------- commands
31482
+
31483
+ obj.SetMode = function (table, mode, options) {
31484
+ if (!table) return null;
31485
+ if (String(mode).toLowerCase() === "layout") return toLayout(table);
31486
+ return toData(table, options);
31487
+ };
31488
+
31489
+ obj.Toggle = function () {
31490
+ var t = tableAtCaret();
31491
+ if (!t) return null;
31492
+ return isLayout(t) ? toData(t) : toLayout(t);
31493
+ };
31494
+
31495
+ obj.InsertLayoutTable = function (rows, cols, options) {
31496
+ var doc = editableDoc();
31497
+ if (!doc) return null;
31498
+ rows = Math.max(1, parseInt(rows, 10) || 2);
31499
+ cols = Math.max(1, parseInt(cols, 10) || 2);
31500
+ options = options || {};
31501
+
31502
+ var table = doc.createElement("table");
31503
+ table.setAttribute("role", "presentation");
31504
+ // The three attributes every email layout table needs; without
31505
+ // border-collapse the cells show a 2px gap in Outlook.
31506
+ table.setAttribute("cellpadding", "0");
31507
+ table.setAttribute("cellspacing", "0");
31508
+ table.style.width = options.width || "100%";
31509
+ table.style.borderCollapse = "collapse";
31510
+
31511
+ var tbody = doc.createElement("tbody");
31512
+ for (var r = 0; r < rows; r++) {
31513
+ var tr = doc.createElement("tr");
31514
+ for (var c = 0; c < cols; c++) {
31515
+ var td = doc.createElement("td");
31516
+ td.style.verticalAlign = "top";
31517
+ td.appendChild(doc.createElement("br"));
31518
+ tr.appendChild(td);
31519
+ }
31520
+ tbody.appendChild(tr);
31521
+ }
31522
+ table.appendChild(tbody);
31523
+
31524
+ insertAtCaret(table, doc);
31525
+ injectStyles(doc);
31526
+ fireChange();
31527
+ return table;
31528
+ };
31529
+
31530
+ // ------------------------------------------------------------- conversions
31531
+
31532
+ function toLayout(table) {
31533
+ table.setAttribute("role", "presentation");
31534
+
31535
+ // A caption is announced as the table's name; a layout table has no name
31536
+ // to announce. Keep the text — it is content the author typed — by moving
31537
+ // it out into a paragraph before the table rather than deleting it.
31538
+ var caption = firstOwnChild(table, "caption");
31539
+ if (caption) {
31540
+ var text = String(caption.textContent || "").replace(/^\s+|\s+$/g, "");
31541
+ if (text) {
31542
+ var p = table.ownerDocument.createElement("p");
31543
+ while (caption.firstChild) p.appendChild(caption.firstChild);
31544
+ if (table.parentNode) table.parentNode.insertBefore(p, table);
31545
+ }
31546
+ caption.parentNode.removeChild(caption);
31547
+ }
31548
+
31549
+ table.removeAttribute("summary");
31550
+
31551
+ var cells = ownCells(table), converted = [];
31552
+ for (var i = 0; i < cells.length; i++) {
31553
+ var cell = cells[i];
31554
+ cell.removeAttribute("scope");
31555
+ cell.removeAttribute("headers");
31556
+ cell.removeAttribute("abbr");
31557
+ if (cell.nodeName === "TH") {
31558
+ var td = rename(cell, "td");
31559
+ converted.push(td);
31560
+ }
31561
+ }
31562
+ if (headerMemory && converted.length) headerMemory.set(table, converted);
31563
+
31564
+ // <thead> on a layout table is harmless to AT once role=presentation is
31565
+ // set, but it makes the markup lie to the next human who reads it.
31566
+ var thead = firstOwnChild(table, "thead");
31567
+ if (thead) unwrapSection(table, thead);
31568
+
31569
+ fireChange();
31570
+ return table;
31571
+ }
31572
+
31573
+ function toData(table, options) {
31574
+ table.removeAttribute("role");
31575
+
31576
+ var restored = headerMemory ? headerMemory.get(table) : null;
31577
+ var promoted = false;
31578
+ if (restored && restored.length) {
31579
+ for (var i = 0; i < restored.length; i++) {
31580
+ var cell = restored[i];
31581
+ // The cell may have been deleted, or the whole table replaced,
31582
+ // since it was remembered.
31583
+ if (cell && cell.parentNode && contains(table, cell)) {
31584
+ rename(cell, "th");
31585
+ promoted = true;
31586
+ }
31587
+ }
31588
+ headerMemory["delete"](table);
31589
+ }
31590
+ if (!promoted && options && options.promoteFirstRow) promoteFirstRow(table);
31591
+
31592
+ fireChange();
31593
+ return table;
31594
+ }
31595
+
31596
+ function promoteFirstRow(table) {
31597
+ var rows = ownRows(table);
31598
+ if (!rows.length) return;
31599
+ var cells = rowCells(rows[0]);
31600
+ for (var i = 0; i < cells.length; i++) {
31601
+ var th = rename(cells[i], "th");
31602
+ th.setAttribute("scope", "col");
31603
+ }
31604
+ }
31605
+
31606
+ // ------------------------------------------------------------------ helpers
31607
+
31608
+ function isLayout(table) {
31609
+ if (!table || table.nodeName !== "TABLE") return false;
31610
+ var role = String(table.getAttribute("role") || "").toLowerCase();
31611
+ return role === "presentation" || role === "none";
31612
+ }
31613
+
31614
+ // Tables nest: only touch nodes whose nearest ancestor table is this one.
31615
+ function ownsNode(table, node) {
31616
+ var n = node.parentNode;
31617
+ while (n && n !== table) {
31618
+ if (n.nodeName === "TABLE") return false;
31619
+ n = n.parentNode;
31620
+ }
31621
+ return n === table;
31622
+ }
31623
+
31624
+ function ownCells(table) {
31625
+ var out = [], all = table.querySelectorAll ? table.querySelectorAll("th,td") : [];
31626
+ for (var i = 0; i < all.length; i++) if (ownsNode(table, all[i])) out.push(all[i]);
31627
+ return out;
31628
+ }
31629
+
31630
+ function ownRows(table) {
31631
+ var out = [], all = table.querySelectorAll ? table.querySelectorAll("tr") : [];
31632
+ for (var i = 0; i < all.length; i++) if (ownsNode(table, all[i])) out.push(all[i]);
31633
+ return out;
31634
+ }
31635
+
31636
+ function rowCells(row) {
31637
+ var out = [];
31638
+ for (var n = row.firstChild; n; n = n.nextSibling) {
31639
+ if (n.nodeName === "TH" || n.nodeName === "TD") out.push(n);
31640
+ }
31641
+ return out;
31642
+ }
31643
+
31644
+ function firstOwnChild(table, name) {
31645
+ name = name.toUpperCase();
31646
+ for (var n = table.firstChild; n; n = n.nextSibling) if (n.nodeName === name) return n;
31647
+ return null;
31648
+ }
31649
+
31650
+ function contains(root, node) {
31651
+ var n = node;
31652
+ while (n) { if (n === root) return true; n = n.parentNode; }
31653
+ return false;
31654
+ }
31655
+
31656
+ // Move a <thead>'s rows into the table body and drop the section.
31657
+ function unwrapSection(table, section) {
31658
+ var body = firstOwnChild(table, "tbody");
31659
+ if (!body) {
31660
+ body = table.ownerDocument.createElement("tbody");
31661
+ table.insertBefore(body, section.nextSibling);
31662
+ }
31663
+ while (section.firstChild) body.insertBefore(section.firstChild, body.firstChild);
31664
+ section.parentNode.removeChild(section);
31665
+ }
31666
+
31667
+ // Replace an element with the same content and attributes under a new tag.
31668
+ // The caret may be inside it, so the range is restored afterwards.
31669
+ function rename(el, tagName) {
31670
+ var doc = el.ownerDocument;
31671
+ var replacement = doc.createElement(tagName);
31672
+ for (var i = 0; i < el.attributes.length; i++) {
31673
+ var a = el.attributes[i];
31674
+ try { replacement.setAttribute(a.name, a.value); } catch (e) {}
31675
+ }
31676
+ while (el.firstChild) replacement.appendChild(el.firstChild);
31677
+ el.parentNode.replaceChild(replacement, el);
31678
+ return replacement;
31679
+ }
31680
+
31681
+ function tableAtCaret() {
31682
+ var ed = getEditable();
31683
+ if (!ed) return null;
31684
+ var node = null;
31685
+ try {
31686
+ var sel = editor.getSelection();
31687
+ if (sel && sel.rangeCount) node = sel.getRangeAt(0).startContainer;
31688
+ } catch (e) {}
31689
+ if (!node) return null;
31690
+ if (node.nodeType === 3) node = node.parentNode;
31691
+ while (node && node !== ed) {
31692
+ if (node.nodeName === "TABLE") return node;
31693
+ node = node.parentNode;
31694
+ }
31695
+ return null;
31696
+ }
31697
+
31698
+ function insertAtCaret(node, doc) {
31699
+ var inserted = false;
31700
+ try {
31701
+ var sel = editor.getSelection();
31702
+ if (sel && sel.rangeCount) {
31703
+ var range = sel.getRangeAt(0);
31704
+ range.deleteContents();
31705
+ range.insertNode(node);
31706
+ inserted = true;
31707
+ }
31708
+ } catch (e) {}
31709
+ if (!inserted) {
31710
+ var ed = getEditable();
31711
+ if (ed) { ed.appendChild(node); inserted = true; }
31712
+ }
31713
+ // A table at the very end of the document is a caret trap; gapcursor.js
31714
+ // handles escaping it, so nothing is appended here.
31715
+ return inserted;
31716
+ }
31717
+
31718
+ function getEditable() { try { return editor.getEditable(); } catch (e) { return null; } }
31719
+
31720
+ function editableDoc() {
31721
+ var ed = getEditable();
31722
+ return ed ? ed.ownerDocument : null;
31723
+ }
31724
+
31725
+ function injectStyles(doc) {
31726
+ if (!doc) return;
31727
+ var text = outlineOn
31728
+ ? "table[role=presentation]{outline:1px dashed rgba(120,120,140,.55);outline-offset:1px;}" +
31729
+ "table[role=presentation] td{outline:1px dashed rgba(120,120,140,.28);}"
31730
+ : "";
31731
+ var existing = doc.getElementById("rte-layout-table-styles");
31732
+ if (existing) {
31733
+ if (existing.getAttribute("data-css") === text) return;
31734
+ existing.parentNode && existing.parentNode.removeChild(existing);
31735
+ }
31736
+ if (!text) return;
31737
+ var st = doc.createElement("style");
31738
+ st.id = "rte-layout-table-styles";
31739
+ st.setAttribute("data-css", text);
31740
+ st.appendChild(doc.createTextNode(text));
31741
+ (doc.head || doc.getElementsByTagName("head")[0] || doc.documentElement).appendChild(st);
31742
+ }
31743
+
31744
+ function fireChange() {
31745
+ try { if (typeof editor.updateDesign === "function") editor.updateDesign(); } catch (e) {}
31746
+ try { if (typeof editor.fireChange === "function") editor.fireChange(); } catch (e) {}
31747
+ }
31748
+ }
31749
+
31750
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
31751
+
31277
31752
  // 2026-07-27 Margin line numbers. Numbers every visual line in the left gutter,
31278
31753
  // the way a word processor does for pleadings, statutes, transcripts and any
31279
31754
  // document people cite by line ("page 4, lines 12-15").
@@ -37880,6 +38355,259 @@ function RTE_Plugin_Sanitizer() {
37880
38355
 
37881
38356
  if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
37882
38357
 
38358
+ // 2026-08-30 Shadow DOM support — make the editor usable inside a shadow root.
38359
+ //
38360
+ // TinyMCE documents shadow-DOM support as a supported integration; we had none
38361
+ // under any name. Design systems, micro-frontends and anything built as a custom
38362
+ // element put their markup inside a shadow root, and that is where this editor
38363
+ // was quietly broken.
38364
+ //
38365
+ // Measured before writing a line of it, on a real instance in Chromium:
38366
+ //
38367
+ // | mounted in | toolbar height | font |
38368
+ // |-----------------------|----------------|-------------------|
38369
+ // | the page (light DOM) | 82 px | system-ui |
38370
+ // | a shadow root | 45,468 px | Times New Roman |
38371
+ //
38372
+ // So it is not "mostly fine". The editor CONSTRUCTS, the iframe appears, and
38373
+ // setHTMLCode/getHTMLCode round-trip correctly — which is exactly why this was
38374
+ // never noticed — but a style boundary is a wall: `<link rel=stylesheet>` in the
38375
+ // document head does not cross into a shadow root, so every rule in
38376
+ // rte_theme_default.css misses, and the toolbar renders as a 45,000-pixel column
38377
+ // of unstyled buttons.
38378
+ //
38379
+ // The fix is to put the editor's own stylesheets on the inside of the wall too.
38380
+ //
38381
+ // Design notes:
38382
+ // - <link> clones, not fetch + CSSStyleSheet. Constructing a stylesheet means
38383
+ // fetching the CSS with JS, which needs CORS on a CDN-hosted theme and fails
38384
+ // closed — a cloned <link> hits the HTTP cache, needs no CORS, and works on
38385
+ // every browser that has shadow DOM at all. adoptedStyleSheets is used only
38386
+ // for sheets already adopted by the document, which have no URL to clone.
38387
+ // - Only the EDITOR's stylesheets are copied. Cloning the whole head into a
38388
+ // shadow root would undo the encapsulation the integrator chose it for. A
38389
+ // sheet counts as ours by origin — served from the folder rte.js was loaded
38390
+ // from — plus an `rte-`/`__rte` element id, the same origin-based test that
38391
+ // importstyles.js had to arrive at after prefix matching failed.
38392
+ // - A MutationObserver keeps watching the head. Plugins inject their styles
38393
+ // lazily, when their panel first opens — the accessibility checker and the AI
38394
+ // toolkit both do — so a one-shot copy at init produces a styled toolbar and
38395
+ // an unstyled dialog half an hour later, which is a worse bug to diagnose
38396
+ // than a wholly unstyled editor.
38397
+ // - Nothing is copied when the editor is NOT in a shadow root, and the
38398
+ // observer is not even created. This plugin must cost nothing for the 99%.
38399
+ RTE_DefaultConfig.plugin_shadowdom = RTE_Plugin_ShadowDom;
38400
+
38401
+ // Copy the editor's stylesheets into the shadow root that contains it.
38402
+ if (typeof RTE_DefaultConfig.shadowStyleAdoption === "undefined") RTE_DefaultConfig.shadowStyleAdoption = true;
38403
+ // Extra stylesheet URLs to copy in as well (an integrator's own skin overrides).
38404
+ if (typeof RTE_DefaultConfig.shadowStyleSources === "undefined") RTE_DefaultConfig.shadowStyleSources = null;
38405
+
38406
+ function RTE_Plugin_ShadowDom() {
38407
+ var obj = this;
38408
+ var config, editor;
38409
+ var root = null;
38410
+ var observer = null;
38411
+ var copied = null;
38412
+
38413
+ obj.PluginName = "ShadowDom";
38414
+
38415
+ obj.InitConfig = function (argconfig) { config = argconfig; };
38416
+
38417
+ obj.InitEditor = function (argeditor) {
38418
+ editor = argeditor;
38419
+
38420
+ // Public API — useful even outside a shadow root, for a host page that
38421
+ // wants to style a second root (a dialog portal, a preview pane).
38422
+ editor.getShadowRoot = function () { return findRoot(); };
38423
+ editor.isInShadowDom = function () { return !!findRoot(); };
38424
+ editor.adoptEditorStyles = function (target) { return adopt(target || findRoot(), true); };
38425
+ editor.getAdoptedStyleCount = function () { return copied ? copied.size : 0; };
38426
+
38427
+ if (config && config.shadowStyleAdoption === false) return;
38428
+
38429
+ // The chrome is built asynchronously; the container may not exist yet.
38430
+ whenReady(function () {
38431
+ root = findRoot();
38432
+ if (!root) return;
38433
+ copied = new (window.Set || Array)();
38434
+ adopt(root, false);
38435
+ watchHead(root);
38436
+ });
38437
+ };
38438
+
38439
+ obj.Destroy = function () {
38440
+ if (observer) { try { observer.disconnect(); } catch (e) {} observer = null; }
38441
+ };
38442
+
38443
+ // ------------------------------------------------------------------ lookup
38444
+
38445
+ // The editor's container is the element the integrator passed in; from the
38446
+ // editing iframe it is the nearest ancestor carrying the editor's own class.
38447
+ function container() {
38448
+ try {
38449
+ var editable = editor.getEditable();
38450
+ var frame = editable && editable.ownerDocument && editable.ownerDocument.defaultView
38451
+ ? editable.ownerDocument.defaultView.frameElement : null;
38452
+ if (!frame) return null;
38453
+ var node = frame;
38454
+ while (node) {
38455
+ if (node.classList && node.classList.contains("richtexteditor")) return node;
38456
+ node = node.parentNode || node.host;
38457
+ }
38458
+ return frame;
38459
+ }
38460
+ catch (e) { return null; }
38461
+ }
38462
+
38463
+ function findRoot() {
38464
+ var el = container();
38465
+ if (!el || typeof el.getRootNode !== "function") return null;
38466
+ var r = el.getRootNode();
38467
+ // A DocumentFragment with a `host` is a shadow root; a plain document is not.
38468
+ return (r && r.host && r !== document) ? r : null;
38469
+ }
38470
+
38471
+ function whenReady(callback) {
38472
+ if (container()) { callback(); return; }
38473
+ var tries = 0;
38474
+ var timer = window.setInterval(function () {
38475
+ if (container() || ++tries > 100) { window.clearInterval(timer); callback(); }
38476
+ }, 50);
38477
+ }
38478
+
38479
+ // ------------------------------------------------------------------ adopt
38480
+
38481
+ // Where rte.js itself was served from. Everything under that folder is ours.
38482
+ function assetBase() {
38483
+ try {
38484
+ var scripts = document.getElementsByTagName("script");
38485
+ for (var i = 0; i < scripts.length; i++) {
38486
+ var src = scripts[i].src || "";
38487
+ if (/(^|\/)rte(\.min)?\.js(\?|$)/.test(src)) return src.replace(/[^\/]*$/, "");
38488
+ }
38489
+ }
38490
+ catch (e) {}
38491
+ return null;
38492
+ }
38493
+
38494
+ function isOurs(node) {
38495
+ var id = String(node.id || "");
38496
+ if (/^rte[-_]|^__rte/.test(id)) return true;
38497
+ var href = node.href || "";
38498
+ if (!href) return false;
38499
+ var base = assetBase();
38500
+ if (base && href.indexOf(base) === 0) return true;
38501
+ // Fall back to the file names this product actually ships, so an
38502
+ // integrator who renamed or re-hosted the folder still gets styled
38503
+ // chrome rather than a silent 45,000-pixel toolbar.
38504
+ return /rte_theme|rte_mobile|richtexteditor_content|richtexteditor_preview|aitoolkit/.test(href);
38505
+ }
38506
+
38507
+ function extraSources() {
38508
+ var list = (config && config.shadowStyleSources) || null;
38509
+ if (!list) return [];
38510
+ return typeof list === "string" ? [list] : list;
38511
+ }
38512
+
38513
+ function key(node) {
38514
+ return (node.href || "") + "|" + (node.id || "") + "|" + (node.textContent || "").length;
38515
+ }
38516
+
38517
+ function has(k) {
38518
+ return copied && (copied.has ? copied.has(k) : copied.indexOf(k) >= 0);
38519
+ }
38520
+
38521
+ function remember(k) {
38522
+ if (!copied) return;
38523
+ if (copied.add) copied.add(k); else copied.push(k);
38524
+ }
38525
+
38526
+ function adopt(target, force) {
38527
+ if (!target) return 0;
38528
+ if (!copied) copied = new (window.Set || Array)();
38529
+ var added = 0;
38530
+ var nodes = document.querySelectorAll("link[rel~=stylesheet],style");
38531
+
38532
+ for (var i = 0; i < nodes.length; i++) {
38533
+ var node = nodes[i];
38534
+ if (!isOurs(node)) continue;
38535
+ var k = key(node);
38536
+ if (!force && has(k)) continue;
38537
+ if (has(k)) continue;
38538
+ target.appendChild(cloneSheet(node, target));
38539
+ remember(k);
38540
+ added++;
38541
+ }
38542
+
38543
+ var extras = extraSources();
38544
+ for (var e = 0; e < extras.length; e++) {
38545
+ var ek = "extra|" + extras[e];
38546
+ if (has(ek)) continue;
38547
+ var link = document.createElement("link");
38548
+ link.rel = "stylesheet";
38549
+ link.href = extras[e];
38550
+ link.setAttribute("data-rte-adopted", "1");
38551
+ target.appendChild(link);
38552
+ remember(ek);
38553
+ added++;
38554
+ }
38555
+
38556
+ // Sheets the page adopted programmatically have no URL to clone, so they
38557
+ // are re-adopted by reference where the browser supports it.
38558
+ try {
38559
+ if (target.adoptedStyleSheets && document.adoptedStyleSheets && document.adoptedStyleSheets.length) {
38560
+ var merged = target.adoptedStyleSheets.slice();
38561
+ for (var a = 0; a < document.adoptedStyleSheets.length; a++) {
38562
+ if (merged.indexOf(document.adoptedStyleSheets[a]) < 0) merged.push(document.adoptedStyleSheets[a]);
38563
+ }
38564
+ target.adoptedStyleSheets = merged;
38565
+ }
38566
+ }
38567
+ catch (e2) {}
38568
+
38569
+ return added;
38570
+ }
38571
+
38572
+ function cloneSheet(node, target) {
38573
+ if (node.tagName === "LINK") {
38574
+ var link = document.createElement("link");
38575
+ link.rel = "stylesheet";
38576
+ // Resolved (absolute) href — a relative one would resolve against the
38577
+ // document anyway, but being explicit survives a <base> element.
38578
+ link.href = node.href;
38579
+ if (node.media) link.media = node.media;
38580
+ link.setAttribute("data-rte-adopted", "1");
38581
+ return link;
38582
+ }
38583
+ var style = document.createElement("style");
38584
+ style.textContent = node.textContent;
38585
+ if (node.id) style.setAttribute("data-rte-source-id", node.id);
38586
+ style.setAttribute("data-rte-adopted", "1");
38587
+ return style;
38588
+ }
38589
+
38590
+ // A plugin's <style> arrives when its panel first opens, long after init.
38591
+ function watchHead(target) {
38592
+ if (!window.MutationObserver) return;
38593
+ observer = new MutationObserver(function (records) {
38594
+ var interesting = false;
38595
+ for (var i = 0; i < records.length && !interesting; i++) {
38596
+ var added = records[i].addedNodes || [];
38597
+ for (var j = 0; j < added.length; j++) {
38598
+ var n = added[j];
38599
+ if (n.nodeType !== 1) continue;
38600
+ if ((n.tagName === "STYLE" || n.tagName === "LINK") && isOurs(n)) { interesting = true; break; }
38601
+ }
38602
+ }
38603
+ if (interesting) adopt(target, false);
38604
+ });
38605
+ observer.observe(document.head || document.documentElement, { childList: true, subtree: true });
38606
+ }
38607
+ }
38608
+
38609
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
38610
+
37883
38611
  RTE_DefaultConfig.plugin_slashcommand = RTE_Plugin_SlashCommand;
37884
38612
 
37885
38613
  function RTE_Plugin_SlashCommand() {
@@ -38125,6 +38853,14 @@ function RTE_Plugin_SlashCommand() {
38125
38853
  push("Tools", "tablerownumbers", "Number table rows", "Add or remove an automatic row-numbering column", ["number", "rows", "table", "count", "index"], iconSort(),
38126
38854
  function () { editor.execCommand("tablerownumbers"); });
38127
38855
  }
38856
+ if (typeof editor.setTableLayoutMode === "function") {
38857
+ push("Insert", "layouttable", "Layout table", "Insert a borderless table for positioning content (role=presentation)", ["layout", "table", "grid", "email", "columns", "presentation"], iconTable(),
38858
+ function () { editor.insertLayoutTable(2, 2); });
38859
+ push("Tools", "tablelayoutmode", "Mark table as layout", "This table only positions content — stop screen readers announcing it as data", ["layout", "table", "presentation", "accessibility", "screen reader"], iconTable(),
38860
+ function () { editor.setTableLayoutMode("layout"); });
38861
+ push("Tools", "tabledatamode", "Mark table as data", "This table states relationships — restore its header cells", ["data", "table", "header", "accessibility"], iconTable(),
38862
+ function () { editor.setTableLayoutMode("data", null, { promoteFirstRow: true }); });
38863
+ }
38128
38864
  if (typeof editor.moveBlockUp === "function") {
38129
38865
  push("Tools", "moveblockup", "Move block up", "Move this paragraph or block above the previous one (Alt+Shift+Up)", ["move", "block", "up", "reorder", "drag"], iconMoveBlock(),
38130
38866
  function () { editor.moveBlockUp(); });