Markdownが好きなのでMarkdownを学べるサイトをMarkdownAIで作ってみた

この記事は、[Markdown AIのサーバーAI機能を使ってWebサイトを作ってみよう by MarkdownAI Advent Calendar 2024](https://qiita.com/advent-calendar/2024/markdownai)の参加記事です。

https://qiita.com/advent-calendar/2024/markdownai

# この記事で書くこと
- 作ったサイトについて
- 作った動機
- MarkdownAIとは
- 使ってみての感想
- 実装コードとプロンプト


# この記事で書かないこと
- MarkdownAIの使い方
    - 公式の説明が分かりやすいのでそちらに譲ります
- AIに食わせるプロンプトのノウハウなど
    - AI関連詳しくなくて自己流なので参考にならないと思われます

# 作ったサイトについて

Markdownってなに？という方に向けてMarkdownを学べるサイトをMarkdownAIというサービスに登録して作りました。回答はAIが自動生成しています。
まず、どんな感じか触ってみてください。

https://mdown.ai/content/71b2993f-48ff-4296-85e4-85ed88313b38

AIの出力形式を安定させるのが難しかったので表示が崩れたらすみません。

# MarkdownAIとは

何ができるのか、こちらの公式サイトに分かりやすく記載があります。

https://mdown.com/ja/markdown-ai/

ざっくりと概要だけピックアップすると

- Markdown形式で書くと、良い感じの見た目にしてくれてWebサイトのように簡単に表示できる
- 全てUIからの操作で簡単にAIを作成して埋め込める
- サーバーなどの難しい設定何も触らず、簡単にWebサイトを公開できる
- ServerAIを使用すると、難しい設定なしで簡単にAIモデルを作成できます。

という感じですが特に

> ServerAIを使用すると、難しい設定なしで簡単にAIモデルを作成できます。

これは結構売りなんじゃないかと。
AI取り入れたどんなサイトを作ろうって考えるだけでもなんかワクワクしますよね。

公式のサンプルアプリを見るとどんなものが作れるか参考になるのでぜひ見てみてください。
プロンプトとソースもあります。

https://mdown.com/ja/category/sample-app/

# 作った動機

私はMarkdownをエンジニアになってから初めて知ったのですが、色んなエディタで使えて便利だし、簡単に覚えられるしでとても気に入っています（Markdownが使えるかどうかが使用するアプリの選考理由だったりまでします）

なのでこのサイトがMarkdownを知らない人がMarkdownに興味を持つとっかかりになったらいいなと思い作成しました。

先ほど紹介したMarkdownAIはMarkdownでWebサイトを公開できるというサービスです。
そう！これはMarkdown記法を学ぶのに最適ではないですか！

MarkdownAIで私が作ったWebサイトでMarkdownの書き方を覚えれば、MarkdownAIを使ってWebサイトを作って公開できるんですよ！すごい循環呼吸のようだ。
学んだことがすぐ生かせるとても良い学習環境なのではないかと思った次第です。

# MarkdownAIの使い方

難しいところはないので説明を見なくても分かる人も多い気がしますが、下記の公式の記事が分かりやすいので困ったらこちらを参照することをお勧めします。
※現在knowledge機能は設定するとエラーになりますのでご注意ください

https://qiita.com/mdown_ai_jpn/items/d3e281565c876a0bd64f

# 使ってみての感想

- 本当に簡単にWeb公開できてすごい
    - MarkdownAI内のエディタにHTML,CSS,JavaScriptを記述して画面作成
    - そのままプレビュー表示して動作確認
    - OKだったら公開ボタンでURLが生成される
    - 他のツール使わず公開まで完結できるのが手軽で良い
- ServerAI機能もUIだけで完結できて良かった
    - AIを使うためのコードも自動生成してくれるので張り付けるだけ
    - AIの回答の精度を上げるためにプロンプトを考えるのは沼
- AIからの回答のMarkdownをHTMLに変換するのが難しかった
    - 力技のJavaScriptで実装してますがほとんどClaudeくんに書いてもらいました
    - リストのインデントとか結構難しかった
    - AIの出力形式の精度を上げるのとJSの変換の精度を上げるのとバランスを取ったけどまだ上手くいかないときがあります

# 実装コードとプロンプト

AIモデルはClaudeを使いました。
プロンプトの内容は試行錯誤して以下のような感じです。

```text
Role:あなたはMarkdownについて教える先生です。
Request:入力されたコンテンツのMarkdownでの書き方とその説明をしてください。
Request:入力されたコンテンツが「イタリック」のときは英語の文字列を例として使ってください。
Request:入力されたコンテンツが「画像」のときは表示できる画像を使ってください。
Rule:「○○（○○はユーザーが選択した値に置き換えてください）について」「書き方」「説明」を３つ別々の段落で出力しそれぞれ「# ○○について」「## 書き方」「## 説明」から書き始めてください。
Rule: 「○○について」はMarkdownで出力しそのまま解釈できるように改行を含めてください。
Rule:「○○について」の出力にはバッククオート３つを絶対に絶対に含めないでください。例：# 見出し１
Rule:「書き方」はMarkdownとして解釈されないよう<pre><code></code></pre>で囲んで出力してください。
Rule:「説明」はMarkdownにせず、文章で出力してください。くれぐれもリストの-は使わないでください。
Rule:「説明」の中でMarkdown記法を用いるときは<code></code>で囲んで出力してください。
Recommend:「説明」はMarkdownを全く知らない初心者にも伝わるように平易な言葉にしてください。
```

HTMLはこちら。これをMarkdownAIのエディタにコピペすれば、同じサイトができます。
JSでMarkdownをHTMLのタグに変換する部分はこちらの記事を参考にさせていただきました！

https://qiita.com/kazu_iroiro/items/408ab51b6858003d2785

```html
# Markdownを学んでWebサイトを作ってみよう！

皆さんはMarkdownを知っていますか？
このサイトではMarkdownの書き方や使い方を学ぶことができます。
AI先生が回答するようになってるのでぜひ好きなコンテンツを選んでボタンを押してみてください。

Markdownを使うと色々なアプリケーションで使うことができますが、
[MarkdownAI](https://mdown.com/ja/markdown-ai/)を使うと、Markdownを使って**簡単にWebサイトを作れます！このサイトのようにAIを組み込むこともできます！**
（このサイトもMarkdownAIを使用して作られています）

興味が沸いたらぜひMarkdownAIでWebサイトを作ってみてください！

<div style="display: inline-block;">
  <select id="text-1733747070" style="width: 200px;">
    <option value="">選択してください</option>
    <option value="見出し">見出し (# h1 ~ ###### h6)</option>
    <option value="太字">太字 (**text**)</option>
    <option value="イタリック">イタリック (*text*)</option>
    <option value="取り消し線">取り消し線 (~~text~~)</option>
    <option value="順序なしリスト">順序なしリスト (- item)</option>
    <option value="番号付きリスト">番号付きリスト (1. item)</option>
    <option value="チェックリスト">チェックリスト (- [ ] task)</option>
    <option value="引用">引用 (> text)</option>
    <option value="水平線">水平線 (---)</option>
    <option value="テーブル">テーブル (|header|)</option>
    <option value="コードブロック">コードブロック (```lang)</option>
    <option value="インラインコード">インラインコード (`code`)</option>
    <option value="リンク">リンク ([text](url))</option>
    <option value="画像">画像 (![alt](url))</option>
  </select>
  <button type="button" id="button-1733747070">AI先生に聞いてみる</button>
</div>
<div class="dot-spinner" id="loading">
  <div class="dot-spinner__dot"></div>
  <div class="dot-spinner__dot"></div>
  <div class="dot-spinner__dot"></div>
  <div class="dot-spinner__dot"></div>
  <div class="dot-spinner__dot"></div>
  <div class="dot-spinner__dot"></div>
  <div class="dot-spinner__dot"></div>
  <div class="dot-spinner__dot"></div>
</div>
<div id="answer-1733747070"></div>

<style>
  #text-1733747070,
  #button-1733747070 {
    height: 38px;
    padding: 6px 12px;
    line-height: 1.5;
    vertical-align: middle;
    box-sizing: border-box;
  }

  /* From Uiverse.io by e-coders */
  button {
    appearance: none;
    background-color: transparent;
    border: 0.125em solid #1A1A1A;
    border-radius: 0.9375em;
    box-sizing: border-box;
    color: #3B3B3B;
    cursor: pointer;
    display: inline-block;
    font-family: Roobert, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
    font-size: 16px;
    font-weight: 600;
    line-height: normal;
    margin: 0;
    min-width: 0;
    outline: none;
    padding: 1em 2.3em;
    text-align: center;
    text-decoration: none;
    transition: all 300ms cubic-bezier(.23, 1, 0.32, 1);
    user-select: none;
    -webkit-user-select: none;
    touch-action: manipulation;
    will-change: transform;
  }

  button:disabled {
    pointer-events: none;
  }

  button:hover {
    color: #fff;
    background-color: #1A1A1A;
    box-shadow: rgba(0, 0, 0, 0.25) 0 8px 15px;
    transform: translateY(-2px);
  }

  button:active {
    box-shadow: none;
    transform: translateY(0);
  }

  /* From Uiverse.io by abrahamcalsin */
  .dot-spinner {
    margin-top: 20px;
    --uib-size: 2.8rem;
    --uib-speed: .9s;
    --uib-color: #183153;
    position: relative;
    display: none;
    align-items: center;
    justify-content: flex-start;
    height: var(--uib-size);
    width: var(--uib-size);
  }

  .dot-spinner__dot {
    position: absolute;
    top: 0;
    left: 0;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    height: 100%;
    width: 100%;
  }

  .dot-spinner__dot::before {
    content: '';
    height: 20%;
    width: 20%;
    border-radius: 50%;
    background-color: var(--uib-color);
    transform: scale(0);
    opacity: 0.5;
    animation: pulse0112 calc(var(--uib-speed) * 1.111) ease-in-out infinite;
    box-shadow: 0 0 20px rgba(18, 31, 53, 0.3);
  }

  .dot-spinner__dot:nth-child(2) {
    transform: rotate(45deg);
  }

  .dot-spinner__dot:nth-child(2)::before {
    animation-delay: calc(var(--uib-speed) * -0.875);
  }

  .dot-spinner__dot:nth-child(3) {
    transform: rotate(90deg);
  }

  .dot-spinner__dot:nth-child(3)::before {
    animation-delay: calc(var(--uib-speed) * -0.75);
  }

  .dot-spinner__dot:nth-child(4) {
    transform: rotate(135deg);
  }

  .dot-spinner__dot:nth-child(4)::before {
    animation-delay: calc(var(--uib-speed) * -0.625);
  }

  .dot-spinner__dot:nth-child(5) {
    transform: rotate(180deg);
  }

  .dot-spinner__dot:nth-child(5)::before {
    animation-delay: calc(var(--uib-speed) * -0.5);
  }

  .dot-spinner__dot:nth-child(6) {
    transform: rotate(225deg);
  }

  .dot-spinner__dot:nth-child(6)::before {
    animation-delay: calc(var(--uib-speed) * -0.375);
  }

  .dot-spinner__dot:nth-child(7) {
    transform: rotate(270deg);
  }

  .dot-spinner__dot:nth-child(7)::before {
    animation-delay: calc(var(--uib-speed) * -0.25);
  }

  .dot-spinner__dot:nth-child(8) {
    transform: rotate(315deg);
  }

  .dot-spinner__dot:nth-child(8)::before {
    animation-delay: calc(var(--uib-speed) * -0.125);
  }

  @keyframes pulse0112 {

    0%,
    100% {
      transform: scale(0);
      opacity: 0.5;
    }

    50% {
      transform: scale(1);
      opacity: 1;
    }
  }
</style>

<script>
  (() => {
    const button = document.getElementById('button-1733747070');
    const loading = document.getElementById('loading');
    const answerElement = document.getElementById('answer-1733747070');

    button.addEventListener('click', async event => {
      answerElement.innerHTML = '';
      // ボタンを無効化
      button.disabled = true;
      // ローディング表示
      loading.style.display = 'flex';
      try {
        const serverAi = new ServerAI();
        const message = document.getElementById('text-1733747070').value;
        let answer = await serverAi.getAnswerText('t2hyV52YELnZKe5LsRgP13', '', message);
        console.log('raw answer:', answer);

        answerElement.innerHTML = markdownToHtml(answer);
      } finally {
        // ローディング非表示
        loading.style.display = 'none';
        button.disabled = false;
      }


      function markdownToHtml(markdown) {
        // HTML出力を格納する変数
        let html = "";

        // Markdownテキストを行ごとに分割
        const lines = markdown.split("\n");

        // リスト処理用の変数
        let currentListLevel = 0;
        let listStack = [];
        let currentOrderedListLevel = 0;
        let orderedListStack = [];
        let currentCheckListLevel = 0;
        let checkListStack = [];
        let isInChecklist = false;

        // 引用処理用の変数を追加
        let currentQuoteLevel = 0;
        let quoteStack = [];

        // テーブル処理用の変数を修正
        let tableRows = [];
        let tableAlignments = [];

        // コードブロック状態管理用変数を追加
        let isInCodeBlock = false;

        lines.forEach((line) => {
          if (/<pre><code>/.test(line)) {
            isInCodeBlock = true;
            html += line;
          } else if (/<\/code><\/pre>/.test(line)) {
            isInCodeBlock = false;
            html += line;
          } else if (isInCodeBlock) {
            html += line;
            // テーブルの処理
          } else if (/^\|(.+)\|$/.test(line)) {
            // テーブル行を配列に追加
            const cells = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim());
            tableRows.push(cells);
            return; // 次の行へ
          }
          // テーブル以外の行が来た場合、蓄積したテーブルを処理
          else if (tableRows.length > 0) {
            // アライメント行を解析
            const alignments = tableRows[1].map(cell => {
              if (cell.startsWith(':') && cell.endsWith(':')) return 'center';
              if (cell.endsWith(':')) return 'right';
              return 'left';
            });

            // テーブルのHTML生成
            html += '<table class="table is-bordered is-hoverable"><tbody>';

            // ヘッダー行
            html += '<tr>';
            tableRows[0].forEach((cell, i) => {
              html += `<th align="${alignments[i]}">${cell}</th>`;
            });
            html += '</tr>';

            // データ行
            tableRows.slice(2).forEach(row => {
              html += '<tr>';
              row.forEach((cell, i) => {
                html += `<td align="${alignments[i]}">${cell.replace(/ /g, '&nbsp;')}</td>`;
              });
              html += '</tr>';
            });

            html += '</tbody></table>';

            // テーブル変数をリセット
            tableRows = [];
            tableAlignments = [];

            // 現在の行を処理
            // ...existing code for non-table elements...
          }
          // 画像（![ ]( )） -> <img>
          else if (/^!\[([^\]]*)\]\(([^)]+)\)$/.test(line)) {
            const [_, alt, url] = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
            html += `<img src="${url}" alt="${alt}"><br>`;
          }
          // Markdownリンク記法 [text](url)
          else if (/\[([^\]]+)\]\(([^)]+)\)/.test(line)) {
            const [fullMatch, text, url] = line.match(/\[([^\]]+)\]\(([^)]+)\)/);
            const link = `<a href="${url}">${text}</a><br>`;
            // 行の残りの部分を処理
            const restOfLine = line.replace(fullMatch, link);
            if (restOfLine !== link) {
              html += `<p>${restOfLine}</p>`;
            } else {
              html += link;
            }
          }
          // URL自動リンク
          else if (/^https?:\/\/\S+$/.test(line)) {
            html += `<a href="${line}">${line}</a><br>`;
          }
          // 水平線 (---, ___, ***) -> <hr class="horizontal-rule">
          else if (/^([-_*])\1{2,}$/.test(line)) {
            html += '<hr class="horizontal-rule">';
          }
          else {
            // 引用の処理を最優先で行う
            if (/^(>+)\s*(.+)$/.test(line)) {
              const [_, quotes, content] = line.match(/^(>+)\s*(.+)$/);
              const level = quotes.length;

              // 新しい引用レベルを開始
              while (currentQuoteLevel < level) {
                html += '<blockquote class="markdown">';
                quoteStack.push('</blockquote>');
                currentQuoteLevel++;
              }

              // 引用レベルを閉じる
              while (currentQuoteLevel > level) {
                html += quoteStack.pop();
                currentQuoteLevel--;
              }

              html += `<p>${content}</p>`;
            }
            // 引用以外の要素に移る場合、開いている引用をすべて閉じる
            else {
              while (quoteStack.length > 0) {
                html += quoteStack.pop();
                currentQuoteLevel--;
              }

              // チェックリストのパターンをより具体的に定義
              if (/^(\s*)- \[(x| )\] (.+)$/.test(line)) {
                const [_, indent, checked, content] = line.match(/^(\s*)- \[(x| )\] (.+)$/);
                const level = indent.length / 2;

                // 最初のレベルでulタグがない場合は作成
                if (currentCheckListLevel === 0) {
                  html += '<ul class="markdown">';
                  checkListStack.push('</ul>');
                }

                // インデントレベルに応じてネストを処理
                while (currentCheckListLevel < level) {
                  html = html.replace(/<\/li>$/, '');
                  html += '<ul class="markdown">';
                  checkListStack.push('</ul></li>');
                  currentCheckListLevel++;
                }

                while (currentCheckListLevel > level) {
                  html += checkListStack.pop();
                  currentCheckListLevel--;
                }

                const isChecked = checked === 'x' ? 'checked' : '';
                html += `<li class="li-checkbox"><input type="checkbox" ${isChecked}>${content}</li>`;
              }
              // 通常のリストの処理から、チェックリストパターンを除外
              else if (/^(\s*)- (?!\[(?:x| )\])(.+)$/.test(line)) {
                const [_, indent, content] = line.match(/^(\s*)- (.+)$/);
                const level = indent.length / 2; // インデントの深さを計算

                // 新しいレベルを開始
                while (currentListLevel < level) {
                  html += '<ul>';
                  listStack.push('</ul>');
                  currentListLevel++;
                }

                // レベルを閉じる
                while (currentListLevel > level) {
                  html += listStack.pop();
                  currentListLevel--;
                }

                html += `<li>${content}</li>`;
              }
              // 見出し1 (# ) -> <h1 class="is-size-1">
              else if (/^# (.+)/.test(line)) {
                const content = line.match(/^# (.+)/)[1];
                html += `<h1 class="is-size-1">${content}</h1>`;
              }
              // 見出し2 (## ) -> <h2 class="is-size-2">
              else if (/^## (.+)/.test(line)) {
                const content = line.match(/^## (.+)/)[1];
                html += `<h2 class="is-size-2">${content}</h2>`;
              }
              // 見出し3 (### ) -> <h3 class="is-size-3">
              else if (/^### (.+)/.test(line)) {
                const content = line.match(/^### (.+)/)[1];
                html += `<h3 class="is-size-3">${content}</h3>`;
              }
              // 見出し4 (#### ) -> <h4 class="is-size-4">
              else if (/^#### (.+)/.test(line)) {
                const content = line.match(/^#### (.+)/)[1];
                html += `<h4 class="is-size-4">${content}</h4>`;
              }
              // 見出し5 (##### ) -> <h5 class="is-size-5">
              else if (/^##### (.+)/.test(line)) {
                const content = line.match(/^##### (.+)/)[1];
                html += `<h5 class="is-size-5">${content}</h5>`;
              }
              // 見出し6 (###### ) -> <h6 class="is-size-6">
              else if (/^###### (.+)/.test(line)) {
                const content = line.match(/^###### (.+)/)[1];
                html += `<h6 class="is-size-6">${content}</h6>`;
              }
              // 太字 (** **) -> <strong>
              else if (/\*\*([^*]+)\*\*/.test(line)) {
                const content = line.match(/\*\*([^*]+)\*\*/)[1];
                html += `<strong>${content}</strong>`;
              }
              // イタリック (* *) -> <em>
              else if (/\*([^*]+)\*/.test(line)) {
                const content = line.match(/\*([^*]+)\*/)[1];
                html += `<em>${content}</em>`;
              }
              // 取り消し線 (~~ ~~) -> <del>
              else if (/^~~(.+)~~$/.test(line)) {
                const content = line.match(/^~~(.+)~~$/)[1];
                html += `<del>${content}</del>`;
              }
              // 番号付きリスト（1. ）のインデント対応
              else if (/^(\s*)\d+\. (.+)$/.test(line)) {
                const [_, indent, content] = line.match(/^(\s*)(\d+\. .+)$/);
                const level = indent.length / 2;

                // 番号を取得
                const number = parseInt(content.match(/^(\d+)/)[0]);

                // 最初のレベルでolタグがない場合は作成
                if (currentOrderedListLevel === 0 && orderedListStack.length === 0) {
                  html += '<ol class="markdown">';
                  orderedListStack.push('</ol>');
                  currentOrderedListLevel = 0;
                }

                // インデントレベルに応じてネストを処理
                while (currentOrderedListLevel < level) {
                  if (currentOrderedListLevel === level - 1) {
                    // 直前の</li>を削除して新しいネストを開始
                    html = html.replace(/<\/li>$/, '');
                    html += `<ol class="markdown" start="${number}">`;
                    orderedListStack.push('</ol></li>');
                  } else {
                    html += `<li><ol class="markdown" start="${number}">`;
                    orderedListStack.push('</ol></li>');
                  }
                  currentOrderedListLevel++;
                }

                while (currentOrderedListLevel > level) {
                  html += orderedListStack.pop();
                  currentOrderedListLevel--;
                }

                html += `<li>${content.replace(/^\d+\. /, '')}</li>`;
              }
              // チェックリスト以外の要素に移る場合
              else {
                if (isInChecklist) {
                  // チェックリストを閉じる
                  while (checkListStack.length > 0) {
                    html += checkListStack.pop();
                  }
                  html += '</ul>';
                  isInChecklist = false;
                  currentCheckListLevel = 0;
                }
                // 見出し1 (# ) -> <h1 class="is-size-1">
                if (/^# (.+)/.test(line)) {
                  const content = line.match(/^# (.+)/)[1];
                  html += `<h1 class="is-size-1">${content}</h1>`;
                }
                // 見出し2 (## ) -> <h2 class="is-size-2">
                else if (/^## (.+)/.test(line)) {
                  const content = line.match(/^## (.+)/)[1];
                  html += `<h2 class="is-size-2">${content}</h2>`;
                }
                // 見出し3 (### ) -> <h3 class="is-size-3">
                else if (/^### (.+)/.test(line)) {
                  const content = line.match(/^### (.+)/)[1];
                  html += `<h3 class="is-size-3">${content}</h3>`;
                }
                // 見出し4 (#### ) -> <h4 class="is-size-4">
                else if (/^#### (.+)/.test(line)) {
                  const content = line.match(/^#### (.+)/)[1];
                  html += `<h4 class="is-size-4">${content}</h4>`;
                }
                // 見出し5 (##### ) -> <h5 class="is-size-5">
                else if (/^##### (.+)/.test(line)) {
                  const content = line.match(/^##### (.+)/)[1];
                  html += `<h5 class="is-size-5">${content}</h5>`;
                }
                // 見出し6 (###### ) -> <h6 class="is-size-6">
                else if (/^###### (.+)/.test(line)) {
                  const content = line.match(/^###### (.+)/)[1];
                  html += `<h6 class="is-size-6">${content}</h6>`;
                }
                // 太字 (** **) -> <strong>
                else if (/\*\*([^*]+)\*\*/.test(line)) {
                  const content = line.match(/\*\*([^*]+)\*\*/)[1];
                  html += `<strong>${content}</strong>`;
                }
                // イタリック (* *) -> <em>
                else if (/\*([^*]+)\*/.test(line)) {
                  const content = line.match(/\*([^*]+)\*/)[1];
                  html += `<em>${content}</em>`;
                }
                // 取り消し線 (~~ ~~) -> <del>
                else if (/^~~(.+)~~$/.test(line)) {
                  const content = line.match(/^~~(.+)~~$/)[1];
                  html += `<del>${content}</del>`;
                }
                // リスト（- ） -> <ul><li>
                else if (/^(\s*)- (.+)$/.test(line)) {
                  const [_, indent, content] = line.match(/^(\s*)- (.+)$/);
                  const level = indent.length / 2; // インデントの深さを計算

                  // 新しいレベルを開始
                  while (currentListLevel < level) {
                    html += '<ul>';
                    listStack.push('</ul>');
                    currentListLevel++;
                  }

                  // レベルを閉じる
                  while (currentListLevel > level) {
                    html += listStack.pop();
                    currentListLevel--;
                  }

                  html += `<li>${content}</li>`;
                }
                // 番号付きリスト（1. ）のインデント対応
                else if (/^(\s*)\d+\. (.+)$/.test(line)) {
                  const [_, indent, content] = line.match(/^(\s*)(\d+\. .+)$/);
                  const level = indent.length / 2;

                  // 番号を取得
                  const number = parseInt(content.match(/^(\d+)/)[0]);

                  // 最初のレベルでolタグがない場合は作成
                  if (currentOrderedListLevel === 0 && orderedListStack.length === 0) {
                    html += '<ol class="markdown">';
                    orderedListStack.push('</ol>');
                    currentOrderedListLevel = 0;
                  }

                  // インデントレベルに応じてネストを処理
                  while (currentOrderedListLevel < level) {
                    if (currentOrderedListLevel === level - 1) {
                      // 直前の</li>を削除して新しいネストを開始
                      html = html.replace(/<\/li>$/, '');
                      html += `<ol class="markdown" start="${number}">`;
                      orderedListStack.push('</ol></li>');
                    } else {
                      html += `<li><ol class="markdown" start="${number}">`;
                      orderedListStack.push('</ol></li>');
                    }
                    currentOrderedListLevel++;
                  }

                  while (currentOrderedListLevel > level) {
                    html += orderedListStack.pop();
                    currentOrderedListLevel--;
                  }

                  html += `<li>${content.replace(/^\d+\. /, '')}</li>`;
                }
                // リスト以外の要素に移る場合、開いているリストをすべて閉じる
                else if (currentListLevel > 0 || currentOrderedListLevel > 0 || currentCheckListLevel > 0) {
                  // 順番を逆にして、内側のリストから閉じていく
                  while (currentOrderedListLevel >= 0 && orderedListStack.length > 0) {
                    html += orderedListStack.pop();
                    currentOrderedListLevel--;
                  }
                  while (listStack.length > 0) {
                    html += listStack.pop();
                  }
                  while (checkListStack.length > 0) {
                    html += checkListStack.pop();
                  }
                  currentListLevel = 0;
                  currentOrderedListLevel = 0;
                  currentCheckListLevel = 0;
                }
                // 段落 -> <p>
                else if (/^(.+)$/.test(line) && !/^#/.test(line)) {
                  const content = line.match(/^(.+)$/)[1];
                  html += `<p>${content}</p>`;
                }
                // 空行 -> <p style="margin-bottom: 1.35rem"></p>
                else if (/^\s*$/.test(line)) {
                  html += `<p style="margin-bottom: 1.35rem"></p>`;
                }
                // 水平線 (---) -> <hr class="horizontal-rule">
                else if (/^---$/.test(line)) {
                  html += `<hr class="horizontal-rule">`;
                }
                // テーブル処理
                else if (/^\|(.+)\|$/.test(line)) {
                  const cells = line.match(/\|(.+)\|/)[1].split('|').map(cell => cell.trim());

                  // アライメント行の処理
                  if (/^[\s:|-]+$/.test(cells[0])) {
                    tableAlignments = cells.map(cell => {
                      if (cell.startsWith(':') && cell.endsWith(':')) return 'center';
                      if (cell.endsWith(':')) return 'right';
                      return 'left';
                    });
                    return;
                  }

                  // テーブルの開始
                  if (!isInTable) {
                    html += '<table class="table is-bordered is-hoverable"><tbody>';
                    isInTable = true;
                  }

                  html += '<tr>';
                  cells.forEach((cell, index) => {
                    const align = tableAlignments[index] || 'left';
                    if (tableRowCount === 0) {
                      // ヘッダー行
                      html += `<th align="${align}">${cell}</th>`;
                    } else {
                      // データ行
                      html += `<td align="${align}">${cell.replace(/ /g, '&nbsp;')}</td>`;
                    }
                  });
                  html += '</tr>';
                  tableRowCount++;
                }
                // テーブル以外の要素の場合、テーブルを閉じる
                else {
                  if (isInTable) {
                    html += '</tbody></table>';
                    isInTable = false;
                    tableRowCount = 0;
                    tableAlignments = [];
                  }
                  // 引用（> ） -> <blockquote class="markdown"><p>
                  if (/^> (.+)$/.test(line)) {
                    const content = line.match(/^> (.+)$/)[1];
                    html += `<blockquote class="markdown"><p>${content}</p></blockquote>`;
                  }
                  // コードブロック（``` ） -> <pre><code>
                  else if (/^```(.+)$/.test(line)) {
                    const content = line.match(/^```(.+)$/)[1];
                    html += `<pre><code>${content}</code></pre>`;
                  }
                  // インラインコード（` `） -> <code>
                  else if (/`(.+)`/.test(line)) {
                    const content = line.match(/`(.+)`/)[1];
                    html += `<code>${content}</code>`;
                  }
                  // リンク（[ ]( )） -> <a>
                  else if (/\[(.+)\]\((.+)\)/.test(line)) {
                    const content = line.match(/\[(.+)\]\((.+)\)/)[1];
                    const url = line.match(/\[(.+)\]\((.+)\)/)[2];
                    html += `<a href="${url}">${content}</a>`;
                  }
                  // 画像（![ ]( )） -> <img>
                  else if (/!\[(.+)\]\((.+)\)/.test(line)) {
                    const alt = line.match(/!\[(.+)\]\((.+)\)/)[1];
                    const url = line.match(/!\[(.+)\]\((.+)\)/)[2];
                    html += `<img src="${url}" alt="${alt}">`;
                  }
                  // その他 -> 入力されたテキストをそのまま出力
                  else {
                    html += line;
                  }
                }
              }
            }
          }
        });

        // 閉じていないリストを閉じる
        while (listStack.length > 0) {
          html += listStack.pop();
        }
        while (orderedListStack.length > 0) {
          html += orderedListStack.pop();
        }
        while (checkListStack.length > 0) {
          html += checkListStack.pop();
        }

        // 閉じていない引用を閉じる
        while (quoteStack.length > 0) {
          html += quoteStack.pop();
        }

        // 最後のテーブルが残っている場合の処理
        if (tableRows.length > 0) {
          const alignments = tableRows[1].map(cell => {
            if (cell.startsWith(':') && cell.endsWith(':')) return 'center';
            if (cell.endsWith(':')) return 'right';
            return 'left';
          });

          html += '<table class="table is-bordered is-hoverable"><tbody>';
          html += '<tr>';
          tableRows[0].forEach((cell, i) => {
            html += `<th align="${alignments[i]}">${cell}</th>`;
          });
          html += '</tr>';

          tableRows.slice(2).forEach(row => {
            html += '<tr>';
            row.forEach((cell, i) => {
              html += `<td align="${alignments[i]}">${cell.replace(/ /g, '&nbsp;')}</td>`;
            });
            html += '</tr>';
          });

          html += '</tbody></table>';
        }

        return html;
      }

    });
  })();
</script>
```