Ankiでフラッシュカードの両面間でデータを永続化させる

# はじめに
暗記のための自由/オープンソースソフトウェア「[Anki](https://apps.ankiweb.net/)」があります。要は単語帳のデジタル版なのですが、エビングハウスの忘却曲線に基づいて適切なタイミングで復習出来るように出題をコントロールしてくれます。
英語学習者や医学生に人気があります。
[【使ってみた】暗記アプリAnkiで単語学習を超絶効率化！](https://web.archive.org/web/20221002063809/https://eigonote.jp/2020/09/%E3%80%90%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%81%9F%E3%80%91%E6%9A%97%E8%A8%98%E3%82%A2%E3%83%97%E3%83%AAanki%E3%81%A7%E5%8D%98%E8%AA%9E%E5%AD%A6%E7%BF%92%E3%82%92%E8%B6%85%E7%B5%B6%E5%8A%B9/)

## JavaScriptによる機能拡張
AnkiはPythonを利用したアドオンを作成することが出来るのですが、PC向け(Windows、Mac、Linux)のみで、モバイル向けは対象外です。
JavaScriptによる機能拡張は、PCやモバイルで使用可能です。
私はJavaScriptによる機能拡張をいろいろ試そうとしており、昨年も記事を書きました。

https://qiita.com/yaju/items/ba70f2ec294c50fca9d3

# 両面間でデータを永続化
下記サイトではJavaScriptを使用して選択肢をランダム表示させています。

https://zenn.dev/shivase/articles/002-anki-random-question

作者(shivase)の共有デッキ「[問題の選択肢をランダムにしたデック](https://ankiweb.net/shared/info/880026204)」を例にします。但し CSSを一部変更して順序付きリストをアルファベットから算用数字にしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/962f6eab-df04-a6f2-2068-73497c4a1210.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/22401d13-283c-bf9f-9416-763216b3a39d.png)

>問題点
>解答の表示を押すと、画面がリフレッシュするので再度ランダム処理が走ります。ですので、解答を見たときに順番が変わってしまうので、選択肢の番号を覚えるのではなく、ちゃんと解答した中身を覚えておく必要があります（結果としてちゃんと覚えるのでよいかもですが）

その際に上記の問題点が書かれています。上図のように表面と裏面の問題内容(3.B,4.A)が違っています。
今回はこれを解決しようとした記事になります。

それには下記プログラムを使用します。例では表面と裏面で乱数を一致させています。

https://github.com/SimonLammer/anki-persistence

# 改良
作者(shivase)の共有デッキ「[問題の選択肢をランダムにしたデック](https://ankiweb.net/shared/info/880026204)」を改良していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/976ff479-9347-00b5-8f91-788b738b8e47.png)

メニューの[ツール]→[ノートタイプを管理]をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/8b2c2e2a-12dc-1031-6b4c-ce8be585d0c7.png)

今回のフィールドは、1.Question、2.Answerになっています。
基本の1.表面、2.裏面でもいいです。その場合、カード内の{{Question}}を{{表面}}、{{Answer}}を{{裏面}}にする必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/faa79dfb-c3ff-a7de-7253-c2c39113bf45.png)

ノートタイプを管理画面でカードボタンをクリックします。
問題文の選択肢の部分はリスト（右上のリストアイコンを選択）にします。Answerには解答を書きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/9653c4ea-9376-a06a-951d-67ec9b1b5136.png)

## 表面
表面のテンプレートに記載します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/3663ff2f-ba8f-a48c-e4dd-95e7a66f7580.png)

```html
<script>
// v1.1.8 - https://github.com/SimonLammer/anki-persistence/blob/584396fea9dea0921011671a47a0fdda19265e62/script.js
if(void 0===window.Persistence){var e="github.com/SimonLammer/anki-persistence/",t="_default";if(window.Persistence_sessionStorage=function(){var i=!1;try{"object"==typeof window.sessionStorage&&(i=!0,this.clear=function(){for(var t=0;t<sessionStorage.length;t++){var i=sessionStorage.key(t);0==i.indexOf(e)&&(sessionStorage.removeItem(i),t--)}},this.setItem=function(i,n){void 0==n&&(n=i,i=t),sessionStorage.setItem(e+i,JSON.stringify(n))},this.getItem=function(i){return void 0==i&&(i=t),JSON.parse(sessionStorage.getItem(e+i))},this.removeItem=function(i){void 0==i&&(i=t),sessionStorage.removeItem(e+i)},this.getAllKeys=function(){for(var t=[],i=Object.keys(sessionStorage),n=0;n<i.length;n++){var s=i[n];0==s.indexOf(e)&&t.push(s.substring(e.length,s.length))}return t.sort()})}catch(n){}this.isAvailable=function(){return i}},window.Persistence_windowKey=function(i){var n=window[i],s=!1;"object"==typeof n&&(s=!0,this.clear=function(){n[e]={}},this.setItem=function(i,s){void 0==s&&(s=i,i=t),n[e][i]=s},this.getItem=function(i){return void 0==i&&(i=t),void 0==n[e][i]?null:n[e][i]},this.removeItem=function(i){void 0==i&&(i=t),delete n[e][i]},this.getAllKeys=function(){return Object.keys(n[e])},void 0==n[e]&&this.clear()),this.isAvailable=function(){return s}},window.Persistence=new Persistence_sessionStorage,Persistence.isAvailable()||(window.Persistence=new Persistence_windowKey("py")),!Persistence.isAvailable()){var i=window.location.toString().indexOf("title"),n=window.location.toString().indexOf("main",i);i>0&&n>0&&n-i<10&&(window.Persistence=new Persistence_windowKey("qt"))}}
</script>

<script>
(() => {
  const range = (start, end) => [...Array(end + 1).keys()].slice(start);
  let container = $('.question ol');
  let content = container.find("li");
  let total = content.length;
  let array = range(0, total-1);

  if (Persistence.isAvailable()) {
    array = Persistence.getItem();
    if (array == null) {
      array = range(0, total-1);
      for (i = array.length; 1 < i; i--) {
        k = Math.floor(Math.random() * i);
        [array[k], array[i - 1]] = [array[i - 1], array[k]];
      }
      Persistence.setItem(array);
    }
  }
  content.each(function(index) {
    rnd = array[index]; 
    content.eq(rnd).prependTo(container);
  });

})()
</script>

<div class="question-header">
Question
</div>

<div class="question">
{{Question}}
</div>
``` 
### 表面の解説
データの永続化として、シャッフルした配列をセットしています。

```js
Persistence.setItem(array);
```

裏面で再読み込みした場合、保存した配列を読み込みます。

```js
array = Persistence.getItem();
```

## 裏面
裏面のテンプレートに記載します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/ce1bf100-5108-a501-7a7c-630274e2f5bb.png)

```html
<script>
// v1.1.8 - https://github.com/SimonLammer/anki-persistence/blob/584396fea9dea0921011671a47a0fdda19265e62/script.js
if(void 0===window.Persistence){var e="github.com/SimonLammer/anki-persistence/",t="_default";if(window.Persistence_sessionStorage=function(){var i=!1;try{"object"==typeof window.sessionStorage&&(i=!0,this.clear=function(){for(var t=0;t<sessionStorage.length;t++){var i=sessionStorage.key(t);0==i.indexOf(e)&&(sessionStorage.removeItem(i),t--)}},this.setItem=function(i,n){void 0==n&&(n=i,i=t),sessionStorage.setItem(e+i,JSON.stringify(n))},this.getItem=function(i){return void 0==i&&(i=t),JSON.parse(sessionStorage.getItem(e+i))},this.removeItem=function(i){void 0==i&&(i=t),sessionStorage.removeItem(e+i)},this.getAllKeys=function(){for(var t=[],i=Object.keys(sessionStorage),n=0;n<i.length;n++){var s=i[n];0==s.indexOf(e)&&t.push(s.substring(e.length,s.length))}return t.sort()})}catch(n){}this.isAvailable=function(){return i}},window.Persistence_windowKey=function(i){var n=window[i],s=!1;"object"==typeof n&&(s=!0,this.clear=function(){n[e]={}},this.setItem=function(i,s){void 0==s&&(s=i,i=t),n[e][i]=s},this.getItem=function(i){return void 0==i&&(i=t),void 0==n[e][i]?null:n[e][i]},this.removeItem=function(i){void 0==i&&(i=t),delete n[e][i]},this.getAllKeys=function(){return Object.keys(n[e])},void 0==n[e]&&this.clear()),this.isAvailable=function(){return s}},window.Persistence=new Persistence_sessionStorage,Persistence.isAvailable()||(window.Persistence=new Persistence_windowKey("py")),!Persistence.isAvailable()){var i=window.location.toString().indexOf("title"),n=window.location.toString().indexOf("main",i);i>0&&n>0&&n-i<10&&(window.Persistence=new Persistence_windowKey("qt"))}}
</script>

{{FrontSide}}

<hr id=answer>

<div class="answer-header">
Answer
</div>

<div class="answer">
{{裏面}}
</div>

<script>
(() => {
  let ansel = document.getElementsByClassName('answer');
  let container = $('.question ol');
  let content = container.find('li');
  for (let i = 0; i < content.length; i ++) {
    if (content[i].innerText == ansel[0].innerText) {
      ansel[0].innerText = i + 1 + '. ' + content[i].innerText;
      break;
    }
  }

  if (Persistence.isAvailable()) {
    Persistence.clear();
  }
})()
</script>
```
### 裏面の解説
オリジナルにはなかったのですが、解答にリスト番号を付けるようにしました。

```js
  for (let i = 0; i < content.length; i ++) {
    if (content[i].innerText == ansel[0].innerText) {
      ansel[0].innerText = i + 1 + '. ' + content[i].innerText;
      break;
    }
  }
```

裏面した段階で保存した配列をクリアーします。

```js
Persistence.clear();
```

## 書式
オリジナルとの違いとして`list-style-type`をコメントアウトして、順序付きリストをアルファベットから算用数字にしています。

```css
.card {
    font-family: arial;
    font-size: 20px;
    text-align: center;
    color: black;
    background-color: white;
}

.question li {
  border-radius :8px;
  box-shadow :0px 0px 3px black;
  padding: 0.5em 0.5em 0.5em 0.5em;
  margin: 1em 1em;
/*  list-style-type: upper-alpha; */
}

.answer li {
  border-radius :8px;
  box-shadow :0px 0px 3px black;
  padding: 0.5em 0.5em 0.5em 0.5em;
  margin: 0.5em 0.5em;
  list-style-type: disc;
}

.tags {
  margin: 1em 1em 1em 0;
}

.question-header {
  text-aligin: center;
  margin: 1em;
  font-weight: bold;
}

.answer-header {
  text-align: center;
  margin: 1em;
  font-weight: bold;
}
```

# 共有デッキ
今回の共有デッキを作成しました。
上記プログラムがよく分からない方は、共有デッキを使えば楽が出来ます。

https://ankiweb.net/shared/info/760122672

## 単語帳をAnkiWebへの共有する方法
https://rs.luminousspice.com/how-to-share-anki-decks/#i-6

# 最後に
両面間でデータを永続化が出来たので満足しています。
AnkiのJavaScriptの機能拡張だけやって、本来の暗記自体を全く活用していないんだよね。