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

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

/*
[update]
「小説家になろう」など、一部のサイトで動作しなかった問題を修正しました。

[memo]
赤月ゆにちゃんの指摘など
https://twitter.com/kantankikaku/status/1310747945730322433

横幅75%を超えて句読点があれば積極的に改行していくとか
  br追加?ってわけにはいかない気が。
  margin-right追加して改行させる?
*/
(function(){
  const KEY = 'r';/*起動キー*/
  const Y   = 0.1;/*斜め字下げ(em)*/
  const GAP = 0.1;/*斜め字下げ横間隔(em)*/
  const X   = 1.0;/*インデント(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.isContentEditable) 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.before(span);
            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 if(n.nodeType === Node.ELEMENT_NODE){
          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);
        }
      });
    }
  });
})();