Markdown エディタを実装し、さらに Prism.js を適用する

<!-- markdown-mode-on --><img alt="jettheme logo" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEih6tkuCAB6zwNyTYCvTrA07cGstQGH7GBjCipsk3a6HoUiKVea-159v4oK4uq7qhXUjbbnX0Hm5Fds5sw7iiCzhqObJHwcnKQEf9-0iAqgpH6bSzH6FxQCVVBh-XrghyphenhyphenMUwaIjVewBQXI/s1600/jettheme-cover.png" style=" display: none;"/> Bloggerで、Markdown書式からHTMLに変換するクールな方法を見つけた。この方法にすれば、次のようなメリット(気持の良さ)が得られる。 1. ソースはMarkdownのままでOK(HTMLに変換もできるが、未変換のままでもHTMLに変換した文章になる。) 1. コードブロックのシンタックスハイライトにPrism.js を適用できる。(``` でコードを挟むだけで、Prism.js が適用される。) ## 今までの方法 BloggerはMarkdown記法に対応していないので、Markdownで文章を書くときには**外部エディタ**を使うか、marked.jsなどの**変換スクリプト**を使うかの2択になる。 <a name="more"></a> **1. 外部アプリのMarkdown エディタで文章を書く** <figure class="blogcard b-link"><a aria-label="記事詳細へ(別窓で開く)" href="https://www.limosuki.com/2020/01/blogger-markdown_17.html" rel="noopener noreferrer" target="_blank"><div class="blogcard-content"><div class="blogcard-image bi-link"><div class="blogcard-image-wrapper biw-link"><img alt="【Blogger】Markdown で快適・効率的に記事作成する方法 | リモスキ" height="100" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhELzA_QPc4HnTvclHzIXkHLSzWcGbeyMUcAT-CPqOMdAXZW5L39IAnCyNWF5OVU4A4wUSV1v8oa3mpUvW7s8iNLO7fvUeArwwKpt8qlHdSDcAypcR11XQE-RqwOrrjJmS_G7QHI4gvadyL/w720-h350-n-e365-rw/eyecatch-markdown2.png" width="100"/></div></div><div class="blogcard-text"><p class="blogcard-title bt-link">【Blogger】Markdown で快適・効率的に記事作成する方法 | リモスキ</p><p class="blogcard-description bd-link">Blogger で、Markdown を使って快適に記事を書く方法をまとめました。</p></div></div><div class="blogcard-footer bf-link">  <img alt="ファビコン" height="16" loading="lazy" src="https://www.google.com/s2/favicons?domain=https://www.limosuki.com/2020/01/blogger-markdown_17.html" width="16"/>www.limosuki.com</div></a></figure> **2. BloggerのHTML エディタで文章を書く(marked.jsなどのMarkdown記法の変換スクリプトを導入しておく)** <figure class="blogcard b-link"><a aria-label="記事詳細へ(別窓で開く)" href="https://qiita.com/her0m31/items/1804bdc251a647e0e9a8" rel="noopener noreferrer" target="_blank"><div class="blogcard-content"><div class="blogcard-image bi-link"><div class="blogcard-image-wrapper biw-link"><img alt="BloggerでMarkdown書けるようにした。 - Qiita" height="100" loading="lazy" src="https://blog.qiita.com/wp-content/uploads/2019/12/8c88f8f4-9783-d36c-a547-e5c799f1253f-1-1024x538.png" width="100"/></div></div><div class="blogcard-text"><p class="blogcard-title bt-link">BloggerでMarkdown書けるようにした。 - Qiita</p><p class="blogcard-description bd-link">#Markdownめっちゃ良い。さいきん、Qiitaに初投稿してみました。kimonolabsの使い方 基礎編実は、Markdownで何か書く体験も、これが初めてだったりします。めっちゃ良い…</p></div></div><div class="blogcard-footer bf-link">  <img alt="ファビコン" height="16" loading="lazy" src="https://www.google.com/s2/favicons?domain=https://qiita.com/her0m31/items/1804bdc251a647e0e9a8" width="16"/>qiita.com</div></a></figure> いずれの方法でも、完成までには次の手順を加えねばならない。したがって、できるだけ一元管理できる一貫性のある方法がのぞましい。 1. 書いた文章を HTML に変換、コピーして、Blogger 投稿画面に貼り付け 1. 使いたい画像を Blogger にアップし、記事内に挿入していく 1. Blogger のプレビューで内容を確認しながら最終調整し、更新 ## 変更した方法 前項の **2. BloggerのHTML エディタで文章を書く(marked.jsなどのMarkdown記法の変換スクリプトを導入しておく)** をもっと便利にした方法になる。 次のサイトの説明にしたがって導入すれば、HTMLエディタの最初に`<!--markdown-mode-on-->`を書くだけで、 - Markdownで文章が書ける。 - 完成形はプレビューで確認できる。 - このまま、「公開」すれば、自動でMarkdownからHTMLに変換される。 ようになる。さらに、当該サイトの説明を引用すれば、 >Blogger に Markdown エディタを実装する上でこだわったのは以下の3点です。 >- Markdown を投稿エディタに直接記述できる(textarea などのなかに記述しない) >- 変換された HTML をコピーできる >- Markdown で書くときだけ Markdown スクリプトを読み込む <figure class="blogcard b-link"><a aria-label="記事詳細へ(別窓で開く)" href="https://mizunosame.blogspot.com/2024/11/blogger-markdown-editor.html" rel="noopener noreferrer" target="_blank"><div class="blogcard-content"><div class="blogcard-image bi-link"><div class="blogcard-image-wrapper biw-link"><img alt="Blogger の投稿エディタを Markdown エディタに改造する" height="100" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj60qc5kpeRApV8VLLcKnS7VmNLLIVTIc4wqlzS18t5IS_Q6_ZSYCu_kdatXv3QxAHE_wgFPSv6aARDyihA5kjs8a4BQSMF4sCThN94FrPFPRqJQLCMpjb6P6_aussmtOkkpkBm2ytTr-yRjBTqQZAQ1D3nPIK09cSJTGajWwYTtyC7tW-ON00ZY3UKFdg/w1200-h630-p-k-no-nu/blogger-markdown-html.png" width="100"/></div></div><div class="blogcard-text"><p class="blogcard-title bt-link">Blogger の投稿エディタを Markdown エディタに改造する</p><p class="blogcard-description bd-link">Markdown で記事を書くことが多いのですが、現在お世話になっている Blogger の投稿エディタは残念ながら Markdown に対応していません。 そのため、Markdown で記事を書くときは以下のような手順を踏んでいます。 Markdown エディタで記...</p></div></div><div class="blogcard-footer bf-link">  <img alt="ファビコン" height="16" loading="lazy" src="https://www.google.com/s2/favicons?domain=https://mizunosame.blogspot.com/2024/11/blogger-markdown-editor.html" width="16"/>mizunosame.blogspot.com</div></a></figure> 導入して気持ち良く使ってるのだが、ときどき、MarkdownからHTMLの自動変換がおかしくなる(プレビューが乱れる)時がある。原因不明なのだが、その場合は、**再度、HTMLエディタの最初に`<!--markdown-mode-on-->`を書き直す**と直る。 ## [Markdown エディタのカスタマイズ](https://mizunosame.blogspot.com/2024/11/blogger-markdown-editor.html#toc-4) 引用元サイトでは、 >marked.js には、出力される HTML を自分好みに上書きできる **renderer** という拡張機能があります。 と説明されているので、これを適用した。さらに、この **renderer** を使って、コードブロックのシンタックスハイライトに**自動でPrism.js を適用する**改良を加えた。 --- ### **Markdown に Prism.js を適用する手順** 以下は、<svg height="24" style="flex: 0 0 auto; line-height: 1;" viewbox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><title>Copilot</title><defs><radialgradient cx="85.44%" cy="100.653%" fx="85.44%" fy="100.653%" gradienttransform="scale(-.8553 -1) rotate(50.927 2.041 -1.946)" id="lobe-icons-copilot-fill-0" r="105.116%"><stop offset="9.6%" stop-color="#00AEFF"></stop><stop offset="77.3%" stop-color="#2253CE"></stop><stop offset="100%" stop-color="#0736C4"></stop></radialgradient><radialgradient cx="18.143%" cy="32.928%" fx="18.143%" fy="32.928%" gradienttransform="scale(.8897 1) rotate(52.069 .193 .352)" id="lobe-icons-copilot-fill-1" r="95.612%"><stop offset="0%" stop-color="#FFB657"></stop><stop offset="63.4%" stop-color="#FF5F3D"></stop><stop offset="92.3%" stop-color="#C02B3C"></stop></radialgradient><radialgradient cx="82.987%" cy="-9.792%" fx="82.987%" fy="-9.792%" gradienttransform="scale(-1 -.9441) rotate(-70.872 .142 1.17)" id="lobe-icons-copilot-fill-4" r="140.622%"><stop offset="6.6%" stop-color="#8C48FF"></stop><stop offset="50%" stop-color="#F2598A"></stop><stop offset="89.6%" stop-color="#FFB152"></stop></radialgradient><lineargradient id="lobe-icons-copilot-fill-2" x1="39.465%" x2="46.884%" y1="12.117%" y2="103.774%"><stop offset="15.6%" stop-color="#0D91E1"></stop><stop offset="48.7%" stop-color="#52B471"></stop><stop offset="65.2%" stop-color="#98BD42"></stop><stop offset="93.7%" stop-color="#FFC800"></stop></lineargradient><lineargradient id="lobe-icons-copilot-fill-3" x1="45.949%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#3DCBFF"></stop><stop offset="24.7%" stop-color="#0588F7" stop-opacity="0"></stop></lineargradient><lineargradient id="lobe-icons-copilot-fill-5" x1="83.507%" x2="83.453%" y1="-6.106%" y2="21.131%"><stop offset="5.8%" stop-color="#F8ADFA"></stop><stop offset="70.8%" stop-color="#A86EDD" stop-opacity="0"></stop></lineargradient></defs><g fill="none" fill-rule="nonzero"><path d="M17.533 1.829A2.528 2.528 0 0015.11 0h-.737a2.531 2.531 0 00-2.484 2.087l-1.263 6.937.314-1.08a2.528 2.528 0 012.424-1.833h4.284l1.797.706 1.731-.706h-.505a2.528 2.528 0 01-2.423-1.829l-.715-2.453z" fill="url(#lobe-icons-copilot-fill-0)" transform="translate(0 1)"></path><path d="M6.726 20.16A2.528 2.528 0 009.152 22h1.566c1.37 0 2.49-1.1 2.525-2.48l.17-6.69-.357 1.228a2.528 2.528 0 01-2.423 1.83h-4.32l-1.54-.842-1.667.843h.497c1.124 0 2.113.75 2.426 1.84l.697 2.432z" fill="url(#lobe-icons-copilot-fill-1)" transform="translate(0 1)"></path><path d="M15 0H6.252c-2.5 0-4 3.331-5 6.662-1.184 3.947-2.734 9.225 1.75 9.225H6.78c1.13 0 2.12-.753 2.43-1.847.657-2.317 1.809-6.359 2.713-9.436.46-1.563.842-2.906 1.43-3.742A1.97 1.97 0 0115 0" fill="url(#lobe-icons-copilot-fill-2)" transform="translate(0 1)"></path><path d="M15 0H6.252c-2.5 0-4 3.331-5 6.662-1.184 3.947-2.734 9.225 1.75 9.225H6.78c1.13 0 2.12-.753 2.43-1.847.657-2.317 1.809-6.359 2.713-9.436.46-1.563.842-2.906 1.43-3.742A1.97 1.97 0 0115 0" fill="url(#lobe-icons-copilot-fill-3)" transform="translate(0 1)"></path><path d="M9 22h8.749c2.5 0 4-3.332 5-6.663 1.184-3.948 2.734-9.227-1.75-9.227H17.22c-1.129 0-2.12.754-2.43 1.848a1149.2 1149.2 0 01-2.713 9.437c-.46 1.564-.842 2.907-1.43 3.743A1.97 1.97 0 019 22" fill="url(#lobe-icons-copilot-fill-4)" transform="translate(0 1)"></path><path d="M9 22h8.749c2.5 0 4-3.332 5-6.663 1.184-3.948 2.734-9.227-1.75-9.227H17.22c-1.129 0-2.12.754-2.43 1.848a1149.2 1149.2 0 01-2.713 9.437c-.46 1.564-.842 2.907-1.43 3.743A1.97 1.97 0 019 22" fill="url(#lobe-icons-copilot-fill-5)" transform="translate(0 1)"></path></g></svg> Copilot の説明。 #### **1. Prism.js のスクリプトと CSS を追加** Prism.js を読み込むため、ページの `<head>` や適切な場所に以下のコードを追加してください。 ```html <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet"/> <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-html.min.js"></script> ``` - **Prism.js の CSS (`prism-tomorrow.min.css`) を追加 → コードブロックの見た目を整える** - **Prism.js のスクリプト (`prism.min.js`) を追加 → ハイライト処理を実装** - **各プログラミング言語のコンポーネントを読み込む(例:JavaScript, HTML)** --- #### **2. `renderer.code()` の修正** Markdown のコードブロックを **Prism.js に適用できる形式に変換する** よう、`renderer.code()` を修正します。 ```javascript const renderer = { code({ text, lang }) { const validLanguage = lang ? `language-${lang}` : 'language-plaintext'; // 言語未指定ならプレーンテキスト const code = escapeHTML(text); // HTML の特殊文字をエスケープ return `\n<pre><code class="${validLanguage}">${code}</code></pre>\n\n`; }, }; ``` - **`language-xxx` のクラスを付与 → Prism.js に認識される** - **`escapeHTML(text)` を適用 → `<h1>` などのタグがコードとして正しく表示される** - **最後の `},` でオブジェクトを閉じない**(**原因となっていた `};` を排除!**) --- #### **3. `marked.use({renderer})` を適用** Markdown の変換時に **カスタム `renderer` を適用** するため、`marked.use({renderer})` を適用します。 ```javascript marked.use({ renderer }); const converted = marked.parse(input.textContent.replace(comment, '')); input.innerHTML = converted; input.classList.remove('md-input'); ``` - **Markdown のコードブロックを `<pre><code class="language-xxx">...</code></pre>` に変換** - **既存の `marked.use({renderer})` を変更せず、そのまま適用** --- #### **4. ページ読み込み後に Prism.js を適用** コードブロックが **変換された後に** Prism.js のハイライトを適用するため、以下のスクリプトを追加します。 ```html <script> document.addEventListener("DOMContentLoaded", function() { Prism.highlightAll(); }); </script> ``` - **ページが完全に読み込まれた後に `Prism.highlightAll();` を実行** - **変換後のコードブロックにシンタックスハイライトを適用** --- #### **5. Markdown のコードブロックの動作確認** Markdown で以下のコードを書く: ```markdown ```html <h1>タイトル</h1> ``` ``` 変換後の HTML が **以下のように正しく表示される** ことを確認: ```html <pre><code class="language-html"> <h1>タイトル</h1> </code></pre> ``` - **特殊文字 (`<h1>` など) が正しくエスケープされてコードブロックとして表示される** - **Prism.js により `h1` タグがシンタックスハイライトされる** - **HTML タグが実際の見出しではなく、コードとして表示される** --- ### **最終まとめ** 🟢 **Markdown のコードブロックを Prism.js でハイライトするには、以下を実施:** 1️⃣ **Prism.js のスクリプトと CSS を読み込む** 2️⃣ **`renderer.code()` を修正し、`class="language-xxx"` を適用** 3️⃣ **`marked.use({renderer})` を適用し、Markdown 変換時に実行** 4️⃣ **ページロード後に `Prism.highlightAll();` を実行** 5️⃣ **Markdown で書いたコードブロックが正しく変換されることを確認** ### Markdown エディタを実装し、さらに Prism.js を適用するスクリプト 上記の2項から4項を適用したカスタマイズスクリプトを示す。 <details><summary><strong>カスタマイズスクリプト</strong></summary> ```javascript <b:comment>Markdown エディタ</b:comment> <b:with value='"<!-- markdown-mode-on -->"' var="md_comment"> <b:with value="data:post.body contains data:md_comment" var="md_enabled"> <b:comment>メッセージ</b:comment> <b:if cond="data:view.isPreview"> <div class="md-message"> <b:if cond="data:md_enabled"> <p>Markdown モードがオンになっています。オフにしたい場合は投稿から <input expr:value="data:md_comment.escaped" type="text"/> を削除してください。</p> <details class="md-convert"> <summary>変換後の HTML</summary> <textarea aria-label="変換後の HTML" class="md-output"> <button aria-label="コードを全選択する" class="md-select">コード全選択</button> </textarea></details> <b:else></b:else> <p>Markdown モードをオンにしたい場合は投稿に <input expr:value="data:md_comment.escaped" type="text"/> を追加してください。</p> </b:if> </div> </b:if> <b:comment>投稿, ページ本文</b:comment> <div class="entry-text text-break mb-5" id="post-body"> <b:if cond="data:md_enabled"> <b:class cond="data:md_enabled" name="md-input"></b:class> <data:post.body.escaped></data:post.body.escaped> <b:else></b:else> <data:post.body></data:post.body> </b:if> </div> <b:comment>Markdown 変換</b:comment> <b:if cond="data:md_enabled"> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script> (() => { const comment = '<data:md_comment/>'; //<![CDATA[ const input = document.querySelector('div.md-input'); const output = document.querySelector('textarea.md-output'); const select = document.querySelector('button.md-select'); if(!input) return; const escapeHTML = (text) => { return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') } const renderer = { code({ text, lang }) { const validLanguage = lang ? `language-${lang}` : 'language-plaintext'; // 言語が未指定なら plaintext const code = escapeHTML(text); return `\n<pre><code class="${validLanguage}">${code}</code></pre>\n\n`; }, blockquote({tokens}){ const body = this.parser.parse(tokens).replace(/\n$/, ''); return `\n<blockquote>${body}</blockquote>\n\n`; }, heading({tokens, depth}){ return `\n<h${depth}>${this.parser.parseInline(tokens)}</h${depth}>\n\n`; }, hr(token){ return '\n<hr/>\n\n'; }, list(token){ const ordered = token.ordered; const start = token.start; let body = ''; token.items.forEach(item => { body += ' ' + this.listitem(item); }) const type = ordered ? 'ol' : 'ul'; const startAttr = (ordered && start !== 1) ? (' start="' + start + '"') : ''; return '\n<' + type + startAttr + '>\n' + body + '</' + type + '>\n\n'; }, paragraph({tokens}){ const text = this.parser.parseInline(tokens); const regex = /^\s*(<a name=["']more['"]>).*?(<\/a>)\s*$/; if(regex.test(text)){ return `\n${text.replace(regex, '$1$2')}\n`; }else{ return `<p>${text}</p>\n`; } } } marked.use({ renderer }); const converted = marked.parse(input.textContent.replace(comment, '')); input.innerHTML = converted; input.classList.remove('md-input'); if(output){ output.value = converted.replace(/<a name=["']more["']>.*?<\/a>/, '<!--more-->').replace(/\n{3,}/g, '\n\n').trim(); } if(!select) return; select.addEventListener('click', () => { output.focus(); output.setSelectionRange(0, output.value.length); }); })(); //]]></script> </b:if> </b:with> </b:with> ``` </details> ## 関連リンク </h1></h1></head>
Next Post Previous Post