TypeScript初心者がなろう小説を1行ずつ読むマシーンをつくってみた

公開:2020/11/04
更新:2020/11/04
6 min読了の目安(約5700字TECH技術記事

完成物

なろう小説を1行ずつ読むマシーン

つくった理由

小説を1行ずつ読みたかったから。
次の行以降が目に入ると、今後の展開が目に入ってきて困る。
特に面白い小説ほど先が気になり、何行か先のネタバレを踏んでしまう。
できるだけ関数型っぽくつくりたかったが、副作用がたくさん出た。

まずはコンバーターのHTML

index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/siimple@3.3.1/dist/siimple.min.css">
  <link rel="stylesheet" href="./css/common.css">
  <title>Document</title>
</head>

<body>
  <div class="wrapper">
    <div class="siimple-h1">なろう小説を1行ずつ読むマシーン</div>
    <div>
      <textarea name="text-area" id="text-area" cols="100" rows="20" class="siimple-textarea">

      </textarea>
    </div>
    <div class="button-wrapper">
      <button class="siimple-btn siimple-btn--primary" id="js-button">
        変換
      </button>
    </div>
    <div>
      <div class="siimple-box" id="js-show-area"></div>
    </div>
    <div class="control-button">
      <button class="siimple-btn siimple-btn--primary" id="js-back-button">
        前へ
      </button>
      <button class="siimple-btn siimple-btn--primary" id="js-next-button">
        次へ
      </button>
    </div>
  </div>
  <script src="./src/index.js"></script>
</body>

</html>

cssはsiimpleというものをCDNで使用。
若干の調整は./css/common.cssにて行った。

common.css
.wrapper {
  margin-top: 2rem;
  text-align: center;
}

.button-wrapper {
  margin-top: 1rem;
  margin-bottom: 1rem;
}

.control-button {
  margin-top: 1rem;
}

#js-show-area {
  text-align: center;
  margin: auto;
  width: 677px;
  height: 200px;
}

#js-back-button {
  margin-right: 2rem;
}

TypeScriptで書いたものをJavaScriptに変換し./src/index.jsで読み込み。

テキストエリアになろう小説を貼りつけると、配列にして返す

配列にして返す部分はここ

    <div>
      <textarea name="text-area" id="text-area" cols="100" rows="20" class="siimple-textarea">

      </textarea>
    </div>
    <div class="button-wrapper">
      <button class="siimple-btn siimple-btn--primary" id="js-button">
        変換
      </button>
    </div>

index.ts
let convertedText: string[] = [];
const convertButton: HTMLElement = document.getElementById('js-button');
convertButton.addEventListener('click', () => {
  const textareaValue: string = getValue('text-area');
  const splitArray: string[] = stringSplitArray(textareaValue);
  const deleteNullArray: string[] = deleteNull(splitArray);
  convertedText = deleteNullArray;
}, false);

const deleteNull = (splitArray: string[]): string[] => {
  return splitArray.filter(Boolean);
};

const stringSplitArray = (textareaValue: string): string[] => {
  return textareaValue.split(/\r\n|\r|\n/);
};

const getValue = (id: string): string => {
  return (<HTMLInputElement>document.getElementById(id)).value;
};

まず、convertButton(#js-button)が押されると
#text-areaに入ったものがtextareaValueに入る。
次に、textareaValueを引数にstringSplitArrayという関数に渡す。
stringSplitArray関数は、文字を改行ごとに配列に入れてくれる。
仮に
1
2
3
4
5
という文字が#text-areaに入ると
["1", "2", "3", "4", "5"]という配列を返してくれる。

次に、splitArrayという配列を引数に、deleteNullという関数を呼ぶ。
これはさきほど文字を配列にしたあと、空の入った配列を削除する。

1

2
3
だと["1", "", "2", "3"]という配列が返ってくるが
splitArrayに入れると["1", "2", "3"]という風に空("")を削除してくれる。

これでなろう小説を改行ごとに配列に入れることができた。
これをグローバル関数のconvertedTextに入れる。
(グローバル関数を使いたくないが、生JSでstateの管理とかできるのかよくわからないのでグローバル関数に……)

1行ずつ読む部分

index.html
<div>
      <div class="siimple-box" id="js-show-area"></div>
    </div>
    <div class="control-button">
      <button class="siimple-btn siimple-btn--primary" id="js-back-button">
        前へ
      </button>
      <button class="siimple-btn siimple-btn--primary" id="js-next-button">
        次へ
      </button>
    </div>
index.ts
let readCount: number = 0;

const nextButton = document.getElementById('js-next-button');
const backButton = document.getElementById('js-back-button');

nextButton.addEventListener('click', () => {
  const showArea = document.getElementById('js-show-area');
  showTextLine(readCount, showArea);
}, false);

backButton.addEventListener('click', () => {
  const showArea = document.getElementById('js-show-area');
  showBeforeTextLine(readCount, showArea);
}, false);

const showTextLine = (index: number, showArea: HTMLElement) => {
  document.querySelector('#js-show-area').innerHTML = '';
  if (readCount >= convertedText.length) {
    showArea.insertAdjacentHTML('afterbegin', '最終行です! これ以上進めません');
    readCount = convertedText.length + 1;
  } else {
    showArea.insertAdjacentHTML('afterbegin', convertedText[index]);
    readCount++;
  }

};

const showBeforeTextLine = (index: number, showArea: HTMLElement) => {
  document.querySelector('#js-show-area').innerHTML = '';
  if (readCount <= 1) {
    showArea.insertAdjacentHTML('afterbegin', 'これ以上戻れません!');
    readCount = 0;
  } else {
    showArea.insertAdjacentHTML('afterbegin', convertedText[index - 2]);
    readCount--;
  }
};

これもめちゃくちゃ副作用があるなぁ……。
いま配列のどこを読んでいるのか、をreadCountというグローバル変数で管理。

ネクストボタンを押すと、#js-show-areaに1行目から読み込んでいく。
1行目とか2行目とかは、そのままinsertAdjacentHTMLで表示していく。
convertedText[index]のindexにはreadCountが入ってる。
そしてreadCountを1増やす。(readCount++)

バックボタンを押すと、#js-show-areaに2つ前のreadCountの文章が入る。
プラス1して次の行になってるから2個戻す必要がある(このあたり設計ミスの気がする)。
convertedText[index -2]で2個前のものを表示。
readCount--;で次に表示されるものを調整する。

さらに、最終行までいくと「最終行です! これ以上進めません」と出るようにする。
if(readCount >= convertedText.length)
これで、もしいま読んでいるところがconvertedText配列の一番後ろなら~という条件。
このときは最終行なので進めませんよ、ということ。

一番最初の行から、さらに戻ろうとすると
「これ以上戻れません!」と出る。
if(readCount <= 1)で1より小さい値になっていたら強制的に0に戻している。

感想

こんな感じで実装できたけど、状態(state)を持つならReactでつくったほうが良い気がする。
ということで、今度はReactで同じものをつくってみようかなぁ。
今後の展望として、APIを叩いて小説の本文を次々に取ってくるみたいなのがつくれたら楽だなぁ……などと思いつつ。
とりあえず青空小説を1行ずつ読むマシーンでもなんでも良いなぁ。
それと1行ずつ読むマシーンといいつつ改行ごとに読むマシーンだな。
(改行せずにだらだら書かれると1行でなくなるので……)