特殊文字をLaTeXコマンドに変換するWebアプリをReact + Material UIで作ってみた

# はじめに
LaTeXで文章を書いていると、しばしば特殊文字（ラテン文字等）の文字化けが発生します。例えば、コピペしたBibTeXにヨーロッパ圏の著者がいたとか。一番安直な解決方法は[特殊文字をLaTeXコマンドに変換](https://en.wikibooks.org/wiki/LaTeX/Special_Characters)することです。そこで、特殊文字をLaTeXコマンドへ変換するWebアプリ（[DeepL](https://www.deepl.com/ja/translator)や[Shaper](https://dream-exp.net/shaper/)風）をReactとMaterial UIで作成してみました。



# 実装
今回は「[新しい React アプリを作る](https://ja.reactjs.org/docs/create-a-new-react-app.html)」の「[Create React App](https://ja.reactjs.org/docs/create-a-new-react-app.html#create-react-app)」を参考にして、シングルページアプリケーションを作成していきます。完成版は以下の通りです。
![webapp.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/344018/b07b35b7-22ad-0353-9b78-d53b531a4a78.png)

## 開発環境
「[Create React App](https://ja.reactjs.org/docs/create-a-new-react-app.html#create-react-app)」に
> あなたのマシンに Node >= 14.0.0 及び npm >= 5.6 の環境が必要です。

と記載してあったので、nodeとnpmのバージョンを確認しておきます。

```
$ node -v
v16.8.0
$ npm -v
7.21.0
```

## 雛形の作成

プロジェクトを作成します。

```
npx create-react-app special-character-to-latex-cmd-app
```

今回は[Material UI](https://mui.com/)のコンポーネントを使用するため、そのインストールを行います。

```
cd special-character-to-latex-cmd-app
npm install @mui/material @emotion/react @emotion/styled
```

続いて、`src`下に`components`ディレクトリを作成し、その下に`App.js`、`Form.js`、`LaTeXCmdTable.js`を作成します。（ディレクトリ構造は「[Example Projects](https://ja.reactjs.org/community/examples.html)」の「[Calculator](https://github.com/ahfarmer/calculator)」を参考にしています。）

```
src/
├── components
│   ├── App.js
│   ├── App.test.js
│   ├── Form.css
│   ├── Form.js
│   └── LatexCmdTable.js
├── index.css
├── index.js
├── latex-cmd.json
├── reportWebVitals.js
└── setupTests.js
```

`Form.js`では特殊文字をLaTeXコマンドへ変換するためのテキストフォームを提供するコンポーネントを、`LaTeXCmdTable.js`では特殊文字とLaTeXコマンドの対応表を表示するためのコンポーネントを定義しています。`App.js`では`Form.js`と`LaTeXCmdTable.js`をまとめる親コンポーネントを定義します。

`latex-cmd.json`には特殊文字とLaTeXコマンドの対応関係が記述されています。

```latex-cmd（一部抜粋）.json
{
    "à": "{\\` a}",
    "è": "{\\` e}",
    "ì": "{\\` i}",
    "ò": "{\\` o}",
    "ù": "{\\` u}",
    "À": "{\\` A}",
    "È": "{\\` E}",
}
```

続いて、編集が必要な各ファイルについて説明します。

## index.js
`index.js`はReactアプリを実行する際、一番最初に呼び出されるファイルです。`ReactDOM.render`メソッドを用いて、`<App>`コンポーネントを`index.html`の`id="root"`へ出力しています。
ちなみに、`<React.StrictMode>`は子孫要素に対して付加的な検査をしたり、警告をするための機能だそうです。（参考：[strict モード](https://ja.reactjs.org/docs/strict-mode.html)）

```src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

```index(一部抜粋).html
<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
</body>
```

## App.js
`App.js`では、`<Form>`と`<LatexCmdTable>`の2つのコンポーネントを配置し、その間に`<hr>`を用いて線を引きます。

```src/components/App.js
import React from 'react';
import Form from './Form';
import LatexCmdTable from './LatexCmdTable';

function App() {
  return (
    <div>
      <Form />
      <hr size="1" wifth="100%" color="gray"></hr>
      <LatexCmdTable />
    </div>
  );
}

export default App;
```


## Form.js
`Form.js`では特殊文字をLaTeXコマンドへ変換するためのテキストフォームを作成します。見た目をかっこよくするために、ここでは[Material UI](https://mui.com/)が提供する[TextFieldコンポーネント](https://mui.com/api/text-field/)を使用します。

```react
import TextField from '@mui/material/TextField';
```

`<Form>`は関数コンポーネントを用いて定義します。LaTeXコマンドに変換された出力テキスト（`outText`）は`useState`フックを用いて関数内にstate変数として宣言します。

```react
function Form() {
  const [outText, setOutText] = useState("");
   ...
}
export default Form;
```

`<TextField>`コンポーネントは入力フィールド用（`name="input text"`）と出力フィールド用（`name="output text"`）の2つ配置します。入力フィールドで入力値が変化したとき、コールバック関数`handleChange`が呼び出されます。

```react
<TextField 
  name="input field"
  onChange={handleChange}
/>
```

`handleChange`では、入力フィールド内の特殊文字をLaTeXコマンドに変換します。コールバック関数内では、更新された入力値は`event.target.value (string)`からアクセスできます。
replaceメソッドを用いて`event.target.value`に含まれる特殊文字をLaTeXコマンドへ変換します。この時マッチした文字をすべて変換したいので、正規表現を用いています。（`RegExp`は正規表現オブジェクトです。）
最後に、`setOutText`メソッドを用いて、変換後の値を`outText`にセットします。

```react
const handleChange = (event) => {
  tmpText = event.target.value;
  Object.keys(latexCmdJson).forEach(function (key) {
    tmpText = tmpText.replace(RegExp(key, 'g'), latexCmdJson[key]);
  });
  setOutText(tmpText);
}
```

`outText`の値は、出力フィールド（`name="output text"`）の入力要素（`value`）にセットされます。また、`InputProps`から`<input>`の属性を用いて編集不可に設定します。

```react
<TextField 
  name="output field"
  value={outText} 
  InputProps={{
    readOnly: true,
  }}
/>
```

全体のコード

```src/components/Form.js
import React, { useState } from 'react';
import TextField from '@mui/material/TextField';
import './Form.css';
import latexCmdJson from '../latex-cmd.json';


function Form() {
  const [outText, setOutText] = useState("");
  let tmpText = "";

  const handleChange = (event) => {
    tmpText = event.target.value;
    Object.keys(latexCmdJson).forEach(function (key) {
      tmpText = tmpText.replace(RegExp(key, 'g'), latexCmdJson[key]);
    });
    setOutText(tmpText);
  }

  return (
    <div>
      <div>
        <TextField 
          multiline 
          focused 
          className="textfield-large" 
          label="LaTeX (including special characters)"
          name="input field"
          variant="filled"
          rows={6}
          margin="normal"
          onChange={handleChange}/>
      </div>
      <div>
        <TextField 
          multiline 
          focused 
          className="textfield-large" 
          label="LaTeX (using escaped codes)"
          name="output field" 
          value={outText} 
          variant="filled"
          rows={6}
          margin="normal"
          InputProps={{
            readOnly: true,
          }}/>
      </div>
    </div>
  );
}

export default Form;
```


```src/components/Form.css
.textfield-large {
  width: 100%;
}
```



## LaTeXCmdTable.js
`LaTeXCmdTable.js`では特殊文字とLaTeXコマンドの対応表を表示するためのコンポーネントを定義します。表はMaterial UIの「[Basic table](https://mui.com/components/tables/#basic-table)」を参考に作成しました。

```src/components/LaTeXCmdTable.js
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import latexCmdJson from '../latex-cmd.json';


function createData(key, value){
  return { key, value };
};

let rows = [];

Object.keys(latexCmdJson).forEach(function (key) {
  rows.push(createData(key, latexCmdJson[key]));
});


function LatexCmdTable() {
  return (
    <div>
      <TableContainer component={Paper}>
        <Table sx={{ minWidth: 400 }} aria-label="simple table"s>
          <TableHead>
            <TableRow>
              <TableCell>Special characters</TableCell>
              <TableCell>LaTeX commands</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {rows.map((row) => (
              <TableRow key={row.key}>
                <TableCell>{row.key}</TableCell>
                <TableCell>{row.value}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
    </div>
  );
}
export default LatexCmdTable;
```

# 完成品
https://github.com/yakiimo121/special-character-to-latex-cmd-app

実行方法

```
git clone https://github.com/yakiimo121/special-character-to-latex-cmd-app
cd special-character-to-latex-cmd-app
npm install
npm start
```

# 参考
- [LaTeX/Special Characters](https://en.wikibooks.org/wiki/LaTeX/Special_Characters)
- [DeepL](https://www.deepl.com/ja/translator)
- [Shaper](https://dream-exp.net/shaper/)
- [新しい React アプリを作る](https://ja.reactjs.org/docs/create-a-new-react-app.html)
- [React初心者の実行の流れに関するメモ書き](https://qiita.com/take9021/items/c363d15bd5fdc3b0109c)
- [strict モード](https://ja.reactjs.org/docs/strict-mode.html)
- [Reactでクラスコンポーネントより関数コンポーネントを使うべき理由5選](https://tyotto-good.com/blog/reaseons-to-use-function-component)
- [コンポーネントの state](https://ja.reactjs.org/docs/faq-state.html)
- [コンポーネントと props](https://ja.reactjs.org/docs/components-and-props.html)
- [ステートフックの利用法](https://ja.reactjs.org/docs/hooks-state.html)
- [Material UI](https://mui.com/)
- [TextField API](https://mui.com/api/text-field/)
- [正規表現](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions)