HTMLで和文と欧文の間にスペース（アキ）を自動で挿入する

和文と欧文が混じった文章において、読みやすさのため少し文字間のスペース（アキ）を入れたいときがあります。LaTeXなど組版に特化した環境ではそのような機能がサポートされていますが、HTMLでは今のところ標準ではサポートされていません。ただし、半角スペースを直接入力すると

- 入れるか入れないかの一貫性をとりにくい
- 幅の調整がしにくい

といった問題があります。そこでJSとCSSで自動的に挿入されるようにします。

以下のコードは中国語向けのこちらを参考にしています。

https://github.com/mastermay/text-autospace.js

```typescript:main.ts
import * as findAndReplaceDOMText  from 'findandreplacedomtext';

enum Space {
  Quarter = 'quarter',
  Half = 'half'
}

class TextAutospace {

  static readonly ELEMENT_NODE = 1;

  quarterPatterns: Array<string>;
  halfPatterns: Array<string>;

  constructor() {
    const hiragana = '\u3040-\u309F～';
    const katakana = '\u30A0-\u30FF';
    const kanji = '\u2E80-\u2FFF\u31C0-\u31EF\u3300-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFE30-\uFE4F';

    const punct: string = '[—@&=_\,\.\?\!\$\%\^\*\-\+\/]';
    const left: string = `[\\[({'"<«‘“]`;
    const right: string = `[\\])}'">»’”]`;

    const cjk = '[' +  [hiragana, katakana, kanji].join('') + ']';
    const latin = '[A-Za-z0-9\u00C0-\u00FF\u0100-\u017F\u0180-\u024F\u1E00-\u1EFF]' + '|' + punct;

    this.quarterPatterns = [
      '(' + cjk + ')(' + latin + '|' + left + ')',
      '(' + latin + '|' + right + ')(' + cjk + ')',
      '(・)(' + cjk + ')',
      '(' + latin + '|' + cjk + ')([・])',
      '([：])([『])',
      '(' + cjk + ')([：])'
    ];
    this.halfPatterns = [
      '([、。，．）」』])(' + left + '|' + latin + '|' + cjk + ')',
      '(' + latin + '|' + cjk + ')([（「『])'
    ];

  }
  transform(element: Node): void {
    [Space.Quarter, Space.Half].forEach(space => {
      let patterns = (space == Space.Quarter) ?  this.quarterPatterns : this.halfPatterns;
      patterns.forEach(pattern=> {
        findAndReplaceDOMText(element, {
          find: new RegExp(pattern, 'ig'),
          replace: '$1<' + `${space}` + '>$2'
        })
      });

      findAndReplaceDOMText(element, {
        find: `<${space}>`,
        replace: () => {
          let newElement = document.createElement('span');
          newElement.className = space;
          return newElement;
        }
      });

      // Required to keep space before <code></code>
      document.querySelectorAll(`* > span.${space}:first-child`).forEach(child => {
        let p = child.parentNode;
        if (p.firstChild.nodeType == TextAutospace.ELEMENT_NODE) {
          let newElement = document.createElement('span');
          newElement.className = space;
          p.parentNode.insertBefore(newElement, p);
          p.querySelector(`span.${space}:first-child`).remove();
        }
      });

      element.normalize();

    });
  }
}

document.addEventListener('DOMContentLoaded', event => {
  document.body.querySelectorAll('p, li, h1, h2, h3, h4, h5').forEach(element => {
    let t = new TextAutospace();
    t.transform(element);
  });

});

```

```scss:main.scss
$jp-font: 'source-han-sans-cjk-ja';
$western-font: 'Roboto';

body {
  -webkit-font-smoothing: antialiased;
  font-family: $western-font, $jp-font, sans-serif;
  font-feature-settings: 'palt';
  font-weight: 400;
}

.quarter {
  &::after {
    content: ' ';
    display: inline;
    font-family: 'source-han-sans-cjk-ja';
    letter-spacing: -0.1rem;
  }
}

.half {
  &::after {
    content: ' ';
    display: inline;
    font-family: 'source-han-sans-cjk-ja';
  }
}

code {
  .quarter,
  .half {
    display: none;
  }
}

pre {
  .quarter,
  .half {
    display: none;
  }
}

kbd {
  .quarter,
  .half {
    display: none;
  }
}

samp {
  .quarter,
  .half {
    display: none;
  }
}

ol > .quarter,
ul > .quarter,
ul > .half,
ul > .half {
  display: none;
}
```