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>