Реализация синтаксического анализатора для markdown-подобного языка
у меня есть язык разметки, который похож на markdown и тот, который используется SO.
Legacy parser был основан на regexes и был полным кошмаром для поддержания, поэтому я придумал свое собственное решение, основанное на грамматике EBNF и реализованное через mxTextTools/SimpleParse.
однако есть проблемы с некоторыми токенами, которые могут включать друг друга, и я не вижу "правильного" способа сделать это.
вот часть моей грамматики:
newline := "rn"/"n"/"r"
indent := ("rn"/"n"/"r"), [ t]
number := [0-9]+
whitespace := [ t]+
symbol_mark := [*_>#`%]
symbol_mark_noa := [_>#`%]
symbol_mark_nou := [*>#`%]
symbol_mark_nop := [*_>#`]
punctuation := [(),.!?]
noaccent_code := -(newline / '`')+
accent_code := -(newline / '``')+
symbol := -(whitespace / newline)
text := -newline+
safe_text := -(newline / whitespace / [*_>#`] / '%%' / punctuation)+/whitespace
link := 'http' / 'ftp', 's'?, '://', (-[ trn<>`^'"*,.!?]/([,.?],?-[ trn<>`^'"*]))+
strikedout := -[ trn*_>#`^]+
ctrlw := '^W'+
ctrlh := '^H'+
strikeout := (strikedout, (whitespace, strikedout)*, ctrlw) / (strikedout, ctrlh)
strong := ('**', (inline_nostrong/symbol), (inline_safe_nostrong/symbol_mark_noa)* , '**') / ('__' , (inline_nostrong/symbol), (inline_safe_nostrong/symbol_mark_nou)*, '__')
emphasis := ('*',?-'*', (inline_noast/symbol), (inline_safe_noast/symbol_mark_noa)*, '*') / ('_',?-'_', (inline_nound/symbol), (inline_safe_nound/symbol_mark_nou)*, '_')
inline_code := ('`' , noaccent_code , '`') / ('``' , accent_code , '``')
inline_spoiler := ('%%', (inline_nospoiler/symbol), (inline_safe_nop/symbol_mark_nop)*, '%%')
inline := (inline_code / inline_spoiler / strikeout / strong / emphasis / link)
inline_nostrong := (?-('**'/'__'),(inline_code / reference / signature / inline_spoiler / strikeout / emphasis / link))
inline_nospoiler := (?-'%%',(inline_code / emphasis / strikeout / emphasis / link))
inline_noast := (?-'*',(inline_code / inline_spoiler / strikeout / strong / link))
inline_nound := (?-'_',(inline_code / inline_spoiler / strikeout / strong / link))
inline_safe := (inline_code / inline_spoiler / strikeout / strong / emphasis / link / safe_text / punctuation)+
inline_safe_nostrong := (?-('**'/'__'),(inline_code / inline_spoiler / strikeout / emphasis / link / safe_text / punctuation))+
inline_safe_noast := (?-'*',(inline_code / inline_spoiler / strikeout / strong / link / safe_text / punctuation))+
inline_safe_nound := (?-'_',(inline_code / inline_spoiler / strikeout / strong / link / safe_text / punctuation))+
inline_safe_nop := (?-'%%',(inline_code / emphasis / strikeout / strong / link / safe_text / punctuation))+
inline_full := (inline_code / inline_spoiler / strikeout / strong / emphasis / link / safe_text / punctuation / symbol_mark / text)+
line := newline, ?-[ t], inline_full?
sub_cite := whitespace?, ?-reference, '>'
cite := newline, whitespace?, '>', sub_cite*, inline_full?
code := newline, [ t], [ t], [ t], [ t], text
block_cite := cite+
block_code := code+
all := (block_cite / block_code / line / code)+
первый проблема в том, что спойлер, сильный и акцент могут включать друг друга в произвольном порядке. И возможно, что позже мне понадобится больше таких встроенных наценок.
мое текущее решение включает в себя создание отдельного токена для каждой комбинации (inline_noast, inline_nostrong и т. д.), Но, очевидно, количество таких комбинаций растет слишком быстро с увеличением количества элементов разметки.
вторая проблема заключается в том, что эти lookaheads в strong / emphasis ведут себя очень плохо в некоторых случаях плохого разметка типа __._.__*__.__...___._.____.__**___***
(много случайно размещенных символов разметки). Требуется несколько минут, чтобы разобрать несколько КБ такого случайного текста.
что-то не так с моей грамматикой или я должен использовать какой-то другой парсер для этой задачи?
1 ответов
если одна вещь включает в себя другую, то обычно вы относитесь к ним как к отдельным токенам, а затем вставляете их в грамматику. Lepl (http://www.acooke.org/lepl который я написал) и PyParsing (который, вероятно, является самым популярным парсером pure-Python) позволяют вам рекурсивно вставлять вещи.
таким образом, в Lepl вы можете написать код что-то вроде:
# these are tokens (defined as regexps)
stg_marker = Token(r'\*\*')
emp_marker = Token(r'\*') # tokens are longest match, so strong is preferred if possible
spo_marker = Token(r'%%')
....
# grammar rules combine tokens
contents = Delayed() # this will be defined later and lets us recurse
strong = stg_marker + contents + stg_marker
emphasis = emp_marker + contents + emp_marker
spoiler = spo_marker + contents + spo_marker
other_stuff = .....
contents += strong | emphasis | spoiler | other_stuff # this defines contents recursively
тогда вы можете увидеть, я надеюсь, как содержимое будет соответствовать вложенному использованию сильного акцента, так далее.
для вашего окончательного решения нужно сделать гораздо больше, и эффективность может быть проблемой в любом парсере pure-Python (есть некоторые парсеры, которые реализованы на C, но вызываются из Python. Они будут быстрее, но могут быть сложнее в использовании; я не могу рекомендовать их, потому что я их не использовал).