MarkdownをHtmlにした時に頑張っていっぱい書いた改行が全部消されてしまう問題について
Nuxt/Contentに限らず、Markdownを表示する際に入力した改行がそのまま表示されず。入力と表示に(感覚的に)齟齬が出ます。
それについて!抗おう!とした記事です。
本当はparserの中身をしっかり理解するべきなんですが、今回はそこまでしなくても結果を得られるかなと思い、実際の表示の変化を調べてみました。
本記事中での説明について
Nuxt/Contentはv2のことです
表示はNuxt/Contentの初期状態で確認しています。
怪しい部分はQiitaの下書きでも同じ表示になるか確認しています。
そもそも仕様である
1回の改行が無視され、1つの空行になってしまうのは
仕様です
これについては、remark-breaks
というプラグインがあり、nuxt/contentに取り込むことで解決できます。
連続した改行が一つにまとめられるのは
仕様です
これについては、特にこれといった解決方法はないようです。
英語圏などでは文章の空きで何かを表現するということがないのだと思います。
諦めるのが一番簡単です。
しかし今回はなるべく諦めない方向で行こうと思います!
技術系の記事でこんな空白が使用されていたら即閉じですけど、楽天市場の商品ページみたいな記事をmarkdownで書きたいときもあります。
markdownはどう出力されるのか?
以下の.mdを表示するとhtmlがどうなるかを確認してみます。
※remark-breaks
は入っていません
対象のmdファイル、結果のhtml画面のスクショという構成で見ていきます。
1 単体の\r\n
# Markdownの表示を調整するぞい
haihoi
hoihoi
zoizoi
zoizoizoi
zoizoizoi
\r\nがスペースになり、すべて連続で表示されています。
2 連続した\r\n\r\n
# Markdownの表示を調整するぞい
haihoi
hoihoi
zoizoi
zoizoizoi
zoizoizoi
連続した改行をしたところで別の<p>タグに分離されました。
3回以上連続した改行を入れてもhtml構成は変わりません。
3 \r\n<br>
# Markdownの表示を調整するぞい
haihoi
hoihoi
<br>
zoizoi
zoizoizoi
zoizoizoi
先ほどは連続した\r\nによってpタグが分離していましたが<br>での改行では分離されず1つのpタグ内におさまります。
今回はほとんど表示の見た目は変わっていませんが、pタグにmarginを効かせたりしていた場合は見た目にも差が出ます。
そしてよく見ると zoizoi
となっています、<br>
直後の\r\nがスペースになって先頭に表示されてそうです。
4 <br>の連続
# Markdownの表示を調整するぞい
haihoi
hoihoi
<br>
<br>
<br>
zoizoi
zoizoizoi
zoizoizoi
連続した\r\nがないので1つのpタグでまとまったままです、pタグの中に<br>
が3つ入ります。
5 \r\n\r\n<br>\r\n\r\n
# Markdownの表示を調整するぞい
haihoi
hoihoi
<br>
zoizoi
zoizoizoi
zoizoizoi
<br>の前後両方に連続した改行があるので、それぞれがpタグで分離され、その間に<br>が単体で挿入されます。
6. \r\n\r\n<br>\r\n
# Markdownの表示を調整するぞい
haihoi
hoihoi
<br>
zoizoi
zoizoizoi
zoizoizoi
これはよく見ると不思議な構成になってます。
\r\n\r\nによってpタグが終了しますが、<br>\r\nの直後のテキストがpタグで囲まれずに始まっています。
よくみると zoizoi
と\r\nがスペースになったような跡もあります。
7.\r\n\r\n<br>
# Markdownの表示を調整するぞい
haihoi
hoihoi
<br>zoizoi
zoizoizoi
zoizoizoi
この記述だと結果が変わりました。
\r\n\r\nによってpタグが終了し、 <br>が次のpタグの先頭に挿入される形ではじまっています。
8.\r\n\r\n<br><br>\r\n
# Markdownの表示を調整するぞい
haihoi
hoihoi
<br><br>
zoizoi
zoizoizoi
zoizoizoi
\<br>
を2つ重ねるとpタグで囲まれます
入力と出力まとめ
-
\r\n
はスペース
になる -
<br>
はpタグ
を終了させない -
\r\n\r\n
はpタグ
を終了させ、ほとんどの場合で次のpタグ
が開始される -
\r\n\r\n
の直後の<br>
は以下のようなパターンに分類できる-
<br>\r\nTEXT
- 終了したp直後に
<br>
がpタグで囲まれず直接置かれる、\r\n
に続くテキストもpタグで囲まれない 🙅
- 終了したp直後に
-
<br><br>
- 終了したp直後に
<br><br>
がpタグで囲まれる 🙅
- 終了したp直後に
-
<br>TEXT
- 終了したp直後に
<br>TEXT
がpタグで囲まれる 🙅
- 終了したp直後に
-
<br>\r\n\r\nText
- 直接置かれた
<br>
の後に、TEXTがpタグで囲まれて配置される🙆
- 直接置かれた
-
つまりこういうこと
\r\nのカウント数-2の<br>\r\n
を \r\n\r\n
と\r\n\r\n
の間に挿入する
\r\n → \r\n
\r\n\r\n → \r\n\r\n
\r\n\r\nr\n → \r\n\r\n<br>\r\n\r\n\r\n
\r\n\r\n\r\nr\n → \r\n\r\n<br>\r\n<br>\r\n\r\n\r\n
変換してみる
ここから先は変換処理をコードで書いていきます、コードが汚いのはご容赦ください。
defineNitroPlugin内にhookが用意されている
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('content:file:beforeParse', (file) => {
if (file._id.endsWith('.md')) {
file.body = file.body.replace(/react/g, 'vue')
}
})
})
上記のような形で、NuxtContentがマークダウンをparseする前にマークダウンに手を加えられる、これを利用して連続した改行を<br>
に置換する処理を入れていきます
① frontmatterを分離する
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook(
"content:file:beforeParse",
(file: { _id: string; body: string }) => {
if (file._id.endsWith(".md")) {
const text = file.body
let returnMarkdown = ""
console.log(file._id)
//frontMatterとmarkdownを分離する
const frontmatterReg = /^(?=---)---[\s\S]+?(?<=---)/
const frontmatter = text.match(frontmatterReg)?.[0] ?? ""
returnMarkdown += frontmatter
const markdown = text.replace(frontmatterReg, "")
//markdown本体を整形する
returnMarkdown += modifyBody(markdown)
//整形したbodyに変更する
file.body = returnMarkdown
}
}
)
})
nuxt/contentで表示するmarkdownにはfrontmatterと呼ばれるエリアを書くことができます、そこについては手を加える必要がないので分離します。
分離したmarkdownについては、modifyBody()
内で処理します。
② コードブロックを除外する
function modifyBody(body: string) {
let modifiedBody = ""
const codeblockReg =
/(?=`````)`````[\s\S]+?(?<=`````)|(?=````)````[\s\S]+?(?<=````)|(?=```)```[\s\S]+?(?<=```)|(?=``)``[\s\S]+?(?<=``)|(?=`)`[\s\S]+?(?<=`)/g
let noncodeIndex = 0
let match
while ((match = codeblockReg.exec(body)) !== null) {
if (noncodeIndex < match.index) {
const noncode = body.slice(noncodeIndex, match.index)
modifiedBody += replaceNoncode(noncode)
}
noncodeIndex = codeblockReg.lastIndex
const code = body.slice(match.index, codeblockReg.lastIndex)
modifiedBody += code
}
const rest = body.slice(noncodeIndex, body.length)
modifiedBody += replaceNoncode(rest)
return modifiedBody
}
markdown本体を整形したいのですが、markdown本体の中でも整形してはいけないエリアがあります。
コードブロック
やインラインコード
です、そこで正規表現によってコードブロックの部分を判断します。
コードブロック外の部分は整形したいのでreplaceNoncode()
で整形します、コードブロック内はそのままにします
コードブロック外、という正規表現を書こうとしたのですがこれが難しくて諦めました
③ コードブロックではない部分の改行を置換する
function replaceNoncode(text: string): string {
const multiNewlinesReg = /((\r\n){3,})/g
const countReg = /(?<=(\r\n))(\r\n)(?=(\r\n))/g
text = text.replace(multiNewlinesReg, (match) => {
const count = (match.match(countReg) ?? []).length
const replaceText = `\r\n\r\n${"<br>\r\n".repeat(count)}\r\n\r\n`
return replaceText
})
return text
}
replaceNoncodeに渡される文字列は改行コードを変更していい部分ということになっているので、変更していきます。
3回以上\r\n
が連続している部分をstring.replace()で変換していきます。
replacerには関数を渡せるので、その中で置き換え文字列を生成できます。
ここまで書けば、多分動くと思います。
④実際に結果を確認する
※動画に音はありません
実際動かしてみると出来てるようです。
注意点
改行コードに手を加えてはいけない部分としてコードブロックを挙げましたが、他にもあるかもしれません。
また、このコードではコードブロック内の判定を`
バッククォート(5つまで)のみで判断していますが、コードブロックは他の書き方もできます。
終わりに
今回はNuxt/ContentのMarkdownの改行表示を変更してみました。
これで日記で書くMarkdownの中に<br>
を書かないで済みます。