Greasy Fork is available in English.

* Reading Assist R (読書アシスト)

大日本印刷(DNP)と日本ユニシスが開発した「読書アシスト」をの手法を、手軽に試してみよう。

// ==UserScript==
// @name        * Reading Assist R (読書アシスト)
// @namespace   knoa.jp
// @description 大日本印刷(DNP)と日本ユニシスが開発した「読書アシスト」をの手法を、手軽に試してみよう。
// @include     *
// @version     1.0.2
// @grant       none
// ==/UserScript==

/*
[memo]
*/
(function(){
  const KEY = 'r';/*起動キー*/
  const Y   = .1;/*斜め字下げ(em)*/
  const GAP = .1;/*斜め字下げ横間隔(em)*/
  const X   = 1;/*インデント(em)*/
  const MAXINDENT = 8;/*最大インデント回数*/
  const RE = /.+?([、。!?!?\s]+|\p{sc=Hiragana}(?=\p{sc=Katakana})|\p{sc=Hiragana}(?=\p{sc=Han})|[^\w,\.](?=[\w,\.(「―])|$)/u;/*句読点、ひらがなの終わり、記号の始まりを検出して文節とみなす*/
  window.addEventListener('keydown', function(e){
    if(['input', 'textarea'].includes(e.target.localName) || e.target.contentEditable === 'true') return;
    if(e.key === KEY && !e.metaKey && !e.altKey && !e.shiftKey && !e.ctrlKey){
      /* 対象要素 */
      /* brを含むdivを分割して複数のpにする */
      Array.from(document.querySelectorAll('div')).filter(div => Array.from(div.children).some(c => c.localName === 'br')).forEach(div => {
        let ps = [];
        Array.from(div.childNodes).forEach((n, i) => {
          if(i === 0 || n.localName === 'br'){
            ps.push(document.createElement('p'));
            if(n.localName === 'br') div.replaceChild(ps[ps.length- 1], n);/*i===0かつbrに備えて先に判定*/
            else div.insertBefore(ps[ps.length- 1], n);
          }else{
            ps[ps.length - 1].appendChild(n);
          }
        });
      });
      let targets = [
        ...Array.from(document.querySelectorAll('p')),
      ];
      /* 斜め字下げ */
      const flow = function(n, i){
        n.style.display = 'inline-block';
        n.style.transform = `translateY(${i*Y}em)`;
        n.style.marginRight = `${GAP}em`;
      };
      const split = function(n, i){
        if(n.nodeType === Node.TEXT_NODE){
          n.data = n.data.trim();
          let pos = n.data.search(RE);
          if(pos !== -1){
            let rest = n.splitText(RegExp.lastMatch.length);/*ターゲットであるnと続くrestに分割*/
            /* この時点でn(処理済み),target(新規テキスト),rest(次に処理)の3つに分割されている */
            let span = document.createElement('span');
            flow(span, i)
            /* 直前のrubyは1要素として吸収する */
            while(n.previousElementSibling && n.previousElementSibling.localName === 'ruby') span.appendChild(n.previousElementSibling);
            span.appendChild(n);/*textNode*/
            rest.parentNode.insertBefore(span, rest);
            return split(rest, ++i);
          }else{
            if(n.nextSibling) return split(n.nextSibling, i);
          }
        }
        else if(n.localName === 'ruby'){
          if(n.nextSibling) return split(n.nextSibling, i);
        }
        /* もともと含まれる a や span など */
        else{
          flow(n, i);
          if(n.nextSibling) return split(n.nextSibling, ++i);
        }
      };
      const getMarginBottom = (e) => parseFloat(getComputedStyle(e).marginBottom);
      const getTranslateY = (e) => parseFloat((getComputedStyle(e).transform.match(/[0-9.]+/g) || [0,0,0,0,0,0])[5]);
      targets.forEach(p => {
        if(p.firstChild) split(p.firstChild, 0);/*回しながらchildNodesは増えていく*/
        if(p.children.length === 0) return;
        p.dataset.originalMarginBottom = p.dataset.originalMarginBottom || getMarginBottom(p);
        p.style.marginBottom = parseFloat(p.dataset.originalMarginBottom) + getTranslateY(p.lastElementChild) + 'px';
      });
      /* インデント */
      targets.forEach(p => {
        /* 複数回起動に備えてリセット */
        for(let i = 1; p.children[i]; i++){
          p.children[i].style.marginLeft = `0em`;
        }
        let x = [0], breaks = 0;
        for(let i = 1; p.children[i]; i++){
          setTimeout(function(){
            x[i] = p.children[i].getBoundingClientRect().x;
            if(x[i-1] < x[i]) return;
            if(MAXINDENT <= ++breaks) breaks = 0;
            p.children[i].style.marginLeft = `${breaks * X}em`;
          }, i);
        }
      });
    }
  });
})();