PythonでinlineのScrapbox記法をMarkdown記法に変換する正規表現
作業過程やらつぶやきやら全部入りなのでごちゃごちゃしている
hr.icon
コードブロックの開始
for repl.it
code:py
import re
s = '''画像のテスト
code:python
import os
print('environment variables')
print(os.environ)
code:環境変数.py
import os
print('environment variables')
print(os.environ)
code:js
// code:js と書いてます
console.log('yeah!')
code:javascript
// code:javascript と書いてます
console.log('yeah!')
code:ahk
; GitHubではハイライト対応しているはずです
::d[[::
FormatTime, now,, yyyy/MM/dd
Clipboard = %now%
Send,^v
;SendInput %now%
return
ネストしたコード
バッチリです
code:py
print('hello world.')
ほらね
end'''
lines = s.split('\n')
RE_CODE_BLOCK_START = re.compile(r'^( )*code\:(.+)$')
for i,line in enumerate(lines):
newline = line
newline = re.sub(RE_CODE_BLOCK_START, '`\\2', newline)
if line==newline:
continue
print('{}: {}'.format(i, newline))
ああ、そうか
code:xxxの行も in code block判定されちゃうんだ
画像
とりあえず
code:py
EXTENSION = '.jpg'
newline = re.sub(
RE_LINK_IMAGE,
'![]({}\\2{})'.format(IMAGE_URL_PREFIX, EXTENSION),
newline
)
jpg決め打ち
code:py
import re
s = '''画像のテスト
画像 noindent
https://gyazo.com/e4aae2345d1927c777db267138d1e419
画像 indent
https://gyazo.com/639242beda8d44936421325524cd99f3
https://gyazo.com/777cfb7cd2528ebf90db1617ed659a40
ユーザーアイコンはどうしましょうかsta.icon
わかるー(x3 してます)sta.icon*3
わかるーsta.iconsta.iconsta.icon
end'''
lines = s.split('\n')
for i,line in enumerate(lines):
EXTENSION = '.jpg'
newline = re.sub(
RE_IMAGE,
'![]({}\\2{})'.format(IMAGE_URL_PREFIX, EXTENSION),
line
)
if line==newline:
continue
print('{}: {}'.format(i, newline))
あとsta.icon ← これよ
バリエーションは
sta.icon [sta.icon]
sta.icon*2 [sta.icon*2]
sta.iconsta.icon*3 [sta.icon][sta.icon*3]
存在しないページ.icon [存在しないページ.icon]
test.icon [test.icon] 存在するけど画像がない
これパッとは浮かばないな
[xxx.icon]
xxxのページを見に行く
一行目からパースしていって画像にヒットするのを待つ
見つけたら、rawlinkに加工
<img src="ここにrawlink入れて" />
つまりこう
<img src="https://i.gyazo.com/e4aae2345d1927c777db267138d1e419.jpg" width="64" height="64"/>
問題
画像の縦横比を考慮しないと表示が潰れる
SVGはどうするねん?(imgタグ非対応)
いったん32x32で決め打ちする
実装上の問題
xxxの画像 ← これどうやって手に入れるか
scb_to_markdown_in_line()の仕事ではない
っつーことは、もっとメタな処理にすべきってことだよな
そのままだと[xxx.icon]([xxx.icon.md)になっちゃうから、これはガードするとして
]内の末尾が .icon あるいは \*[0-9]+ ならガード
ガード
RE_LINK_ANOTHER_PAGE = re.compile(r'\[([^\-\*/])(.+?)\]([^\(]|$)')
これをさらに加工しなきゃいけないのか、ぐー
自分で書いた正規表現だけど、もう読めないww
code:読み解く
^^----------^^^^^--^^^^^^^^^
1 2 3 4 5
2 は [* と [- を除いてる
5 は A or B の形でどちらも除外してる
Aでは []() のような markdown link が来るパターン
Bでは行末
3 の部分に対して、.icon\]と\*[0-9]+\]を除くように加えればいい
(.+?)★\]
(\.icon|\*[0-9]+)
↓
(.+?)(\.icon|\*[0-9]+)\]
これで良い?
非貪欲が壊れそうな気がしないでもない
駄目やsta.icon
普通のリンクはactualが[Link](Link.md)じゃなくて[Link]になる
icon記法はactualが変更なしじゃなくて[sta](sta.md)になる
待てよ、非貪欲なくてもいける?
いや駄目か
たとえばsta.iconこんなふうにsta.icon*2何個も使ってる場合にsta.icon死ぬ
たとえば[sta.icon]こんなふうに[sta.icon*2]何個も使ってる場合に[sta.icon]死ぬ
つまり
(.+?) この非貪欲を維持したまま、
(\.icon|\*[0-9]+) これを注入する必要がある
sta.icon「??」
これは?
code:py
RE_LINK_ANOTHER_PAGE = re.compile(r'\[(^\-\*/)(.+[\.icon|\*0-9]+?)\](^\(|$)') newline = re.sub(RE_LINK_ANOTHER_PAGE, '\\1\\2(\\1\\2.md)\\3', newline) icon記法のスルーはできたが、普通のリンクもスルーされてしまう
いや、違うわ
.icon あるいは .icon*x
\.icon|\.icon\*[0-9]+
つまり
(.+?) この非貪欲を維持したまま、
\.icon|\.icon\*[0-9]+ これを注入する必要がある
否定的先読み?
「abc」という文字列で終わらない行にマッチする。
^(?!.*abc$).*$
先読みやっぱり必要そうだなぁ
んー、わけわからんな、これは気合要るsta.icon
だめ。今の俺じゃ追いつかんsta.icon*2
あとから機械的に取り除いてやるか
[sta.icon](sta.icon.md)や[sta.icon*3](sta.icon*3.md)を
[sta.icon]や[sta.icon*3]に戻す
むしろそうするべきでは?
scb_to_markdown_in_line()はscb記法をmarkdownに変える存在
あ、いやでもicon記法もscb記法の一種だよな
ならサポートしてもいい
が、フルサポートするためには「別ページに載ってる画像のpermalinkを取ってくる」処理が必要
これは明らかに違う
ここでできるのは
icon記法を(メタで処理するために)放置しておく
icon記法を(メタで処理できることを信じて)そのまま処理しちゃう
……どっちも本質変わらなくない?
だったらシンプルにした方がいい
まとめます
追加で以下が必要です
icon記法をパースして<img>タグに置換し、(その中身を)手に入れてた情報を使って仕上げる
が、これはscb_to_markdown_in_line()より上のレイヤーの仕事なので、ここではしません
一言でまとめると
開始記号のネスト is 手強い
[- [リンクに対するstrike]]
実装できたはずだが上手くいかない。今こうなる
- ~~[リンクに対するstrike~~(リンクに対するstrike.md)]
小さいサンプルで試す
ああ、そうか
code:今のアルゴリズム.py
import re
RE_HASHTAG = re.compile(r'(^| )#(.+?)( |$)')
RE_LINK_ANOTHER_PROJECT = re.compile(r'\/(.+?)\') RE_LINK_ANOTHER_PAGE = re.compile(r'\[(^\-\*/)(.+?)\](^\(|$)') newline = line
newline = re.sub(RE_HASHTAG, '\\1#\\2(\\2.md)\\3', newline) newline = re.sub(RE_LINK_URL_TEXT, '\\4(http\\1://\\2)', newline) newline = re.sub(RE_LINK_TEXT_URL, '\\1(http\\3://\\4)', newline) newline = re.sub(RE_LINK_ANOTHER_PAGE, '\\1\\2(\\1\\2.md)\\3', newline) print(newline)
newline = re.sub(RE_BOLD, '**\\2**', newline)
newline = re.sub(RE_STRIKE, '~~\\2~~', newline)
print(newline)
https://gyazo.com/143bfa5643294b240a37ea4cf7362981
[- この中にあったリンク表記をmarkdownに変換しちゃったことでmarkdown記法が出現した]
sta.icon(当たり前。。。)
整理
Q:貪欲にすればよくない?
非貪欲じゃないとだめな理由あったんだっけ?
複数使われてるケースで個々を置換できないからだめです
たとえば[- こういう]ふうに[- 複数使われてる]やつ
個々を置換させるには非貪欲させるしかない
Q:非貪欲にしてるのに今はなんで動かないの?
開始記号がネストしているから
下記したように、正規表現では開始記号のネストには対処できない
再帰など工夫が必要
Q: 工夫したんじゃなかったっけ?
工夫として「先に中のリンク表記置き換えたら [ のネスト消えるやろ」と目論んだ
が、実際は消えてない
markdownに変換すると[]()
相変わらず[は残る
sta.icon(当たり前。。。)
残りの対処は?
boldやstrikeをlinkの先に展開するしかない
それがだめだったら、このパターンを捉える正規表現を別途つくるか、愚直に文字列パースして何とかする
strikeを先にする案
机上で
1: [- [リンクに対するstrike]]
2: ~~[リンクに対するstrike]~~
3: ~~[リンクに対するstrike](リンクに対するstrike.md)~~
あ、いけそうだけど
こうなった
~~[リンクに対するstrike~~](リンクに対するstrike~~.md)
sta.icon*4いや、だから先に置換しても [ の開始記号ネストしてるから正しく処理できないって言うてるやん
sta.iconもう疲れてるな。寝ようか。
このパターンを捉える正規表現を別途つくる案
[- [xxx]xxx[xxx][xxx]xxx[xxx]xxx]yyy[- [xxx]zzz]
やっぱりネスト問題絡んでだめだよな
貪欲にしたら上記みたいなケース(2個ある)だめだし
愚直に文字列パースして何とかする案
トリガー
[を見つけた後、終点来る前に2個目の[を見つけたら
Q: 3個目以上はありえる?
ありえないはず
下で調べたけど、Scrapboxは三重以上のネストは対応してない
[- [* [link]bold]strike]
どうする?
1個目の終点の]を探す
その間を打ち消す(~~と~~で囲む)
厳密には[の直後の文字次第でbold or strikeの場合分け
sta.iconうーん
各行処理するのにこの処理入れるの重くならない?
いや正規表現バンバン使ってるからいまさらか
リテラルは?
ぐあ、予想以上にしんどそうだこれ
code:py
def _scb_to_markdown_in_line_about_link_in_decoration(line):
# [- [xxx]xxx[xxx][xxx]xxx[xxx]xxx]yyy[- [xxx]zzz]
# 最低でも 7 文字はあるはず
if len(line)<7:
return line
is_not_decoration = firstchar != '['
if is_not_decoration:
return line
is_not_decoration = thirdchar != ' '
if is_not_decoration:
return line
surrounder = ''
is_strike_decoration = secondchar == '*'
is_bold_decoration = secondchar == '*'
if is_strike_decoration:
surrounder = '~~'
elif is_bold_decoration:
surrounder = '**'
else:
return line
bracket_nest = 0
for c in line:
if c=='[':
bracket_nest += 1
continue
if c==']':
bracket_nest -= 1
continue
is_end_of_decoration = bracket_nest == 0
if is_end_of_decoration:
break
return line
基本路線はこれで合ってるけど、link in decorationが行のどこに出現するかは不定(先頭とは限らない)
最初からfor c in line:して、状態保持しつつパースしていく感じだよな
初期状態
[をみつけた
-|*をみつけた
をみつけた
loop
[をみつけた
]をみつけた(これは無視する)
]をみつけた(ここでdecoration範囲確定)
そしてリテラルも考慮しなきゃいけないという
状態がえぐい
https://gyazo.com/bd494a6267efed0119c03dc812840350
いや、仕方ない、全部考慮したる
code:py
mode_initial = 0
mode_1_leftbracket = 1
mode_2_decoration_char = 2
mode_start = 3
mode_rightbracket_without_nest = 4
mode_literal_start = 5
mode_literal_end = 6
mode_second_leftbracket = 7
mode_second_rightbracket = mode_start
mode_end = 8
ひぃぃ
やっぱり有限オートマトンをコードに落とし込む方法ちゃんと調べるべきでは?
たぶんテーブルデータ定義して、テーブル辿って動くマシンちゃん書いてって感じだよな
クリアできる気がしない。。。。
装飾文法の置換どうやるか
newline用意して、そこに追加してやるべきか
startposとendposがいる
endposは今のi
startposはmode_first_leftbracketに遷移したときのi
surrounderが入ってるので、'{}{}{}'.format(surrounder, xxx, surrounder)みたいにして囲む
あー、これ想像以上にきついぞ
いや
こうしよう
装飾を囲む範囲をメモしておいて、一番最後に加工する
code:cases
000 XXXXXXXXX
||
VV
||
VV
012345678901234567890123456
s g
s g
s gs g
s g
s g s g
s gs g
s gs gs g
code:仮にこいつを例にして
012345678901234567890123456
s g s g
0,8,12,20
sとgを~~で囲んでやればいい
sとgの位置に~~を挿入できればいい
012345678901234567890123456
| [- までは消せる
V
012345678901234567890123456
| sの位置に挿入
V
| gの位置に挿入……の前に何すればいい?
| -1 して、
| ] があるのでそれ消して
V
| gの位置に挿入
V
あー、こういう境界の考慮本当に苦手
頭の中で配列の動作をシミュレートできないという
たぶんこんな感じだよな
sの位置から3文字消して
~~入れて
gの位置に移動して
1文字落として
1文字(])消して
1文字落として
~~入れる
1文字進んで(~~入れたことで差し引き+1文字増えてる)
……(次のsへ)
一応ワンパス通ったけどコードがえぐい。。。
愚直に力技で落とし込んだw
テストコード
https://gyazo.com/46b7bbbc0e5caa78f83becfeb4c315e1
↑ 一応こういうのもちゃんと動作してくれる
sta.icon*2うう、コードが気持ち悪すぎて、これ、いや、うーん……
いやでも状態遷移伴うロジック実装したらこんなもん?
SIerで実用小物ツールつくるマンだから、こういうごりっごりのプログラミング全然縁がない(から単に新鮮さ感じてるだけ?)
本流に組み込んで動作確認
yeah!
全部一致しました(末尾の<br>残ってるけど)
https://gyazo.com/8bdaf83b9a28b2d7a90105b9dbe16c91
リテラル
そのままなので変換不要
ハッシュタグ
判定調べる
文中で#hashtag書きます
文中で#hashtag 書きます
文末で書きますよ#hashtag
文末で書きますよ#hashtag
code:py
RE_HASHTAG = re.compile(r'(^| )#(.+?)( |$)')
newline = re.sub(RE_HASHTAG, '#\\2(\\2.md)', newline) 引用
既にリストを処理しているから、以下パターンになるはず
>XXX
- >XXX
- >XXX
これ考慮するのだるいので、リストより先に処理すべきか?
yes
code:py
RE_QUOTE = re.compile(r'^( )*\>(.+)')
newline = re.sub(RE_QUOTE, '\\1<blockquote>\\2</blockquote>', newline)
link [link_to_another_page]
これはハゲそう、intoc思い出すわぁ……いや見出しつくる話じゃないので違った scb: [link_to_another_page]
markdown: [link_to_another_page](ここのファイル名をどうするか問題)
こっちで適当にルール決めてしまえばいい話
既に処理した[text](url)を除外してやらないといけない
ちょっと場合分けする
case1: 先に[link_to_another_page]を処理する場合
.+だと[text url]等もヒットするので回避する必要あり
text and url表記が判定される条件は
spaceが含まれている and (左端がhttp(s)://を含んでいる or 右端がhttp(s)://を含んでいる)
A and (B or C)
判定されない条件は?
not (A and (B or C))
not A or not(B or C)
not A or not B and not C
つまり
spaceが含まれていないなら、link to another page で ok
spaceが含まれている場合、左端にも右端にもhttp(s)://が含まれてなければ、link to another page で ok
sta.iconこれ正規表現でどう書くの。。。
case2: 先にtext and url表記を処理する場合
処理後、[text](url)という文字列が散らばっている
愚直にr'\[/(.+?)]'すると、上記のmarkdown link文法もヒットしてしまう
]の後に(が来ない、にすればいい?
Scrapbox記法でgoogle(英語版googleです) ← いじわるな書き方してたらどうなる? text and url部分変換すると[google](https://google.co.jp)(英語版googleです)こうなる
いや、問題ないのか
変換後のリンク表記は必ず[]()である([の後に(が来ることが保証されている)
sta.iconこっちが正解ですね
case2で進めてるけど意外と泥臭い
code:py
RE_LINK_ANOTHER_PAGE = re.compile(r'\(.+?)\(^\(|$)') newline = re.sub(RE_LINK_ANOTHER_PAGE, '\\1(\\1.md)\\2', newline) これだと[*みたいな他の表記部分も死んでしまう
Q: 先にboldを処理すればいいのでは?
strikeとboldは、リンクに限り一重のネストを許す
これの関係でbold達を先に処理しないといけないからだめ
全部指定して弾くしかないかー。。。?
code:py
RE_LINK_ANOTHER_PAGE = re.compile(r'\[(^\-\*/)(.+?)\](^\(|$)') newline = re.sub(RE_LINK_ANOTHER_PAGE, '\\1\\2(\\1\\2.md)\\3', newline) だいぶしんどい表現
link [/link_to_another_project]
projectnameの諸元則った方がいい?
それとも雑に (.+?)でいい?
[/始まりがこれしかないし、こっちでいいか
code:py
RE_LINK_ANOTHER_PROJECT = re.compile(r'\/(.+?)') link [text url]と[url text]
場合分けが色々あるよな
ghpageはurl書いただけではリンク張られない
<>で囲む必要がある
[text url]と[url text]
[link_to_another_page]
[/link_to_another_project]
画像系はいったん無視します
url
httpから始まる文字列 ← こういうラフなのでいい?
sta.iconだよな、http prefix限定にしてるよな(その方がパース楽だもん)
url text
code:py
import re
s = '''段落段落(この段落の前後には空行があります)
list1
list1
list2
list2
list1
文字装飾
太字
太字2
太字3
strike
literal
引用
リンク
scrapbox link
direct url
文章中の文字装飾
文字装飾mix
引用の中で literal したり 太字したりリンク入れたり end'''
lines = s.split('\n')
RE_LINK_URL_TEXT = re.compile(r'\[https{0,1}\:\/\/(.+?)( )(.+?)\]') for i,line in enumerate(lines):
newline = re.sub(RE_LINK_URL_TEXT, '\\3(\\1)', line) if line==newline:
continue
print('{}: {}'.format(i, newline))
あー、https://の復元できてないな
https固定じゃだめだろうかw
キャプチャさせて一応http, https両方追従
code:py
newline = re.sub(RE_LINK_URL_TEXT, '\\4(http\\1://\\2)', line) okかしら
code:py
import re
s = '''リンク
scrapbox link
direct url
文字装飾mix
end'''
lines = s.split('\n')
for i,line in enumerate(lines):
newline = line
newline = re.sub(RE_LINK_URL_TEXT, '\\4(http\\1://\\2)', newline) newline = re.sub(RE_LINK_TEXT_URL, '\\1(http\\3://\\4)', newline) if line==newline:
continue
print('{}: {}'.format(i, newline))
https://gyazo.com/bca4ea2751c8c97b2ea0a0a906d66249
strike
むずくない?
[- [リンクに対するstrike]]
with RE_strike = re.compile(r'\[\-+( )(.+?)\]')
非貪欲だけど、これが最後から二番目の]にマッチする
開始記号が(終了記号が来る前に再び)くるケース、だよな
言い換えるとネストしてるケース
'<a> b <c>'じゃなくて'<a <b> c>'みたいな(こんなタグは書けないけど)
ggr wtih 正規表現 ネスト 外側
したがって、正規表現だけで文字列を処理する場合は、何らかの方法で括弧が2重、3重にならないように工夫する必要があります。(あるいは括弧内を再帰的に処理するプログラムを書くなど。)
なるほど、正規表現さんはネストには弱いのか
Pythonの標準のreには,(?R)など再帰が扱えないことが分かり,代わりにregexを使えばよさそう*1ということは分かりましたが,わざわざ正規表現を使わなくてもよいのではと思い,シンプルに書いてみました.
へー、再帰扱える正規表現っていう概念もないことはないのね
depthに括弧の深さを記録し,depthに応じた処理をするようにするのが簡単だと思いました.
そうだな、工夫は必要だ
再帰とか使いたくないので、解析の順番を工夫してネストしないことを保証させたい
strike
o
x
boldも?
yes
仕様
strikeとboldは、リンクに限り一重のネストを許す
結論
先にリンク記法のreplaceをしてしまえば、一重ネストも消えるので普通にstrikeやboldも通ります
2 bold
list以外も実装していく
が、苦戦するもの以外はたぶんソースに直接書いて終わりな気がする
最短マッチにならん
*?, +?, ??
'*' 、 '+' 、および '?' 修飾子は全て 貪欲 (greedy) マッチで、できるだけ多くのテキストにマッチします。この挙動が望ましくない時もあります。例えば正規表現 <.*> が '<a> b <c>' に対してマッチされると、 '<a>' だけでなく文字列全体にマッチしてしまいます。修飾子の後に ? を追加すると、 非貪欲 (non-greedy) あるいは 最小 (minimal) のマッチが行われ、できるだけ 少ない 文字にマッチします。正規表現 <.*?> を使うと '<a>' だけにマッチします。
ok
code:py
https://gyazo.com/e3c1b98b2eb4e96a6f50482b1bfe9c6d
1 list
とりあえずlist
code:py
import re
RE_list = re.compile(r'^ +(.+)')
s = '''scb
para
parapara
paraparapara
para
list
list
list
list
list
code:py
import os
dir(os)
list
para
list
list'''
lines = s.split('\n')
for i,line in enumerate(lines):
results = re.findall(RE_list, line)
print('{}: {}'.format(i, results))
https://gyazo.com/2a3b421b90890af6c141560d4f524452
置換はどうやる?
code:py
RE_list = re.compile(r'^ +(.+)')
for i,line in enumerate(lines):
newline = re.sub(RE_list, '- \\1', line)
print(newline)
https://gyazo.com/983a8126672a4b11897297c55a208027
n個のインデントがあったときに4*(n-1)個のspace入れたいんだけど、どうすれば……
code:py
RE_list = re.compile(r'^ +(.+)')
# ^^^^
# マッチしたspaceの数をキャプチャするって無理?
look-ahead?
https://gyazo.com/ed46499ecd425b17080261d82d9d893a
いや違うなぁ
つまりこうしたんだけど
code:py
RE_list = re.compile(r'^( +)(.+)')
...
newline = re.sub(RE_list, ' {!1}- \\2', line)
!nは、n番目のグループの繰り返しが何回現れたかという数字を示す
このような正規表現はある?
……素直に愚直に(正規表現使わない処理)にした方が無難そうですが
ちょっと対応洗うか
太字
太字2
太字3
strike
literal
引用
リンク
scrapbox link
direct url
文章中の文字装飾
文字装飾mix
引用の中で literal したり 太字したりリンク入れたり 整理
table:変換手段
表現 何使う? before after
list 愚直に " list" " - list"
bold regex "bold" " **bold** "
いや、listみたいに個数わからないと詰む奴だけ見ればいい
何がある?
list
listだけやね
listは結局こうなった
code:py
def scb_to_markdown_in_line(line, cur_indentdepth, inblockstate_user):
newline = line
state = inblockstate_user.state
is_in_list = cur_indentdepth>0
is_in_block = state.is_in_block()
if is_in_block and state.is_in_code_block():
return line
if is_in_block and state.is_in_table_block():
return '| テーブルは | あとで | {} | 実装します |'.format(line)
if is_in_list:
lstripped_newline = newline.lstrip()
markdown_indent = ' '*(cur_indentdepth-1)
newline = '{}- {}'.format(markdown_indent, lstripped_newline)
return newline
意外とやることが多い
そもそもlineが「ブロックの中かどうか」を知らないといけない
のでline受け取る関数だけでは完結できない
markdownのインデントは文字列の乗算使えばワンライナー
'='*8 で ======== になる
'='*0 だとちゃんと '' になってくれる