@richscripts/richtexteditor 2.4.2 → 2.5.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.
@@ -232,7 +232,14 @@ function RTE_Plugin_AccessibilityChecker() {
232
232
  function repairIssue(issueIndex, options) {
233
233
  if (!lastResult || !lastResult.issues || !lastResult.issues[issueIndex]) return runAudit();
234
234
  var ctor = window.RichTextEditor;
235
- if (!ctor || typeof ctor.repairAccessibilityIssue !== "function") return repairDomIssue(lastResult.issues[issueIndex], options || {});
235
+ // An issue carrying `_target` came from the DOM audit and is addressed
236
+ // by a DOM node, not by a JSON path. Handing it to the structured-content
237
+ // repairer looks like it works — no error, a document comes back — but
238
+ // that repairer cannot resolve the node and returns the input unchanged,
239
+ // so the fix silently does nothing.
240
+ var issue = lastResult.issues[issueIndex];
241
+ if (issue._target || lastResult.source === "dom") return repairDomIssue(issue, options || {});
242
+ if (!ctor || typeof ctor.repairAccessibilityIssue !== "function") return repairDomIssue(issue, options || {});
236
243
  var nextDocument = ctor.repairAccessibilityIssue(editor.getJSON(), lastResult.issues[issueIndex], options || {});
237
244
  editor.setJSON(nextDocument);
238
245
  selectedIssueIndex = issueIndex;
@@ -264,6 +271,101 @@ function RTE_Plugin_AccessibilityChecker() {
264
271
  editor.focus();
265
272
  }
266
273
 
274
+ // ---- WCAG 3.1.2 Language of Parts -----------------------------------
275
+ //
276
+ // A passage in a language other than the document's must be marked, or a
277
+ // screen reader pronounces it with the wrong phoneme set. Detecting
278
+ // "French inside English" is a language-identification problem we are not
279
+ // going to pretend to solve; detecting a different WRITING SYSTEM is
280
+ // deterministic, and it catches the cases that actually break speech
281
+ // synthesis outright (Hebrew, Arabic, Greek, Cyrillic, CJK in a Latin
282
+ // document, and the reverse). We flag only what we can be certain of —
283
+ // a checker that cries wolf gets switched off.
284
+ var SCRIPTS = [
285
+ { name: "Hebrew", lang: "he", re: /[֐-׿]/g },
286
+ { name: "Arabic", lang: "ar", re: /[؀-ۿݐ-ݿﭐ-﷿ﹰ-]/g },
287
+ { name: "Greek", lang: "el", re: /[Ͱ-Ͽἀ-῿]/g },
288
+ { name: "Cyrillic", lang: "ru", re: /[Ѐ-ӿ]/g },
289
+ { name: "Han", lang: "zh", re: /[㐀-䶿一-鿿]/g },
290
+ { name: "Kana", lang: "ja", re: /[぀-ゟ゠-ヿ]/g },
291
+ { name: "Hangul", lang: "ko", re: /[가-힯ᄀ-ᇿ]/g },
292
+ { name: "Devanagari", lang: "hi", re: /[ऀ-ॿ]/g },
293
+ { name: "Thai", lang: "th", re: /[฀-๿]/g },
294
+ { name: "Latin", lang: "en", re: /[A-Za-zÀ-ɏ]/g }
295
+ ];
296
+
297
+ // Below this, a "run" is more likely a stray glyph, a maths symbol or a
298
+ // single borrowed character than a passage worth annotating.
299
+ var MIN_SCRIPT_RUN = 4;
300
+
301
+ // Every script present in the text with enough characters to be a passage.
302
+ //
303
+ // Deliberately NOT "the dominant script": a foreign phrase is by definition
304
+ // the minority of its paragraph, so picking the most common script finds
305
+ // the document's own language and reports nothing. "A Hebrew proverb,
306
+ // שלום עליכם, appears mid-sentence" is 40 Latin characters and 9 Hebrew —
307
+ // the 9 are the entire point.
308
+ function scriptsIn(text) {
309
+ var found = [];
310
+ for (var i = 0; i < SCRIPTS.length; i++) {
311
+ var m = String(text).match(SCRIPTS[i].re);
312
+ if (m && m.length >= MIN_SCRIPT_RUN) found.push(SCRIPTS[i]);
313
+ }
314
+ return found;
315
+ }
316
+
317
+ function documentScript() {
318
+ var editable = editor.getEditable ? editor.getEditable() : null;
319
+ var code = (editable && editable.getAttribute("lang")) ||
320
+ (typeof document !== "undefined" && document.documentElement.getAttribute("lang")) || "en";
321
+ code = String(code).toLowerCase().split("-")[0];
322
+ for (var i = 0; i < SCRIPTS.length; i++) if (SCRIPTS[i].lang === code) return SCRIPTS[i];
323
+ // An unlisted language tag (nl, pt, sv…) is Latin-script; defaulting to
324
+ // Latin keeps us from flagging every English word in a Dutch document.
325
+ return SCRIPTS[SCRIPTS.length - 1];
326
+ }
327
+
328
+ function hasLangAncestor(node, editable) {
329
+ var n = (node && node.nodeType === 3) ? node.parentNode : node;
330
+ while (n && n !== editable && n.nodeType === 1) {
331
+ if (n.hasAttribute && n.hasAttribute("lang")) return true;
332
+ n = n.parentNode;
333
+ }
334
+ return false;
335
+ }
336
+
337
+ function collectUnmarkedLanguageRuns(block, editable, path, issues) {
338
+ var docScript = documentScript();
339
+ var doc = block.ownerDocument;
340
+ if (!doc || !doc.createTreeWalker) return;
341
+ var walker = doc.createTreeWalker(block, 4 /* SHOW_TEXT */, null, false);
342
+ var textNode, seen = {};
343
+ while ((textNode = walker.nextNode())) {
344
+ var value = textNode.nodeValue || "";
345
+ if (!value.replace(/\s+/g, "")) continue;
346
+ if (hasLangAncestor(textNode, editable)) continue;
347
+ var scripts = scriptsIn(value);
348
+ for (var s = 0; s < scripts.length; s++) {
349
+ var script = scripts[s];
350
+ if (script === docScript) continue;
351
+ // One issue per script per block: a paragraph with six Hebrew
352
+ // words is one thing to fix, not six.
353
+ if (seen[script.name]) continue;
354
+ seen[script.name] = true;
355
+ issues.push({
356
+ code: "language-of-parts",
357
+ severity: "warning",
358
+ message: script.name + " text is not marked with a language. Select the phrase and set its " +
359
+ "language so assistive technology pronounces it correctly (WCAG 3.1.2).",
360
+ path: path + ".lang",
361
+ _target: block,
362
+ _lang: script.lang,
363
+ _script: script.name
364
+ });
365
+ }
366
+ }
367
+ }
368
+
267
369
  function auditDomAccessibility() {
268
370
  var editable = editor.getEditable ? editor.getEditable() : null;
269
371
  var issues = [];
@@ -333,6 +435,8 @@ function RTE_Plugin_AccessibilityChecker() {
333
435
  });
334
436
  }
335
437
  }
438
+
439
+ collectUnmarkedLanguageRuns(node, editable, path, issues);
336
440
  }
337
441
 
338
442
  return { document: null, issues: issues, valid: !issues.length, source: "dom" };
@@ -358,12 +462,74 @@ function RTE_Plugin_AccessibilityChecker() {
358
462
  else if (issue.code === "table-missing-header") {
359
463
  promoteFirstTableRow(target);
360
464
  }
465
+ else if (issue.code === "language-of-parts") {
466
+ markLanguageRuns(target, options && options.lang ? options.lang : issue._lang, issue._script);
467
+ }
361
468
 
362
469
  selectedIssueIndex = 0;
363
470
  scheduleEditorChange();
364
471
  return runAudit();
365
472
  }
366
473
 
474
+ // Wrap the offending script's runs in <span lang> — the same markup
475
+ // textpartlanguage.js produces, so the two features agree on one shape and
476
+ // the author can retarget the result from the Language dropdown afterwards.
477
+ //
478
+ // Only the matching characters (plus the spaces and punctuation BETWEEN
479
+ // them) are wrapped: wrapping the whole text node would relabel the
480
+ // surrounding English as Hebrew, which is a worse lie than leaving it
481
+ // unmarked.
482
+ function markLanguageRuns(block, lang, scriptName) {
483
+ if (!block || !lang) return;
484
+ var script = null;
485
+ for (var i = 0; i < SCRIPTS.length; i++) if (SCRIPTS[i].name === scriptName) script = SCRIPTS[i];
486
+ if (!script) return;
487
+ var doc = block.ownerDocument;
488
+ var editable = editor.getEditable ? editor.getEditable() : null;
489
+ var rtl = (lang === "he" || lang === "ar" || lang === "fa" || lang === "ur");
490
+
491
+ var walker = doc.createTreeWalker(block, 4 /* SHOW_TEXT */, null, false);
492
+ var pending = [], n;
493
+ while ((n = walker.nextNode())) {
494
+ if (hasLangAncestor(n, editable)) continue;
495
+ pending.push(n);
496
+ }
497
+
498
+ for (var t = 0; t < pending.length; t++) {
499
+ var node = pending[t];
500
+ var value = node.nodeValue || "";
501
+ // Character classes are per-char; build a run matcher that also
502
+ // absorbs the neutral characters sitting inside a passage.
503
+ var runRe = new RegExp("(?:" + script.re.source + "|[\\s.,;:!?'\"()\\u2010-\\u2027\\u00AB\\u00BB\\u201C\\u201D])*" +
504
+ script.re.source +
505
+ "(?:" + script.re.source + "|[\\s.,;:!?'\"()\\u2010-\\u2027\\u00AB\\u00BB\\u201C\\u201D])*", "g");
506
+ var frag = doc.createDocumentFragment();
507
+ var last = 0, m, wrapped = false;
508
+ while ((m = runRe.exec(value))) {
509
+ var text = m[0];
510
+ // Trim the neutrals the greedy match pulled in at the edges;
511
+ // they belong to the surrounding sentence, not the phrase.
512
+ var lead = text.length - text.replace(/^[\s.,;:!?'"()‐-‧«»“”]+/, "").length;
513
+ var trail = text.length - text.replace(/[\s.,;:!?'"()‐-‧«»“”]+$/, "").length;
514
+ var start = m.index + lead;
515
+ var end = m.index + text.length - trail;
516
+ if (end - start < MIN_SCRIPT_RUN) continue;
517
+ if (start > last) frag.appendChild(doc.createTextNode(value.slice(last, start)));
518
+ var span = doc.createElement("span");
519
+ span.setAttribute("lang", lang);
520
+ if (rtl) span.setAttribute("dir", "rtl");
521
+ span.appendChild(doc.createTextNode(value.slice(start, end)));
522
+ frag.appendChild(span);
523
+ last = end;
524
+ wrapped = true;
525
+ if (runRe.lastIndex === m.index) runRe.lastIndex++; // zero-width guard
526
+ }
527
+ if (!wrapped) continue;
528
+ if (last < value.length) frag.appendChild(doc.createTextNode(value.slice(last)));
529
+ node.parentNode.replaceChild(frag, node);
530
+ }
531
+ }
532
+
367
533
  function promoteFirstTableRow(table) {
368
534
  var firstRow = table && table.rows && table.rows.length ? table.rows[0] : null;
369
535
  if (!firstRow) return;
@@ -512,6 +678,25 @@ function RTE_Plugin_AccessibilityChecker() {
512
678
  return;
513
679
  }
514
680
 
681
+ if (issue.code === "language-of-parts") {
682
+ var langRow = detail.ownerDocument.createElement("div");
683
+ langRow.className = "rte-a11y-row";
684
+ var mark = detail.ownerDocument.createElement("button");
685
+ mark.type = "button";
686
+ mark.className = "rte-a11y-button";
687
+ mark.innerText = "Mark as " + issue._script;
688
+ mark.onclick = function () { repairIssue(selectedIssueIndex, { lang: issue._lang }); };
689
+ var look = detail.ownerDocument.createElement("button");
690
+ look.type = "button";
691
+ look.className = "rte-a11y-button rte-a11y-button-secondary";
692
+ look.innerText = "Focus issue";
693
+ look.onclick = function () { focusIssue(issue); };
694
+ langRow.appendChild(mark);
695
+ langRow.appendChild(look);
696
+ detail.appendChild(langRow);
697
+ return;
698
+ }
699
+
515
700
  detail.innerHTML += '<div class="rte-a11y-detail-copy">This issue is visible here, but does not have an automatic repair action yet.</div>';
516
701
  }
517
702
 
@@ -39,7 +39,7 @@ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
39
39
 
40
40
  if (!RTE_DefaultConfig.svgCode_aiassist) {
41
41
  RTE_DefaultConfig.svgCode_aiassist = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true"><defs><linearGradient id="rte-ai-robot-shell" x1="5" y1="6" x2="19" y2="18" gradientUnits="userSpaceOnUse"><stop stop-color="#1fb6ff"/><stop offset=".52" stop-color="#2563eb"/><stop offset="1" stop-color="#7c3aed"/></linearGradient><linearGradient id="rte-ai-robot-glow" x1="8" y1="10" x2="16" y2="15" gradientUnits="userSpaceOnUse"><stop stop-color="#fef9c3"/><stop offset="1" stop-color="#fde68a"/></linearGradient></defs><path d="M12 2.4v2.4" stroke="#f59e0b" stroke-width="1.7" stroke-linecap="round"/><circle cx="12" cy="2.4" r="1" fill="#fbbf24"/><rect x="5.1" y="6.1" width="13.8" height="11.2" rx="3.4" fill="url(#rte-ai-robot-shell)" stroke="#1e40af" stroke-width="1.2"/><path d="M8.2 6.1V5.8A1.8 1.8 0 0110 4h4a1.8 1.8 0 011.8 1.8v.3" stroke="#93c5fd" stroke-width="1.2" stroke-linecap="round"/><rect x="7.7" y="9.2" width="8.6" height="4.8" rx="2.4" fill="#0f172a" opacity=".26"/><circle cx="10" cy="11.6" r="1.15" fill="url(#rte-ai-robot-glow)"/><circle cx="14" cy="11.6" r="1.15" fill="url(#rte-ai-robot-glow)"/><path d="M9.4 14.9c.8.55 1.66.83 2.6.83s1.8-.28 2.6-.83" stroke="#ffffff" stroke-width="1.45" stroke-linecap="round"/><path d="M8.1 19.2l1-2.1M15.9 19.2l-1-2.1" stroke="#2563eb" stroke-width="1.5" stroke-linecap="round"/><path d="M18.8 6.4l.55-1.25.55 1.25 1.25.55-1.25.55-.55 1.25-.55-1.25-1.25-.55z" fill="#fef08a"/></svg>';
42
- RTE_DefaultConfig.svgCode_aiassist = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true"><defs><linearGradient id="rte-ai-bot-v4-shell" x1="4.2" y1="5.8" x2="19.8" y2="18.4" gradientUnits="userSpaceOnUse"><stop stop-color="#22d3ee"/><stop offset=".52" stop-color="#2563eb"/><stop offset="1" stop-color="#4338ca"/></linearGradient><linearGradient id="rte-ai-bot-v4-visor" x1="7.8" y1="9" x2="16.2" y2="13.8" gradientUnits="userSpaceOnUse"><stop stop-color="#f8fafc"/><stop offset="1" stop-color="#bfdbfe"/></linearGradient></defs><path d="M12 2.6v2.3" stroke="#f59e0b" stroke-width="1.7" stroke-linecap="round"/><circle cx="12" cy="2.6" r=".9" fill="#fbbf24"/><path d="M4.9 12.2h-1a1.4 1.4 0 010-2.8h1M19.1 9.4h1a1.4 1.4 0 010 2.8h-1" stroke="#2563eb" stroke-width="1.25" stroke-linecap="round"/><rect x="5.2" y="5.9" width="13.6" height="12.2" rx="4" fill="url(#rte-ai-bot-v4-shell)" stroke="#1e40af" stroke-width="1.15"/><rect x="7.6" y="8.8" width="8.8" height="5.2" rx="2.6" fill="url(#rte-ai-bot-v4-visor)" opacity=".98"/><circle cx="10.2" cy="11.4" r=".9" fill="#0f172a"/><circle cx="13.8" cy="11.4" r=".9" fill="#0f172a"/><path d="M9.5 15.3c.78.52 1.6.78 2.5.78s1.72-.26 2.5-.78" stroke="#eff6ff" stroke-width="1.35" stroke-linecap="round"/><path d="M7.7 20.2l1.15-2.15M16.3 20.2l-1.15-2.15" stroke="#2563eb" stroke-width="1.45" stroke-linecap="round"/><path d="M18.6 5.1l.45-.95.45.95.95.45-.95.45-.45.95-.45-.95-.95-.45z" fill="#fde68a"/></svg>';
42
+ RTE_DefaultConfig.svgCode_aiassist = '<svg viewBox="1.78 0.3 21.8 21.8" width="20" height="20" fill="none" aria-hidden="true"><defs><linearGradient id="rte-ai-bot-v5-shell" x1="3" y1="5" x2="21" y2="20" gradientUnits="userSpaceOnUse"><stop stop-color="#22d3ee"/><stop offset=".52" stop-color="#2563eb"/><stop offset="1" stop-color="#4338ca"/></linearGradient><linearGradient id="rte-ai-bot-v5-visor" x1="7" y1="8" x2="17" y2="15" gradientUnits="userSpaceOnUse"><stop stop-color="#f8fafc"/><stop offset="1" stop-color="#bfdbfe"/></linearGradient></defs><rect x="2.6" y="5.2" width="16.4" height="16" rx="4.8" fill="url(#rte-ai-bot-v5-shell)" stroke="#1e40af" stroke-width="1.6"/><rect x="5.9" y="9.1" width="9.8" height="6.8" rx="3.4" fill="url(#rte-ai-bot-v5-visor)"/><circle cx="8.6" cy="12.5" r="1.35" fill="#0f172a"/><circle cx="13" cy="12.5" r="1.35" fill="#0f172a"/><path d="M8.3 18.2c.9.7 1.8 1 2.5 1s1.6-.3 2.5-1" stroke="#eff6ff" stroke-width="1.7" stroke-linecap="round"/><path d="M19.6 .4l1.25 2.7 2.7 1.25-2.7 1.25-1.25 2.7-1.25-2.7-2.7-1.25 2.7-1.25z" fill="#fbbf24"/></svg>';
43
43
  }
44
44
 
45
45
  if (!RTE_DefaultConfig.svgCode_aiassist_open_dialog) {