Script documentation Anglais

Technical documentation

Babel Scripts – Technical Documentation

This page documents the behavior, scope, limitations, and source code of the bookmarklets used for reviewing and analyzing Babel segments.

← Back to toolbox

Punctuation V1.00

Description

Script for detecting typographic anomalies in visible Babel segments. It scans compatible text fields and stops at each detected anomaly.

Covered rules

  • Expected capital letter at the beginning of a segment, unless the segment starts with ... or .
  • Misplaced uppercase letters inside the segment.
  • Uppercase after :, except for certain tags such as SIC: and PRO:.
  • Detection of hyphen-joined repetitions, such as de-de.
  • Detection of il y without a.
  • Double spaces.
  • Space before , and ..
  • Space before ).
  • Missing space after , . ; : ! ?.
  • Missing space before ; : ! ?.
  • Incorrect spacing around « and ».

Known limitations

  • The script remains heuristic: some stylistic cases may be flagged incorrectly.
  • The behavior depends on Babel’s DOM structure, especially for selection in certain virtualized fields.
  • The script does not replace a complete style guide or a final editorial validation.

Readable code

(function () {
  const sel = 'textarea,input[type=text],[contenteditable=true],[role=textbox]';
  const skipSegRe = /^\s*(?:\[[^\]]*\]|<[^>]*>)\s*$/;
  const INV = /[\u200B-\u200D\u2060\uFEFF\u00AD]/g;

  function toast(msg) {
    let d = document.getElementById('__babel_punct_toast');
    if (!d) {
      d = document.createElement('div');
      d.id = '__babel_punct_toast';
      d.style.cssText =
        'position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#111;color:#fff;padding:8px 12px;border-radius:10px;font:13px/1.35 system-ui,Segoe UI,Arial;box-shadow:0 6px 18px rgba(0,0,0,.25);max-width:92vw;';
      document.documentElement.appendChild(d);
    }
    d.textContent = msg;
    d.style.display = 'block';
    clearTimeout(d.__t);
    d.__t = setTimeout(() => {
      d.style.display = 'none';
    }, 2600);
  }

  function getT(el) {
    return el.value != null ? el.value : ((el.textContent ?? el.innerText) ?? '');
  }

  function keyOf(el, idx) {
    const attrs = ['data-segment-id', 'data-segid', 'data-id', 'id', 'name', 'data-testid', 'aria-label'];
    let n = el;
    for (let k = 0; k < 8 && n; n = n.parentElement, k++) {
      for (const a of attrs) {
        const v = n.getAttribute && n.getAttribute(a);
        if (v) return a + ':' + v;
      }
    }
    return 'idx:' + idx;
  }

  function selectInInput(el, s, e) {
    try {
      el.focus();
      el.setSelectionRange(s, e);
    } catch (_) {}
  }

  function selectInCE(el, s, e) {
    try {
      el.focus();
      const tw = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
      let node,
        idx = 0,
        sn = null,
        so = 0,
        en = null,
        eo = 0;

      while ((node = tw.nextNode())) {
        const len = node.nodeValue.length;
        if (!sn && idx + len >= s) {
          sn = node;
          so = s - idx;
        }
        if (idx + len >= e) {
          en = node;
          eo = e - idx;
          break;
        }
        idx += len;
      }

      if (!sn || !en) return false;

      const r = document.createRange();
      r.setStart(sn, Math.max(0, Math.min(so, sn.nodeValue.length)));
      r.setEnd(en, Math.max(0, Math.min(eo, en.nodeValue.length)));

      const selObj = window.getSelection();
      selObj.removeAllRanges();
      selObj.addRange(r);
      return true;
    } catch (_) {
      return false;
    }
  }

  function focusAndSelect(el, s, e) {
    try {
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    } catch (_) {
      try {
        el.scrollIntoView(true);
      } catch (__) {}
    }

    if (el.value != null && typeof el.setSelectionRange === 'function') {
      selectInInput(el, s, e);
      return true;
    }

    if (el.isContentEditable) {
      return selectInCE(el, s, e);
    }

    return false;
  }

  function skipLeading(t) {
    let i = 0;
    while (i < t.length && /\s/.test(t[i])) i++;
    return i;
  }

  function startsWithEllipsis(t) {
    const i = skipLeading(t);
    const s = t.slice(i);
    return s.startsWith('…') || s.startsWith('...');
  }

  function firstLetterIndex(t) {
    for (let i = 0; i < t.length; i++) {
      const c = t[i];
      if (/[A-Za-zÀ-ÖØ-öø-ÿ]/.test(c)) return i;
      if (/\S/.test(c)) break;
    }
    return -1;
  }

  function getTagBeforeColon(t, i) {
    let w = i - 1;
    while (w >= 0 && t[w] === ' ') w--;
    let end = w + 1;
    while (w >= 0 && /[A-Za-z]/.test(t[w])) w--;
    return t.slice(w + 1, end).toUpperCase();
  }

  function isTaxoColon(t, i) {
    if (t[i] !== ':') return false;
    const tag = getTagBeforeColon(t, i);
    return tag === 'SIC' || tag === 'PRO';
  }

  function isEllipsisDot(t, i) {
    return t[i] === '.' && (t[i - 1] === '.' || t[i + 1] === '.');
  }

  function findBadCaps(t, from) {
    const U = /[A-ZÀ-ÖØ-Þ]/,
      L = /[a-zà-öø-ÿ]/;
    for (let i = Math.max(0, from || 0); i < t.length; i++) {
      const c = t[i];
      if (!U.test(c)) continue;
      const p = t[i - 1] || '';
      const n = t[i + 1] || '';
      if (L.test(p)) return { s: i, e: i + 1 };
      if (p === ' ') {
        let j = i - 1;
        while (j >= 0 && t[j] === ' ') j--;
        const pn = j >= 0 ? t[j] : '';
        if (pn && '.!?…\n\r«(":\''.indexOf(pn) >= 0) continue;
        if (L.test(n)) return { s: i, e: i + 1 };
      }
      if (U.test(p) && U.test(n)) continue;
    }
    return null;
  }

  function findRepeatDashError(t, from) {
    const re = /\b([A-Za-zÀ-ÖØ-öø-ÿ]{1,20})(-{1,2})(\1)\b/g;
    re.lastIndex = Math.max(0, from || 0);
    let m;
    while ((m = re.exec(t))) {
      return { s: m.index, e: m.index + m[0].length };
    }
    return null;
  }

  function findIlYError(t, from) {
    const re = /\bil y\b(?!\s+a)/gi;
    re.lastIndex = Math.max(0, from || 0);
    let m;
    while ((m = re.exec(t))) {
      return { s: m.index, e: m.index + m[0].length };
    }
    return null;
  }

  const RULES = [
    {
      label: 'début segment: majuscule (sauf …/...)',
      find: (t, from) => {
        if (startsWithEllipsis(t)) return null;
        const i = firstLetterIndex(t);
        if (i < 0 || i < from) return null;
        if (/[a-zà-öø-ÿ]/.test(t[i])) return { s: i, e: i + 1 };
        return null;
      }
    },
    {
      label: 'majuscule mal placée',
      find: (t, from) => findBadCaps(t, from)
    },
    {
      label: 'majuscule après :',
      find: (t, from) => {
        for (let i = Math.max(0, from || 0); i < t.length - 2; i++) {
          if (t[i] === ':') {
            if (isTaxoColon(t, i)) continue;
            let j = i + 1;
            while (j < t.length && t[j] === ' ') j++;
            if (j >= t.length) continue;
            if (/[A-ZÀ-ÖØ-Þ]/.test(t[j])) return { s: j, e: j + 1 };
          }
        }
        return null;
      }
    },
    {
      label: 'répétition collée avec tiret',
      find: (t, from) => findRepeatDashError(t, from)
    },
    {
      label: 'il y sans a',
      find: (t, from) => findIlYError(t, from)
    },
    {
      label: 'double espace',
      re: / {2,}/g,
      span: (m) => [m.index, m.index + m[0].length]
    },
    {
      label: 'espace avant , .',
      re: / +[,.]/g,
      span: (m) => [m.index, m.index + m[0].length]
    },
    {
      label: 'espace avant )',
      re: / +\)/g,
      span: (m) => [m.index, m.index + m[0].length]
    },
    {
      label: 'espace avant .',
      re: / +\./g,
      span: (m) => [m.index, m.index + m[0].length]
    },
    {
      label: 'manque espace après , . ; : ! ?',
      find: (t, from) => {
        for (let i = Math.max(0, from || 0); i < t.length; i++) {
          const c = t[i];
          if (',.;:!?'.includes(c)) {
            if (c === ':' && isTaxoColon(t, i)) continue;
            if (c === '.' && isEllipsisDot(t, i)) continue;
            let j = i + 1;
            if (j >= t.length) continue;
            if (t[j] !== ' ') return { s: i, e: i + 1 };
          }
        }
        return null;
      }
    },
    {
      label: 'manque espace avant ; : ! ?',
      find: (t, from) => {
        for (let i = Math.max(1, from || 0); i < t.length; i++) {
          const c = t[i];
          if (';:!?'.includes(c)) {
            if (c === ':' && isTaxoColon(t, i)) continue;
            if (t[i - 1] !== ' ') return { s: i, e: i + 1 };
          }
        }
        return null;
      }
    },
    {
      label: 'guillemets: espace après «',
      find: (t, from) => {
        for (let i = Math.max(0, from || 0); i < t.length; i++) {
          if (t[i] === '«') {
            if (i + 1 >= t.length) return null;
            if (t[i + 1] !== ' ') return { s: i, e: i + 1 };
          }
        }
        return null;
      }
    },
    {
      label: 'guillemets: espace avant »',
      find: (t, from) => {
        for (let i = Math.max(0, from || 0); i < t.length; i++) {
          if (t[i] === '»') {
            if (i === 0) return null;
            if (t[i - 1] !== ' ') return { s: i, e: i + 1 };
          }
        }
        return null;
      }
    }
  ];

  function findNext(txt, from) {
    txt = txt.replace(INV, '');
    let best = null;
    for (const r of RULES) {
      if (r.find) {
        const hit = r.find(txt, from);
        if (hit && hit.e > from) {
          if (!best || hit.s < best.s) best = { s: hit.s, e: hit.e, label: r.label };
        }
        continue;
      }
      r.re.lastIndex = 0;
      let m;
      while ((m = r.re.exec(txt))) {
        const [s, e] = r.span(m);
        if (e <= from) continue;
        if (!best || s < best.s) best = { s, e, label: r.label };
        break;
      }
    }
    return best;
  }

  const pageKey = location.origin + location.pathname;
  const st =
    window.__babelPONCT_STATE ||
    (window.__babelPONCT_STATE = { field: 0, posByKey: {}, pageKey: '' });

  if (st.pageKey !== pageKey) {
    st.field = 0;
    st.posByKey = {};
    st.pageKey = pageKey;
  }

  const all = [...document.querySelectorAll(sel)].filter(
    (el) => el && !el.disabled && !el.readOnly
  );

  if (!all.length) {
    toast('PONCT: 0 champ détecté');
    return;
  }

  st.field = (Number.isInteger(st.field) ? st.field : 0) % all.length;

  for (let tries = 0; tries < all.length; tries++) {
    const idx = (st.field + tries) % all.length;
    const el = all[idx];
    let txt = getT(el) || '';
    if (!txt || skipSegRe.test(txt)) continue;
    const k = keyOf(el, idx);
    let pos = st.posByKey[k];
    pos = Number.isInteger(pos) ? pos : 0;
    if (pos > txt.length) pos = 0;
    const hit = findNext(txt, pos);

    if (hit) {
      st.field = idx;
      st.posByKey[k] = hit.e;
      const ok = focusAndSelect(el, hit.s, hit.e);
      toast(
        'PONCT: ' +
          hit.label +
          ' → champ ' +
          (idx + 1) +
          '/' +
          all.length +
          (ok ? '' : ' (sélection impossible)')
      );
      return;
    }

    st.posByKey[k] = 0;
  }

  toast('PONCT: Fin du scan ✅');
  st.field = 0;
})();

Bookmarklet

NE / N’ V1.00

Description

Script used to browse occurrences of ne, n’ and n’ in visible segments.

Covered rules

  • Detection of the word ne.
  • Detection of n' and n’.
  • Occurrence-by-occurrence navigation.

Known limitations

  • Does not verify grammatical correctness.
  • Only detects occurrences present in visible fields.

Readable code

(function () {
  const sel = 'textarea,input[type=text],[contenteditable=true],[role=textbox]';
  const scanRe = /\bne\b|\bn[’'](?=\p{L})/iu;
  const excRe = /\bne\b|\bn[’%27](?=\p{L})/giu;

  const getT = (el) =>
    el.value != null ? el.value : (el.innerText ?? el.textContent ?? '');

  const fields = [...document.querySelectorAll(sel)].filter((el) => scanRe.test(getT(el)));

  if (!fields.length) {
    alert("✅ Aucun %27ne%27 ou %27n%27%27 / %27n’%27 trouvé.");
    return;
  }

  window.__babelNeField = Number.isInteger(window.__babelNeField)
    ? window.__babelNeField
    : 0;

  for (let tries = 0; tries < fields.length; tries++) {
    let idx = (window.__babelNeField + tries) % fields.length;
    let el = fields[idx];
    let txt = getT(el);

    if (el.__babelNeLastTxt !== txt) {
      el.__babelNeLastTxt = txt;
      el.__babelNePos = 0;
    }

    let pos = Number.isInteger(el.__babelNePos) ? el.__babelNePos : 0;
    excRe.lastIndex = pos;
    let m = excRe.exec(txt);

    if (m) {
      window.__babelNeField = idx;
      let start = m.index;
      let end = start + m[0].length;
      el.__babelNePos = end;

      try {
        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      } catch (e) {
        el.scrollIntoView(true);
      }

      try {
        el.focus();
      } catch (e) {}

      if (el.value != null && typeof el.setSelectionRange === 'function') {
        el.setSelectionRange(start, end);
      } else if (el.isContentEditable) {
        const range = document.createRange();
        const selObj = window.getSelection();
        selObj.removeAllRanges();
        const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
        let p = 0,
          found = false;

        while (walker.nextNode()) {
          const t = walker.currentNode.nodeValue || '';
          const q = p + t.length;
          if (!found && start < q) {
            const s = Math.max(0, start - p);
            const e2 = Math.min(t.length, s + (end - start));
            range.setStart(walker.currentNode, s);
            range.setEnd(walker.currentNode, e2);
            found = true;
            break;
          }
          p = q;
        }

        if (found) {
          selObj.addRange(range);
        }
      }

      return;
    } else {
      if (pos > 0) {
        el.__babelNePos = 0;
        continue;
      }
    }
  }

  alert("✅ Fin : plus de %27ne%27 / %27n%27%27 / %27n’%27 à parcourir.");
})();

Bookmarklet

Word repetitions V1.00

Description

Script for detecting immediately repeated words within a segment.

Covered rules

  • Detection of de de.
  • Detection of je je.
  • Handling of certain apostrophes.
  • Removal of invisible characters.

Known limitations

  • Does not detect distant repetitions.
  • Depends on the visible text on the page.

Readable code

(function () {
  const sel = 'textarea,input[type=text],[contenteditable=true],[role=textbox]';
  const skipSegRe = /^\s*(?:\[[^\]]*\]|<[^>]*>)\s*$/;
  const INV = /[\u200B-\u200D\u2060\uFEFF\u00AD]/g;
  const APOS_SET = new Set(['%27', '’', 'ʼ', '′', '\u2019', '\u02BC', '\u2032']);
  const WS_ONLY = /^[\s\u00A0\u202F\u2009\u200A\u200B\u2060\uFEFF\u00AD]*$/;

  function toast(msg) {
    let d = document.getElementById('__babel_rep_toast');
    if (!d) {
      d = document.createElement('div');
      d.id = '__babel_rep_toast';
      d.style.cssText =
        'position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#111;color:#fff;padding:8px 12px;border-radius:10px;font:13px/1.35 system-ui,Segoe UI,Arial;box-shadow:0 6px 18px rgba(0,0,0,.25);max-width:92vw;';
      document.documentElement.appendChild(d);
    }
    d.textContent = msg;
    d.style.display = 'block';
    clearTimeout(d.__t);
    d.__t = setTimeout(() => {
      d.style.display = 'none';
    }, 2600);
  }

  function getT(el) {
    return el.value != null ? el.value : ((el.textContent ?? el.innerText) ?? '');
  }

  function keyOf(el, idx) {
    const attrs = ['data-segment-id', 'data-segid', 'data-id', 'id', 'name', 'data-testid', 'aria-label'];
    let n = el;
    for (let k = 0; k < 8 && n; n = n.parentElement, k++) {
      for (const a of attrs) {
        const v = n.getAttribute && n.getAttribute(a);
        if (v) return a + ':' + v;
      }
    }
    return 'idx:' + idx;
  }

  function selectInInput(el, s, e) {
    try {
      el.focus();
      el.setSelectionRange(s, e);
    } catch (_) {}
  }

  function selectInCE(el, s, e) {
    try {
      el.focus();
      const tw = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
      let node,
        idx = 0,
        sn = null,
        so = 0,
        en = null,
        eo = 0;

      while ((node = tw.nextNode())) {
        const len = node.nodeValue.length;
        if (!sn && idx + len >= s) {
          sn = node;
          so = s - idx;
        }
        if (idx + len >= e) {
          en = node;
          eo = e - idx;
          break;
        }
        idx += len;
      }

      if (!sn || !en) return false;

      const r = document.createRange();
      r.setStart(sn, Math.max(0, Math.min(so, sn.nodeValue.length)));
      r.setEnd(en, Math.max(0, Math.min(eo, en.nodeValue.length)));

      const selObj = window.getSelection();
      selObj.removeAllRanges();
      selObj.addRange(r);
      return true;
    } catch (_) {
      return false;
    }
  }

  function focusAndSelect(el, s, e) {
    try {
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    } catch (_) {
      try {
        el.scrollIntoView(true);
      } catch (__) {}
    }

    if (el.value != null && typeof el.setSelectionRange === 'function') {
      selectInInput(el, s, e);
      return true;
    }

    if (el.isContentEditable) {
      return selectInCE(el, s, e);
    }

    return false;
  }

  function isLetter(ch) {
    return !!ch && ch.toLowerCase() !== ch.toUpperCase();
  }

  function isWordStart(ch) {
    const c = String(ch).replace(INV, '');
    return !!c && isLetter(c);
  }

  function nextVisibleChar(txt, i) {
    for (let j = i; j < txt.length; j++) {
      const c = String(txt[j] ?? '');
      if (!INV.test(c)) return c;
    }
    return '';
  }

  function okGap(g) {
    if (g == null) return false;
    g = String(g).replace(INV, '');
    g = g.replace(/\[[^\]]*\]/g, '');
    g = g.replace(/\{[^}]*\}/g, '');
    return WS_ONLY.test(g);
  }

  function cleanWord(w) {
    return String(w)
      .replace(INV, '')
      .replace(/%27/g, "'")
      .replace(/[’ʼ′\u2019\u02BC\u2032]/g, "'")
      .toLowerCase();
  }

  function parseWord(txt, i) {
    const s = i;
    let raw = '';
    while (i < txt.length) {
      const ch = txt[i];
      const c = String(ch).replace(INV, '');
      if (!c) {
        raw += ch;
        i++;
        continue;
      }
      if (isLetter(c)) {
        raw += ch;
        i++;
        continue;
      }
      if (APOS_SET.has(c)) {
        const nxt = nextVisibleChar(txt, i + 1);
        if (nxt && isLetter(String(nxt).replace(INV, ''))) {
          raw += ch;
          i++;
          continue;
        }
        break;
      }
      break;
    }
    return { s, e: i, raw };
  }

  function findRepeat(txt, from) {
    let i = 0,
      prev = null;
    while (i < txt.length) {
      while (i < txt.length && !isWordStart(txt[i])) i++;
      if (i >= txt.length) break;
      const cur = parseWord(txt, i);
      i = cur.e;
      const w = cleanWord(cur.raw);
      if (cur.e <= from) {
        prev = { s: cur.s, e: cur.e, w };
        continue;
      }
      if (prev) {
        const gap = txt.slice(prev.e, cur.s);
        if (okGap(gap) && prev.w && w && prev.w === w) {
          return { start: prev.s, end: cur.e };
        }
      }
      prev = { s: cur.s, e: cur.e, w };
    }
    return null;
  }

  const pageKey = location.origin + location.pathname;
  const st =
    window.__babelREP_STATE ||
    (window.__babelREP_STATE = {
      field: 0,
      posByKey: {},
      pageKey: '',
      totalFound: 0,
      lastShownIdx: null
    });

  if (st.pageKey !== pageKey) {
    st.field = 0;
    st.posByKey = {};
    st.pageKey = pageKey;
    st.totalFound = 0;
    st.lastShownIdx = null;
  }

  const all = [...document.querySelectorAll(sel)].filter(
    (el) => el && !el.disabled && !el.readOnly
  );

  if (!all.length) {
    toast('REP: 0 champ détecté');
    return;
  }

  st.field = (Number.isInteger(st.field) ? st.field : 0) % all.length;

  for (let tries = 0; tries < all.length; tries++) {
    const idx = (st.field + tries) % all.length;
    const el = all[idx];
    const txt = getT(el) || '';
    if (!txt || skipSegRe.test(txt)) continue;
    const k = keyOf(el, idx);
    let pos = st.posByKey[k];
    pos = Number.isInteger(pos) ? pos : 0;
    if (pos > txt.length) pos = 0;
    const rr = findRepeat(txt, pos);

    if (rr) {
      if (st.lastShownIdx != null && idx < st.lastShownIdx) {
        toast('REP: Fin du scan ✅ (' + st.totalFound + ' répétitions trouvées)');
        st.field = 0;
        st.totalFound = 0;
        st.lastShownIdx = null;
        return;
      }

      st.field = idx;
      st.posByKey[k] = rr.end;
      st.totalFound++;
      st.lastShownIdx = idx;
      const ok = focusAndSelect(el, rr.start, rr.end);
      toast(
        'REP: répétition détectée → champ ' +
          (idx + 1) +
          '/' +
          all.length +
          (ok ? '' : ' (sélection impossible)')
      );
      return;
    }

    st.posByKey[k] = 0;
  }

  toast('REP: Fin du scan ✅ (' + st.totalFound + ' répétitions trouvées)');
  st.field = 0;
  st.totalFound = 0;
  st.lastShownIdx = null;
})();

Bookmarklet

Copy to Scribens V1.00

Description

Script used to assemble visible segments from the page and copy them to the clipboard for analysis in Scribens.

Covered rules

  • Scans all visible text fields.
  • Ignores empty segments.
  • Assembles segments with separators.
  • Copies them to the clipboard.

Known limitations

  • Depends on browser permissions.
  • Only copies loaded segments.

Readable code

(function () {
  const sel = 'textarea,input[type=text],[contenteditable=true],[role=textbox]';
  const skipSegRe = /^\s*(?:\[[^\]]*\]|<[^>]*>)\s*$/;

  const getT = (el) =>
    el.value != null ? el.value : ((el.textContent ?? el.innerText) ?? '');

  function toast(msg) {
    let d = document.getElementById('__babel_copy_toast');
    if (!d) {
      d = document.createElement('div');
      d.id = '__babel_copy_toast';
      d.style.cssText =
        'position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#111;color:#fff;padding:8px 12px;border-radius:10px;font:13px/1.35 system-ui,Segoe UI,Arial;box-shadow:0 6px 18px rgba(0,0,0,.25);max-width:92vw;';
      document.documentElement.appendChild(d);
    }
    d.textContent = msg;
    d.style.display = 'block';
    clearTimeout(d.__t);
    d.__t = setTimeout(() => {
      d.style.display = 'none';
    }, 2600);
  }

  const fields = [...document.querySelectorAll(sel)].filter(
    (el) => el && !el.disabled && !el.readOnly
  );

  if (!fields.length) {
    toast('SCRI: 0 champ détecté');
    return;
  }

  const parts = [];
  let kept = 0;

  for (let i = 0; i < fields.length; i++) {
    const txt = (getT(fields[i]) || '').trim();
    if (!txt || skipSegRe.test(txt)) continue;
    kept++;
    parts.push('### Segment ' + kept + ' / champ ' + (i + 1) + '/' + fields.length + '\n' + txt);
  }

  const out = parts.join('\n\n---\n\n');

  if (!out) {
    toast('SCRI: rien à copier (segments vides/ignorés)');
    return;
  }

  const ok = async () => {
    try {
      await navigator.clipboard.writeText(out);
      toast('SCRI: copié ✅ (' + kept + ' segments)');
    } catch (e) {
      try {
        const ta = document.createElement('textarea');
        ta.value = out;
        ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;';
        document.body.appendChild(ta);
        ta.focus();
        ta.select();
        document.execCommand('copy');
        ta.remove();
        toast('SCRI: copié ✅ (' + kept + ' segments)');
      } catch (e2) {
        prompt('Copie manuelle (Ctrl+C) :', out);
      }
    }
  };

  ok();
})();

Bookmarklet

Zoom x1.8 V1.00

Description

Script that automatically positions the audio zoom cursor at the level corresponding to x1.8.

Covered rules

  • Finds the first visible slider.
  • Simulates user interaction on the zoom track.
  • Sets the zoom ratio to 0.18.

Known limitations

  • Depends on Babel’s DOM structure.
  • Assumes the first [role="slider"] element corresponds to the zoom control.

Readable code

(function () {
  const thumb = document.querySelectorAll('[role="slider"]')[0];
  const track = thumb.parentElement.parentElement;
  const r = track.getBoundingClientRect();
  const ratio = 0.18;
  const x = r.left + r.width * ratio;
  const y = r.top + r.height / 2;

  track.dispatchEvent(new PointerEvent('pointerdown', {
    bubbles: true,
    clientX: x,
    clientY: y,
    pointerId: 1,
    pointerType: 'mouse',
    isPrimary: true
  }));

  track.dispatchEvent(new PointerEvent('pointermove', {
    bubbles: true,
    clientX: x,
    clientY: y,
    pointerId: 1,
    pointerType: 'mouse',
    isPrimary: true
  }));

  track.dispatchEvent(new PointerEvent('pointerup', {
    bubbles: true,
    clientX: x,
    clientY: y,
    pointerId: 1,
    pointerType: 'mouse',
    isPrimary: true
  }));
})();

Bookmarklet

Zoom x10 V1.00

Description

Script that automatically positions the audio zoom cursor at the maximum level corresponding to x10.

Covered rules

  • Finds the first visible slider.
  • Simulates user interaction on the zoom track.
  • Sets the zoom ratio to 1.00.

Known limitations

  • Depends on Babel’s DOM structure.
  • Assumes the first [role="slider"] element corresponds to the zoom control.

Readable code

(function () {
  const thumb = document.querySelectorAll('[role="slider"]')[0];
  const track = thumb.parentElement.parentElement;
  const r = track.getBoundingClientRect();
  const ratio = 1.00;
  const x = r.left + r.width * ratio;
  const y = r.top + r.height / 2;

  track.dispatchEvent(new PointerEvent('pointerdown', {
    bubbles: true,
    clientX: x,
    clientY: y,
    pointerId: 1,
    pointerType: 'mouse',
    isPrimary: true
  }));

  track.dispatchEvent(new PointerEvent('pointermove', {
    bubbles: true,
    clientX: x,
    clientY: y,
    pointerId: 1,
    pointerType: 'mouse',
    isPrimary: true
  }));

  track.dispatchEvent(new PointerEvent('pointerup', {
    bubbles: true,
    clientX: x,
    clientY: y,
    pointerId: 1,
    pointerType: 'mouse',
    isPrimary: true
  }));
})();

Bookmarklet

Heu without comma V1.00

Description

Script used to browse occurrences of heu or Heu when they are not followed by expected punctuation.

Covered rules

  • Detection of heu and Heu.
  • Ignores cases already followed by , . ? ! -.
  • Occurrence-by-occurrence navigation.

Known limitations

  • Does not assess stylistic context.
  • Only works on visible fields.

Readable code

(function () {
  const sel = 'textarea,input[type=text],[contenteditable=true],[role=textbox]';
  const scanRe = /\b(?:heu|Heu)\b(?!\s*[,.\?!-])/;
  const excRe = /\b(?:heu|Heu)\b(?!\s*[,.\?!-])/g;

  const getT = (el) =>
    el.value != null ? el.value : (el.innerText ?? el.textContent ?? '');

  const fields = [...document.querySelectorAll(sel)].filter((el) => scanRe.test(getT(el)));

  if (!fields.length) {
    alert("✅ Aucun 'heu' sans virgule trouvé.");
    return;
  }

  window.__babelHeuField = Number.isInteger(window.__babelHeuField)
    ? window.__babelHeuField
    : 0;

  for (let tries = 0; tries < fields.length; tries++) {
    let idx = (window.__babelHeuField + tries) % fields.length;
    let el = fields[idx];
    let txt = getT(el);

    if (el.__babelHeuLastTxt !== txt) {
      el.__babelHeuLastTxt = txt;
      el.__babelHeuPos = 0;
    }

    let pos = Number.isInteger(el.__babelHeuPos) ? el.__babelHeuPos : 0;
    excRe.lastIndex = pos;
    let m = excRe.exec(txt);

    if (m) {
      window.__babelHeuField = idx;
      let start = m.index;
      let end = start + m[0].length;
      el.__babelHeuPos = end;

      try {
        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      } catch (e) {
        el.scrollIntoView(true);
      }

      try {
        el.focus();
      } catch (e) {}

      if (el.value != null && typeof el.setSelectionRange === 'function') {
        el.setSelectionRange(start, end);
      } else if (el.isContentEditable) {
        const range = document.createRange();
        const selObj = window.getSelection();
        selObj.removeAllRanges();
        const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
        let p = 0,
          found = false;

        while (walker.nextNode()) {
          const t = walker.currentNode.nodeValue || '';
          const q = p + t.length;
          if (!found && start < q) {
            const s = Math.max(0, start - p);
            const e2 = Math.min(t.length, s + (end - start));
            range.setStart(walker.currentNode, s);
            range.setEnd(walker.currentNode, e2);
            found = true;
            break;
          }
          p = q;
        }

        if (found) {
          selObj.addRange(range);
        }
      }

      return;
    } else {
      if (pos > 0) {
        el.__babelHeuPos = 0;
        continue;
      }
    }
  }

  alert("✅ Fin : plus de 'heu' sans virgule à parcourir.");
})();

Bookmarklet