0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

記事から自分のコードを抜き出したら壊れた。コードフェンスの条件を仕様と突き合わせて測った

0
Posted at

先に断っておくと、この記事に書いた条件は、すべて CommonMark の仕様に明記されています。Fenced code blocks の項です。

A fenced code block begins with a code fence, preceded by up to three spaces of indentation.
The closing code fence may be preceded by up to three spaces of indentation.

読めば分かります。読んでいませんでした。

書くのは、読まずに正規表現を書いて壊した記録と、仕様の条件をパーサで10通り測った結果です。壊れ方の実物と、直した抜き出し関数は、自分の手元にしかありません。

測定は markdown-it-py 4.0.0(CommonMark 準拠)で行いました。

何が起きたか

記事に、こういうコードを貼っていました。Markdownの記法を取り除く関数です。

CODE_BLOCK = re.compile(r'```.*?```', re.S)
INLINE = re.compile(r'`[^`]*`')

書いたあと、この記事のコードをそのまま実行して出力を確かめたくなりました。記事から抜き出す短いスクリプトを書きます。

blocks = re.findall(r'```(.*?)```', text, re.S)

抜き出した結果を実行したら、NameError: name 're' is not defined で落ちました。抜けていたのは import re です。

コードの途中から切り出されていました。

抜き出しが壊れた理由

正規表現が、コードの中に書いてあるバッククォート3つに反応しています。

非貪欲の (.*?) は、最初に見つかった閉じ記号で止まります。1行目の re.compile(r' のうしろにある3つが、そのまま終端として扱われました。

非貪欲 ```(.*?)``` の結果: 2 個 / "python\nCODE = re.compile(r'"

2個に割れています。1個目は1行に満たない断片です。

ここで一瞬、記事のほうが壊れているのではないかと思いました。表示は正常に見えていましたが、見えているものと解釈されているものが違う可能性があります。

確かめました。壊れていませんでした。

行頭かどうかで、すべてが決まる

仕様のとおり、閉じフェンスとして扱われるのは行の先頭にあるバッククォートの並びだけです。行の途中にあるものは、ただの文字です。

パーサに6通り食わせました。

中に置いた3つの位置 コードブロック数 結果
行の途中 1 壊れない
行頭 2 壊れる
半角スペース3つ字下げ 2 壊れる
半角スペース4つ字下げ 1 壊れない
外側を4本にする 1 壊れない
外側をチルダ ~~~ にする 1 壊れない

3つ字下げでも壊れるのは、閉じフェンスの字下げが3つまで許されているからです。4つ以上は行頭とみなされません。ただし、その4つのスペースはコードの中身として残ります。インデントを1段ずらして回避するのは、結果を変えてしまうので勧めません。

壊れる場合、2個目のブロックのあとに続く行が、コードブロックの外に出ます。

コード外に出た行: ['y = 2']

コードだった行が、地の文として表示されます。見た目で気づける壊れ方ではあります。

フェンスの長さには規則がある

外側を4本にする方法を選ぶなら、規則を1つ覚えておく必要があります。こちらも4通り測りました。

開き 中に出てくる並び 結果
3本 4本 壊れる
4本 4本 壊れる
4本 3本 壊れない
5本 4本 壊れない

閉じフェンスは、開きフェンスと同じ長さか、それより長ければ成立します。逆に言えば、開きより短い並びは閉じになりません。

なので、中に3本が出てくるなら外側は4本、中に4本が出てくるなら外側は5本にします。1本多ければ足ります。

チルダを使う手もあります。~~~ で開けば、中のバッククォートは何本並んでいても閉じになりません。記法が混ざるのを嫌う人もいると思うので、ここは好みだと思います。

閉じ忘れると、最後まで飲み込みます

外側を4本にしたとき、閉じも4本にする必要があります。3本で閉じても閉じフェンスになりません。

そして仕様では、閉じフェンスが見つからないまま文書の終わりに達した場合、開きフェンス以降の全部がコードブロックになります。

外側を増やして閉じ忘れると、**記事の残り全部がコードとして表示されます。**これは壊れ方としては派手なので、プレビューを見ればすぐ分かります。

もう1つ、仕様に書かれていて引っかかりやすいものがあります。バッククォートのフェンスでは、情報文字列にバッククォートを含められません。

```python `code`

これは開きフェンスとして成立しません。言語名のあとにインラインコードを書きたくなる場面はないと思いますが、自動生成で情報文字列を組み立てているなら、ここに当たる余地があります。

抜き出す側の直し方

行頭のフェンスだけを見るようにします。

import re

def extract_code_blocks(text):
    out, cur, opener = [], None, None
    for line in text.split('\n'):
        stripped = line.lstrip()
        indent = len(line) - len(stripped)
        if cur is None:
            m = re.match(r'(`{3,}|~{3,})(.*)$', stripped)
            if m and indent < 4:
                opener = m.group(1)
                cur = []
            continue
        m = re.match(r'(`{3,}|~{3,})\s*$', stripped)
        if m and indent < 4 and m.group(1)[0] == opener[0] \
                and len(m.group(1)) >= len(opener):
            out.append('\n'.join(cur))
            cur, opener = None, None
            continue
        cur.append(line)
    return out

見ている条件は4つです。字下げが4つ未満であること。開きと同じ記号であること。開き以上の長さであること。閉じフェンスの行に情報文字列が書かれていないこと。

**どれも仕様に書いてある条件を、そのまま条件式にしただけです。**先に読んでいれば、最初からこう書けていました。

同じ入力で比べると、こうなります。

非貪欲の正規表現 : 2個 / "python\nCODE = re.compile(r'"
行頭フェンス方式 : 1個 / "CODE = re.compile(r'```.*?```')\nx = 1"

コードが1つの塊として取れています。

余談

この記事にも、バッククォート3つを含むコードブロックが入っています。

書きながら、外側を4本にするかどうか迷いました。結局、そのままにしています。行の途中にしか出てこないので、壊れないことを確かめたからです。

念のため、公開後に自分でもう一度抜き出して実行してみるつもりです。仕様上は壊れないはずですが、レンダリングは仕様だけで決まるものでもないので。Qiita が CommonMark にどこまで準拠しているかは、こちらでは分かりません。

まとめ

仕様に書いてあることと、測って確かめたことを分けて並べます。

仕様に書いてあること

  • フェンスは行頭から3スペースまでの字下げを許す。4つ以上は行頭ではない
  • 閉じフェンスは、開き以上の長さが必要
  • 閉じフェンスが無いまま文書が終わると、以降が全部コードになる
  • バッククォートのフェンスでは、情報文字列にバッククォートを含められない

測って確かめたこと

  • 行の途中のバッククォート3つは、ただの文字。ブロックは1つのまま
  • 3つ字下げでは壊れ、4つ字下げでは壊れない(ただし空白が中身に残る)
  • 壊れたとき、続く行が地の文として外に出る

わたしは今回、抜き出す側だけを直しました。記事のほうは壊れていなかったので。


ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?