[習作] 数式をSVGに変換するVSCode拡張機能を作った

# [習作] 数式をSVGに変換するVSCode拡張機能を作った

## はじめに

* ふと VSCode の拡張機能を作ってみたくなった
* そういえば先日、pandoc のマニュアルを見ていたら、数式（latex）をSVGやPNGに変換するサービスが書かれていた
  * 以下の `--webtex` のあたりに記載があります

https://www.pandoc.org/MANUAL.html#math-rendering-in-html

* とりあえず、数式をSVGに変換する拡張機能を作ってみた
  * HTML や Markdown に埋め込めるかな？　と思っています
  * 内容的には Hello world レベルです
* httpsで通信するけど proxy には対応していない
* サーバに負担がかかるような使い方は厳禁

## 環境構築

細かい手順は省略します。

とりあえず、すでに node / npm は入っていたので念のためアップグレードして、それ以外に必要なものをインストールします。

```shell-session
$ sudo apt update
$ sudo apt upgrade
$ sudo npm install -g yo generator-code
$ sudo npm install -g vsce
```
適当なディレクトリにプロジェクトを作成します（実行するとプロジェクト用のディレクトリが新しくできるので、それを考慮して決定）。

```shell-session
$ yo code
```

いろいろ聞かれますがとりあえず `New Extension (TypeScript) ` を選択して、プロジェクト名（拡張機能名）を `Math2Svg` といれて、後はデフォルトのままで進めて、しばらく待つと、カレントディレクトリにプロジェクト名（小文字になっているので、識別名？）で新しいディレクトリができました。
フォルト選択のままで git の初期化もしてくれました。

## 実装

VSCode の API のドキュメントや、その他のサイトを参考に実装します。

https://code.visualstudio.com/api

もしかすると https 通信は便利な機能があるのかもしれないですが、わからなかったので node.js の https を直接使用します。

```ts:src/extension.ts
import * as vscode from 'vscode';
import * as https from 'https';

// convert latex -> svg
const url = "https://latex.codecogs.com/svg.latex?";
const convertLatex2Svg = (text: string) => {
    return new Promise<string>((resolve, reject) => {
        https.get(url + encodeURIComponent(text), (res) => {
            if (res.statusCode === 200) {
                res.on('data', (d: Buffer) => resolve(d.toString()));
            } else {
                reject(new Error(`${res.statusCode} ${res.statusMessage}`));
            }
        }).on('error', (e) => reject(e));
    });
};

export function activate(context: vscode.ExtensionContext) {
    console.log('"math2svg" is now active.');
    const disposable = vscode.commands.registerCommand('math2svg.convert', async () => {
        const editor = vscode.window.activeTextEditor;
        if (!editor) { return; }
        const selection = editor.selection;
        if (selection.isEmpty) { return; }
        const text = editor.document.getText(selection);
        if (text.charAt(0) !== '$' || text.charAt(text.length - 1) !== '$') {
            return;
        }

        const mathString = text.substring(1, text.length - 1);
        try {
            const svgString = await convertLatex2Svg(mathString);
            await editor.edit((edit) => edit.replace(selection, svgString));
        } catch (e) {
            console.error(e);
            await vscode.window.showErrorMessage(`Math2Svg: ${e.name} ${e.message}`);
        }
    });
    context.subscriptions.push(disposable);
}

export function deactivate() { }
```

あとは `package.json` の `activationEvents` と `contributes` を変更しました（それ以外は省略）。

```json:package.json（一部）
{
    "activationEvents": [
        "onCommand:math2svg.convert"
    ],
    "main": "./out/extension.js",
    "contributes": {
        "commands": [
            {
                "command": "math2svg.convert",
                "title": "Math to SVG"
            }
        ],
        "menus": {
            "editor/context": [
                {
                    "when": "editorHasSelection && !editorReadonly && !editorHasMultipleSelections",
                    "command": "math2svg.convert",
                    "group": "z_commands"
                }
            ]
        }
    }
}
```
あとは、デバッグモードで動かして（ `F5` で実行）、動作確認をします。

```html:test.html
<html>
<head>
<title>テスト</title>
</head>
<body>

これは
$sin(x)$
です</body>
</html>
```

ここの `$sin(x)$` を選択して、右クリック、コンテキストメニューから `Math to SVG` を選択すると、数秒後に SVG に置換されました。

```html:test.html
<html>
<head>
<title>テスト</title>
</head>
<body>

これは
<?xml version='1.0' encoding='UTF-8'?>
<!-- Generated by CodeCogs with dvisvgm 2.9.1 -->
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='3em' height='1em' viewBox='-.239051 -.240635 35.11002 13.522849'>
<defs>
<path id='g1-40' d='M3.88543 2.905106C3.88543 2.86924 3.88543 2.84533 3.682192 2.642092C2.486675 1.43462 1.817186-.537983 1.817186-2.976837C1.817186-5.296139 2.379078-7.292653 3.765878-8.703362C3.88543-8.810959 3.88543-8.834869 3.88543-8.870735C3.88543-8.942466 3.825654-8.966376 3.777833-8.966376C3.622416-8.966376 2.642092-8.105604 2.056289-6.933998C1.446575-5.726526 1.171606-4.447323 1.171606-2.976837C1.171606-1.912827 1.338979-.490162 1.960648 .789041C2.666002 2.223661 3.646326 3.000747 3.777833 3.000747C3.825654 3.000747 3.88543 2.976837 3.88543 2.905106Z'/>
<path id='g1-41' d='M3.371357-2.976837C3.371357-3.88543 3.251806-5.36787 2.582316-6.75467C1.876961-8.18929 .896638-8.966376 .765131-8.966376C.71731-8.966376 .657534-8.942466 .657534-8.870735C.657534-8.834869 .657534-8.810959 .860772-8.607721C2.056289-7.400249 2.725778-5.427646 2.725778-2.988792C2.725778-.669489 2.163885 1.327024 .777086 2.737733C.657534 2.84533 .657534 2.86924 .657534 2.905106C.657534 2.976837 .71731 3.000747 .765131 3.000747C.920548 3.000747 1.900872 2.139975 2.486675 .968369C3.096389-.251059 3.371357-1.542217 3.371357-2.976837Z'/>
<path id='g0-105' d='M3.383313-1.709589C3.383313-1.769365 3.335492-1.817186 3.263761-1.817186C3.156164-1.817186 3.144209-1.78132 3.084433-1.578082C2.773599-.490162 2.283437-.119552 1.888917-.119552C1.745455-.119552 1.578082-.155417 1.578082-.514072C1.578082-.836862 1.721544-1.195517 1.853051-1.554172L2.689913-3.777833C2.725778-3.873474 2.809465-4.088667 2.809465-4.315816C2.809465-4.817933 2.450809-5.272229 1.865006-5.272229C.765131-5.272229 .32279-3.53873 .32279-3.443088C.32279-3.395268 .37061-3.335492 .454296-3.335492C.561893-3.335492 .573848-3.383313 .621669-3.550685C.908593-4.554919 1.362889-5.033126 1.829141-5.033126C1.936737-5.033126 2.139975-5.021171 2.139975-4.638605C2.139975-4.327771 1.984558-3.93325 1.888917-3.670237L1.052055-1.446575C.980324-1.255293 .908593-1.06401 .908593-.848817C.908593-.310834 1.279203 .119552 1.853051 .119552C2.952927 .119552 3.383313-1.625903 3.383313-1.709589ZM3.287671-7.460025C3.287671-7.639352 3.144209-7.854545 2.881196-7.854545C2.606227-7.854545 2.295392-7.591532 2.295392-7.280697C2.295392-6.981818 2.546451-6.886177 2.689913-6.886177C3.012702-6.886177 3.287671-7.197011 3.287671-7.460025Z'/>
<path id='g0-110' d='M2.462765-3.502864C2.486675-3.574595 2.785554-4.172354 3.227895-4.554919C3.53873-4.841843 3.945205-5.033126 4.411457-5.033126C4.889664-5.033126 5.057036-4.674471 5.057036-4.196264C5.057036-3.514819 4.566874-2.15193 4.327771-1.506351C4.220174-1.219427 4.160399-1.06401 4.160399-.848817C4.160399-.310834 4.531009 .119552 5.104857 .119552C6.216687 .119552 6.635118-1.637858 6.635118-1.709589C6.635118-1.769365 6.587298-1.817186 6.515567-1.817186C6.40797-1.817186 6.396015-1.78132 6.336239-1.578082C6.06127-.597758 5.606974-.119552 5.140722-.119552C5.021171-.119552 4.829888-.131507 4.829888-.514072C4.829888-.812951 4.961395-1.171606 5.033126-1.338979C5.272229-1.996513 5.774346-3.335492 5.774346-4.016936C5.774346-4.734247 5.355915-5.272229 4.447323-5.272229C3.383313-5.272229 2.82142-4.519054 2.606227-4.220174C2.570361-4.901619 2.080199-5.272229 1.554172-5.272229C1.171606-5.272229 .908593-5.045081 .705355-4.638605C.490162-4.208219 .32279-3.490909 .32279-3.443088S.37061-3.335492 .454296-3.335492C.549938-3.335492 .561893-3.347447 .633624-3.622416C.824907-4.351681 1.0401-5.033126 1.518306-5.033126C1.793275-5.033126 1.888917-4.841843 1.888917-4.483188C1.888917-4.220174 1.769365-3.753923 1.685679-3.383313L1.350934-2.092154C1.303113-1.865006 1.171606-1.327024 1.111831-1.111831C1.028144-.800996 .896638-.239103 .896638-.179328C.896638-.011955 1.028144 .119552 1.207472 .119552C1.350934 .119552 1.518306 .047821 1.613948-.131507C1.637858-.191283 1.745455-.609714 1.80523-.848817L2.068244-1.924782L2.462765-3.502864Z'/>
<path id='g0-115' d='M2.725778-2.391034C2.929016-2.355168 3.251806-2.283437 3.323537-2.271482C3.478954-2.223661 4.016936-2.032379 4.016936-1.458531C4.016936-1.08792 3.682192-.119552 2.295392-.119552C2.044334-.119552 1.147696-.155417 .908593-.812951C1.3868-.753176 1.625903-1.123786 1.625903-1.3868C1.625903-1.637858 1.458531-1.769365 1.219427-1.769365C.956413-1.769365 .609714-1.566127 .609714-1.028144C.609714-.32279 1.327024 .119552 2.283437 .119552C4.100623 .119552 4.638605-1.219427 4.638605-1.841096C4.638605-2.020423 4.638605-2.355168 4.25604-2.737733C3.957161-3.024658 3.670237-3.084433 3.024658-3.21594C2.701868-3.287671 2.187796-3.395268 2.187796-3.93325C2.187796-4.172354 2.402989-5.033126 3.53873-5.033126C4.040847-5.033126 4.531009-4.841843 4.65056-4.411457C4.124533-4.411457 4.100623-3.957161 4.100623-3.945205C4.100623-3.694147 4.327771-3.622416 4.435367-3.622416C4.60274-3.622416 4.937484-3.753923 4.937484-4.25604S4.483188-5.272229 3.550685-5.272229C1.984558-5.272229 1.566127-4.040847 1.566127-3.550685C1.566127-2.642092 2.450809-2.450809 2.725778-2.391034Z'/>
<path id='g0-120' d='M5.66675-4.877709C5.284184-4.805978 5.140722-4.519054 5.140722-4.291905C5.140722-4.004981 5.36787-3.90934 5.535243-3.90934C5.893898-3.90934 6.144956-4.220174 6.144956-4.542964C6.144956-5.045081 5.571108-5.272229 5.068991-5.272229C4.339726-5.272229 3.93325-4.554919 3.825654-4.327771C3.550685-5.224408 2.809465-5.272229 2.594271-5.272229C1.374844-5.272229 .729265-3.706102 .729265-3.443088C.729265-3.395268 .777086-3.335492 .860772-3.335492C.956413-3.335492 .980324-3.407223 1.004234-3.455044C1.41071-4.782067 2.211706-5.033126 2.558406-5.033126C3.096389-5.033126 3.203985-4.531009 3.203985-4.244085C3.203985-3.981071 3.132254-3.706102 2.988792-3.132254L2.582316-1.494396C2.402989-.777086 2.056289-.119552 1.422665-.119552C1.362889-.119552 1.06401-.119552 .812951-.274969C1.243337-.358655 1.338979-.71731 1.338979-.860772C1.338979-1.099875 1.159651-1.243337 .932503-1.243337C.645579-1.243337 .334745-.992279 .334745-.609714C.334745-.107597 .896638 .119552 1.41071 .119552C1.984558 .119552 2.391034-.334745 2.642092-.824907C2.833375-.119552 3.431133 .119552 3.873474 .119552C5.092902 .119552 5.738481-1.446575 5.738481-1.709589C5.738481-1.769365 5.69066-1.817186 5.618929-1.817186C5.511333-1.817186 5.499377-1.75741 5.463512-1.661768C5.140722-.609714 4.447323-.119552 3.90934-.119552C3.490909-.119552 3.263761-.430386 3.263761-.920548C3.263761-1.183562 3.311582-1.374844 3.502864-2.163885L3.921295-3.789788C4.100623-4.507098 4.507098-5.033126 5.057036-5.033126C5.080946-5.033126 5.415691-5.033126 5.66675-4.877709Z'/>
</defs>
<g id='page1' transform='matrix(1.13 0 0 1.13 -63.986043 -64.41)'>
<use x='56.413267' y='65.753425' xlink:href='#g0-115'/>
<use x='61.927273' y='65.753425' xlink:href='#g0-105'/>
<use x='65.920705' y='65.753425' xlink:href='#g0-110'/>
<use x='72.908311' y='65.753425' xlink:href='#g1-40'/>
<use x='77.460637' y='65.753425' xlink:href='#g0-120'/>
<use x='84.112724' y='65.753425' xlink:href='#g1-41'/>
</g>
</svg>
です</body>
</html>
```

試しに、Chrome で表示してみると、こんな感じになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/97350/05d4d1ce-8382-adfc-b012-94b79384de56.png)

ちょっとベースラインがズレていますし、SVGのサイズはピクセル値固定ですが、とりあえずできました！

https://github.com/yoshi389111/tex2svg