@richscripts/richtexteditor 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.12.0",
6
+ "version": "2.13.0",
7
7
  "description": "A full-featured JavaScript rich text editor (WYSIWYG) with toolbar, plugins, i18n support, a built-in AI Toolkit (Ask AI, AI Chat, AI Review drawer, BYOK resolver), structured content APIs (JSON/Markdown), and an optional collaboration / review stack: slash commands, @mentions, human-author Track Changes, threaded comments, revision history, and Yjs-backed presence. Ships first-class React, Vue, Angular and Svelte bindings, a headless \"bring your own UI\" core, and server-safe conversion helpers.",
8
8
  "main": "./richtexteditor/rte.js",
9
9
  "bin": {
@@ -745,6 +745,125 @@ function RTE_Plugin_AccessibilityChecker() {
745
745
  }
746
746
  }
747
747
 
748
+ // 2026-09-22 Six rules the commercial checkers have and this one did not
749
+ // (TinyMCE's paid checker: D1, D3, D4, I3, I4, T1). One pass over the whole
750
+ // editable, like duplicate-id, because several need to see neighbours.
751
+ if (editable && editable.querySelectorAll) {
752
+ // D1 paragraph-as-heading: a short paragraph whose whole text is bold and
753
+ // visibly large looks like a heading but cannot be reached by heading
754
+ // navigation and is missing from the outline.
755
+ var paras = editable.querySelectorAll("p");
756
+ for (var pi = 0; pi < paras.length; pi++) {
757
+ var pEl = paras[pi];
758
+ var pText = getText(pEl);
759
+ if (!pText || pText.length > 80 || /[.!?:;,]$/.test(pText)) continue;
760
+ var win = pEl.ownerDocument.defaultView;
761
+ var strong = pEl.querySelector("strong, b");
762
+ var pStyle = win.getComputedStyle(pEl);
763
+ var boldAll = (strong && getText(strong) === pText) || parseInt(pStyle.fontWeight, 10) >= 600;
764
+ if (!boldAll) continue;
765
+ var sizeEl = strong || pEl;
766
+ var bodySize = parseFloat(win.getComputedStyle(pEl.ownerDocument.body).fontSize) || 16;
767
+ if (parseFloat(win.getComputedStyle(sizeEl).fontSize) < bodySize * 1.2) continue;
768
+ issues.push({
769
+ code: "paragraph-as-heading",
770
+ severity: "warning",
771
+ message: "\"" + pText + "\" looks like a heading (bold, large, on its own line) but is a paragraph. Screen-reader users cannot jump to it. Use a heading level instead.",
772
+ path: "document.p[" + pi + "]",
773
+ _target: pEl
774
+ });
775
+ }
776
+
777
+ // D4 fake-list: two or more consecutive paragraphs that start with the same
778
+ // kind of hand-typed marker ("1." "2." / "-" "*" "•").
779
+ var markerKind = function (el) {
780
+ if (!el || String(el.nodeName).toLowerCase() !== "p") return null;
781
+ var t = getText(el);
782
+ if (/^\d{1,3}[.)]\s+\S/.test(t)) return "ol";
783
+ if (/^[-*•▪●]\s+\S/.test(t)) return "ul";
784
+ return null;
785
+ };
786
+ var kids = editable.children;
787
+ for (var ki = 0; ki < kids.length; ki++) {
788
+ var kind = markerKind(kids[ki]);
789
+ if (!kind) continue;
790
+ var run = 1;
791
+ while (ki + run < kids.length && markerKind(kids[ki + run]) === kind) run++;
792
+ if (run >= 2) {
793
+ issues.push({
794
+ code: "fake-list",
795
+ severity: "warning",
796
+ message: run + " paragraphs are typed as a " + (kind === "ol" ? "numbered" : "bulleted") + " list. Screen readers do not announce them as a list or say how many items it has. Use the list button instead.",
797
+ path: "content[" + ki + "]",
798
+ _target: kids[ki]
799
+ });
800
+ }
801
+ ki += run - 1;
802
+ }
803
+
804
+ // I3 / I4: alt text that is a file name, or too long to be heard comfortably.
805
+ var imgs = editable.querySelectorAll("img[alt]");
806
+ for (var ii = 0; ii < imgs.length; ii++) {
807
+ var alt = (imgs[ii].getAttribute("alt") || "").trim();
808
+ if (!alt) continue; // alt="" is a deliberate decorative marker
809
+ var srcName = ((imgs[ii].getAttribute("src") || "").split(/[?#]/)[0].split("/").pop() || "").toLowerCase();
810
+ if (/\.(jpe?g|png|gif|webp|bmp|svg|avif|tiff?)$/i.test(alt) || (srcName && alt.toLowerCase() === srcName) || /^(img|image|dsc|photo|screenshot)[-_ ]?\d+/i.test(alt)) {
811
+ issues.push({
812
+ code: "image-alt-filename",
813
+ severity: "error",
814
+ message: "Alt text \"" + alt + "\" is a file name, which describes nothing. Say what the image shows.",
815
+ path: "document.img[" + ii + "]",
816
+ _target: imgs[ii]
817
+ });
818
+ } else if (alt.length > 150) {
819
+ issues.push({
820
+ code: "image-alt-too-long",
821
+ severity: "warning",
822
+ message: "Alt text is " + alt.length + " characters. Keep it to a short description; put longer detail in the text or a caption.",
823
+ path: "document.img[" + ii + "]",
824
+ _target: imgs[ii]
825
+ });
826
+ }
827
+ }
828
+
829
+ // D3 link-adjacent-duplicate: two links next to each other with the same
830
+ // href (typically an image link and a text link) are announced twice.
831
+ var links = editable.querySelectorAll("a[href]");
832
+ for (var li = 1; li < links.length; li++) {
833
+ var prev = links[li - 1], cur = links[li];
834
+ if (prev.getAttribute("href") !== cur.getAttribute("href")) continue;
835
+ var between = prev.nextSibling, adjacent = true;
836
+ while (between && between !== cur) {
837
+ if (between.nodeType === 1 || (between.nodeType === 3 && /\S/.test(between.data))) { adjacent = false; break; }
838
+ between = between.nextSibling;
839
+ }
840
+ if (adjacent && between === cur) {
841
+ issues.push({
842
+ code: "link-adjacent-duplicate",
843
+ severity: "warning",
844
+ message: "Two links next to each other go to the same place, so it is announced twice. Combine them into one link.",
845
+ path: "document.link[" + li + "]",
846
+ _target: cur
847
+ });
848
+ }
849
+ }
850
+
851
+ // T1 table-missing-caption: a DATA table (it has header cells) without a caption.
852
+ var tables = editable.querySelectorAll("table");
853
+ for (var ti = 0; ti < tables.length; ti++) {
854
+ var tb = tables[ti];
855
+ if ((tb.getAttribute("role") || "").toLowerCase() === "presentation") continue;
856
+ if (!tb.querySelector("th") || tb.querySelector("caption")) continue;
857
+ issues.push({
858
+ code: "table-missing-caption",
859
+ severity: "warning",
860
+ message: "Data table has no caption. A short caption lets screen-reader users know what the table is before they enter it.",
861
+ path: "document.table[" + ti + "]",
862
+ _target: tb
863
+ });
864
+ }
865
+ }
866
+
748
867
  return { document: null, issues: issues, valid: !issues.length, source: "dom" };
749
868
  }
750
869
 
@@ -801,6 +920,55 @@ function RTE_Plugin_AccessibilityChecker() {
801
920
  markLanguageRuns(target, options && options.lang ? options.lang : issue._lang, issue._script);
802
921
  }
803
922
 
923
+ else if (issue.code === "paragraph-as-heading") {
924
+ var lvl = Math.max(1, Math.min(6, parseInt(options && options.targetLevel, 10) || 2));
925
+ var h = target.ownerDocument.createElement("h" + lvl);
926
+ // the paragraph's whole text was bold/large: that styling is the heading now
927
+ h.textContent = (target.textContent || "").replace(/^\s+|\s+$/g, "");
928
+ copyAttributes(target, h);
929
+ h.removeAttribute("style");
930
+ target.parentNode.replaceChild(h, target);
931
+ }
932
+ else if (issue.code === "fake-list") {
933
+ var d = target.ownerDocument;
934
+ var listParent = target.parentNode;
935
+ var ordered = /^\d/.test((target.textContent || "").replace(/^\s+/, ""));
936
+ var list = d.createElement(ordered ? "ol" : "ul");
937
+ var node = target;
938
+ while (node && node.nodeName === "P") {
939
+ var t = (node.textContent || "").replace(/^\s+/, "");
940
+ var m = ordered ? /^\d{1,3}[.)]\s+([\s\S]*)$/.exec(t) : /^[-*•▪●]\s+([\s\S]*)$/.exec(t);
941
+ if (!m) break;
942
+ var li = d.createElement("li");
943
+ li.textContent = m[1].replace(/\s+$/, "");
944
+ list.appendChild(li);
945
+ var next = node.nextElementSibling;
946
+ node.parentNode.removeChild(node);
947
+ node = next;
948
+ }
949
+ if (node && node.parentNode === listParent) listParent.insertBefore(list, node);
950
+ else listParent.appendChild(list);
951
+ }
952
+ else if (issue.code === "table-missing-caption") {
953
+ var capText = String((options && options.captionText) || "").replace(/^\s+|\s+$/g, "");
954
+ if (capText) {
955
+ var cap = target.ownerDocument.createElement("caption");
956
+ cap.textContent = capText;
957
+ target.insertBefore(cap, target.firstChild);
958
+ }
959
+ }
960
+ else if (issue.code === "link-adjacent-duplicate") {
961
+ // merge the second link into the first: one link, one announcement
962
+ var prevLink = target.previousSibling;
963
+ while (prevLink && prevLink.nodeType !== 1) prevLink = prevLink.previousSibling;
964
+ if (prevLink && prevLink.nodeName === "A") {
965
+ while (target.firstChild) prevLink.appendChild(target.firstChild);
966
+ target.parentNode.removeChild(target);
967
+ }
968
+ }
969
+ else if (issue.code === "image-alt-filename" || issue.code === "image-alt-too-long") {
970
+ target.setAttribute("alt", String((options && options.altText) || "").replace(/^\s+|\s+$/g, ""));
971
+ }
804
972
  selectedIssueIndex = 0;
805
973
  scheduleEditorChange();
806
974
  return runAudit();