Creole 1.0 の Pygments lexer

おけ。

あっちゅうまに出来た。

  1 # -*- coding: utf-8 -*-
  2 """
  3     pygments.lexers.markup
  4     ~~~~~~~~~~~~~~~~~~~~~~
  5 
  6     Lexers for non-HTML markup languages.
  7 
  8     :copyright: Copyright 2006-2015 by the Pygments team, see AUTHORS.
  9     :license: BSD, see LICENSE for details.
 10 """
 11 
 12 import re
 13 
 14 from pygments.lexers.html import HtmlLexer, XmlLexer
 15 from pygments.lexers.javascript import JavascriptLexer
 16 from pygments.lexers.css import CssLexer
 17 
 18 from pygments.lexer import RegexLexer, DelegatingLexer, include, bygroups, \
 19     using, this, do_insertions, default, words
 20 from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
 21     Number, Punctuation, Generic, Other
 22 from pygments.util import get_bool_opt, ClassNotFound
 23 
 24 __all__ = ['BBCodeLexer', 'MoinWikiLexer', 'RstLexer', 'TexLexer', 'GroffLexer',
 25            'MozPreprocHashLexer', 'MozPreprocPercentLexer',
 26            'MozPreprocXulLexer', 'MozPreprocJavascriptLexer',
 27            'MozPreprocCssLexer', 'Creole10Lexer']
 28 
 29 # ...
 30 
 31 
 32 # internal tokens for Creole10Lexer
 33 from pygments.token import _TokenType
 34 _InnerToken = _TokenType()
 35 _StrongInHold = _InnerToken.StrongInHold
 36 _ItalicInHold = _InnerToken.ItalicInHold
 37 _ParaEnd = _InnerToken.ParaEnd
 38 
 39 
 40 class Creole10Lexer(RegexLexer):
 41     """
 42     For `Creole 1.0 <http://www.wikicreole.org/wiki/Creole1.0>`_ markup.
 43 
 44     .. versionadded:: 2.1
 45     """
 46 
 47     name = 'Creole 1.0 markup'
 48     aliases = ['creole10']
 49     filenames = []
 50     mimetypes = []
 51     flags = re.MULTILINE | re.DOTALL
 52 
 53     tokens = {
 54         'root': [
 55             # Escape
 56             (r'~.', Text),
 57 
 58             # Headings
 59             #   Whitespace **is** allowed before the left-side equal signs. 
 60             #   Only whitespace characters are permitted after the closing
 61             #   equal signs.  Markup parsing is optional within headings.
 62             (r'^s*={1,6}[^\n]*$', Generic.Heading),
 63 
 64             # Placeholder
 65             (r'(<<<)([^\n]*?)(>>>)',
 66              bygroups(Generic.Subheading, Keyword, Generic.Subheading)),
 67 
 68             # Nowiki (Preformatted)
 69             #
 70             #   In inline nowiki, any trailing closing brace is included
 71             #   in the nowiki span (i.e. in a sequence of more than three
 72             #   closing braces, the end marker is made of the //last three braces//).
 73             (r'({{{)([^\n]*?)(}}})(?!})',
 74              bygroups(Generic.Subheading, Comment.Preproc, Generic.Subheading)),
 75 
 76             #   As a block, the three curly braces should be on one line by
 77             #   itself to open and another line of three curly braces should
 78             #   be on a line by itself to close, without leading spaces.
 79             (r'^{{{', Generic.Subheading, 'nowikiblock'),
 80             
 81             # Links
 82             (r'(\[\[)([^[\]|]+)(\|)([^[\]|]+)(\]\])',
 83              bygroups(
 84                     Punctuation,
 85                     Keyword, Punctuation, String,
 86                     Punctuation)),
 87             (r'(\[\[)([^[\]|]+)(\]\])',
 88              bygroups(
 89                     Punctuation, Keyword, Punctuation)),
 90 
 91             # Image (inline)
 92             (r'({{)([^[\]|]+)(\|)([^[\]|]+)(}})',
 93              bygroups(
 94                     Punctuation,
 95                     Keyword, Punctuation, String,
 96                     Punctuation)),
 97             (r'({{)([^[\]|]+)(}})',
 98              bygroups(
 99                     Punctuation, Keyword, Punctuation)),
100 
101             # Unordered Lists, Ordered Lists
102             (r'^\s*\*\*\S', _StrongInHold),  # bold special case
103             (r'^\s*\*+', Generic.Subheading),
104             (r'^\s*#+', Generic.Subheading),
105 
106             # Table (cell separator)
107             (r'\|=?', Generic.Subheading),
108 
109             # Horizontal Rule
110             #   Whitespace is optional before and after the hyphens, but no
111             #   whitespace is allowed between them.  The four hyphens must
112             #   be the only characters (other than whitespace) on that line.
113             (r'\s*\-{4}\s*$', Generic.Subheading),
114 
115             # Bold and Italics
116             #   Note that `Creole 1.0 <http://www.wikicreole.org/wiki/Creole1.0>`_
117             #   says `__**xxx__**` and `**__xxx**__` are not acceptable,
118             #   but some engines (for example `wikicreole <https://bitbucket.org/thesheep/wikicreole>`_)
119             #   allow these.
120             (r'\*\*', _StrongInHold),
121             (r'(http|https|ftp|nntp|news|mailto|telnet|file|irc)://', Text),
122             (r'//', _ItalicInHold),
123             (r'\n\n', _ParaEnd),
124 
125             (r'.', Text),
126         ],
127         'nowikiblock': [
128             (r'~.', Comment.Preproc),
129             (r'^}}}', Generic.Subheading, '#pop'),
130             (r'[^~}]+', Comment.Preproc),
131             (r'.', Comment.Preproc),
132         ],
133     }
134 
135     in_hold_tk = []
136     in_hold_iv = []
137 
138     def _flush(self, acc, idx=None, tok=None, val=None):
139         if tok is not None:
140             self.in_hold_tk.append(tok)
141             self.in_hold_iv.append((idx, val))
142         if not self.in_hold_tk:
143             return
144 
145         stk = Generic.Strong if acc else Text
146         itk = Generic.Emph if acc else Text
147         ctk = Text
148         for i, tk in enumerate(self.in_hold_tk):
149             modtk = tk 
150             if tk == _StrongInHold:
151                 modtk = stk
152                 ctk = modtk
153             elif tk == _ItalicInHold:
154                 modtk = itk
155                 ctk = modtk
156             elif tk == _ParaEnd:
157                 modtk = Text
158                 ctk = modtk
159             yield self.in_hold_iv[i][0], ctk, self.in_hold_iv[i][1]
160         self.in_hold_tk = []
161         self.in_hold_iv = []
162 
163     def get_tokens_unprocessed(self, text):
164         self.in_hold_tk = []
165         self.in_hold_iv = []
166 
167         #
168         def _extract_innertoks():
169             return [t for t in self.in_hold_tk
170                     if t in (_StrongInHold, _ItalicInHold, _ParaEnd)]
171 
172         #
173         for idx, tok, val in RegexLexer.get_tokens_unprocessed(self, text):
174             can_yield = False
175             if self.in_hold_tk and tok not in (
176                 _StrongInHold, _ItalicInHold, _ParaEnd):
177 
178                 self.in_hold_tk.append(tok)
179                 self.in_hold_iv.append((idx, val))
180 
181             elif tok == _StrongInHold:
182                 if _StrongInHold not in self.in_hold_tk:
183                     self.in_hold_tk.append(tok)
184                     self.in_hold_iv.append((idx, val))
185                 else:
186                     tmp = _extract_innertoks()
187                     # S-S: acceptable
188                     # S-I-I-S: acceptable
189                     if tmp == [_StrongInHold] or \
190                             tmp == [_StrongInHold, _ItalicInHold,
191                                     _ItalicInHold]:
192                         for i, t, v in self._flush(True, idx, tok, val):
193                             yield i, t, v
194                     # S-I-S: unacceptable
195                     elif tmp == [_StrongInHold, _ItalicInHold]:
196                         for i, t, v in self._flush(False, idx, tok, val):
197                             yield i, t, v
198                     # else(I-S-S): keep in-hold
199                     else:
200                         self.in_hold_tk.append(tok)
201                         self.in_hold_iv.append((idx, val))
202 
203             elif tok == _ItalicInHold:
204                 if _ItalicInHold not in self.in_hold_tk:
205                     self.in_hold_tk.append(tok)
206                     self.in_hold_iv.append((idx, val))
207                 else:
208                     tmp = _extract_innertoks()
209                     # I-I: acceptable
210                     # I-S-S-I: acceptable
211                     if tmp == [_ItalicInHold] or \
212                             tmp == [_ItalicInHold, _StrongInHold,
213                                     _StrongInHold]:
214                         for i, t, v in self._flush(True, idx, tok, val):
215                             yield i, t, v
216                     # I-S-I: unacceptable
217                     elif tmp == [_ItalicInHold, _StrongInHold]:
218                         for i, t, v in self._flush(False, idx, tok, val):
219                             yield i, t, v
220                     # else(S-I-I): keep in-hold
221                     else:
222                         self.in_hold_tk.append(tok)
223                         self.in_hold_iv.append((idx, val))
224 
225             elif tok == _ParaEnd:
226                 if self.in_hold_tk:
227                     for i, t, v in self._flush(False, idx, tok, val):
228                         yield i, t, v
229                 else:
230                     can_yield = True
231 
232             else:
233                 can_yield = True
234 
235             if can_yield:
236                 yield idx, tok, val
237         #
238         for i, t, v in self._flush(False):
239             yield i, t, v

ひとまず .. versionadded:: 2.1 にしてあるけど、まだ PR あげるつもりなくて、Texinfo とまとめてあげようかなと思ってるので、結果的に .. versionadded:: 2.2 になるかもしれんです。

「あっちゅうまに出来た」つぅ割には lexer がちょい複雑なのは、実は「BitBucket の Wiki を CREOLE で書くとちょいとヘンな話」「wikicreole と Creole1.0 と BitBucket の Creole もどきがそれぞれ別物な件」のどちらでも触れなかった、ちょいと繊細な仕様に関係してる。これ:

1 //**Bold italics**//\\
2 **//Bold italics//**\\
3 **This is //also// good**
4 
5 Unacceptable:
6 **//bold italics**//
7 //**bold italics//**

「ちゃんと綺麗に囲めよ」と XML チックなことを要求してるわけね。これそのものは「御意」なんだけれど、ただし実現はかえって大変だ、てわけで。「Unacceptable」とは言ってるけどどう振る舞えとも言ってないので、「反応しない」という、実現が一番手間なものを選んだ。なんでこうしたかと言うと、出回ってるエンジンによってここの扱いが違っちゃってるから。

wikicreole だと、例えば「**//bold italics**//」を、「《ボールド開始》《イタリック開始》bold italic《イタリック開始》《ボールド開始》(をっと閉じてないので…)《ボールド終了》《イタリック終了》《ボールド終了》《イタリック終了》」なんつぅ空気の読み方をする。

で、これって違反じゃなあい? って作者にコンタクト取ってみたら、「別にどう振る舞えって言ってないじゃん、あたしゃ「寛大に受け取るけれども厳密に言われた通りに出力」、それでも最大限に空気読むことにしたんだぜ」と。どうも「Creole 1.0 の仕様策定者が特定の実現方法を念頭に置いた恣意的な仕様、なんだから従わなくたって別にいいじゃん」てのが本音らしい。

個人的にはこれは違うと思うのね。実装をベースに仕様を決めたんじゃなくて、ユーザのための便宜なんだと思うんだわ。つまり「プレインテキストとして読んだ場合の視認性」まで考えたんじゃないか、ってこと。(普通の括弧で考えてみればわかる。「 ( [ あいうえお ) ] 」って、気持ち悪いでしょーよ。

けどまぁ…「Wiki システムそのもの」が本題の wikicreole 作者にとっては当然「自分の wikicreole のユーザは wiki で執筆する人(つまりプレビューしながらトライアンドエラーで書く人)」という認識しか持たないで済んでるのは理解出来るし、「所詮 wiki」なわけで、なのでちょっと言いたいこともあったけど、言いたいことは飲み込んだ。どっちみち直して欲しくてコンタクト取ったわけでもなかったから。どういう考えで、どういう反応になるかな、って興味あったの、単に。(前にも書いたけど、既にコンテンツが書かれてるマークアップ言語の仕様を気軽に変えちゃダメなの。)












さて。いよいよ本題の Texinfo いきますか。