@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 +1 -1
- package/richtexteditor/all_plugins.js +168 -0
- package/richtexteditor/all_plugins.min.js +46 -46
- package/richtexteditor/plugins/accessibilitychecker.js +168 -0
- package/richtexteditor/plugins/all_plugins.js +168 -0
- package/richtexteditor/plugins/all_plugins.min.js +46 -46
- package/richtexteditor/rte.js +2 -2
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "2.
|
|
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();
|