決断できた。
1 "quote" text "quote" text "link":http://google.com
1 This is a link to a "Wikipedia article about Textile":http://en.wikipedia.org/wiki/Textile_(markup_language)
2
3 This is a link to a "Wikipedia article about Textile":http://en.wikipedia.org/wiki/Textile_(markup_language).
1 "I first learned about "Redcloth":http://redcloth.org/ several years ago.
1 "He said it is "very unlikely" this works":http://slashdot.org/
1 "He said it is "very unlikely" the "economic stimulus" works":http://slashdot.org/
1 ""Open the pod bay doors please, HAL."":http://www.youtube.com/watch?v=npN9l2Bd06s
1 ["Wikipedia article about Textile":http://en.wikipedia.org/wiki/Textile_(markup_language)]
1 ["He said it "is very unlikely the economic stimulus works":http://en.wikipedia.org/wiki/Textile_(markup_language)].
アンバランスにならない限り全体で反応せよ、という仕様までは理解できないこともなかったのだが。まず、実装しようとすれば、引用符の数が偶数個で最後の引用符にコロンが続く、というような判断を強いられる。これだけのことであれば、「単なるチャレンジ」として面白いのだが。それでも最後の2つで十分にイヤで、そして決断に至ったのは単にこれ:
これさえ見つけなければ、続けるつもりだったんだけどね、多少荒れてようが。
「良くこんなんで誰も文句言わないな、Textile」で既に指摘した通り、そもそも仕様が荒れている。それと実際に 1/3 ほど取り組んでみてわかったのだが、非常に設計が場当たり的で破滅的である。感触としては、真面目に考えずに作り始めてはみたものの、色んな要望を闇雲に取り入れていったら埒もあかなくなった、という感じだろう、という感想。「メンテナンスが続けられなくなった」と、さも何か事情ありげに書いているが、おそらく作者が完全に興味をなくしたんだろう。あるいはもう作ってしまったことへの後悔フェーズに突入してるんじゃないかな。こんな仕様、保守が続けられるとは思えないもの。
ワタシが思うに、このプロジェクトは、このまま絶滅するに任せた方が、皆の幸せのためにはいいと思う。「バグフィックス」してもまともなものにはならない、これは。設計不全だから。ので、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__ = ['TextileLexer']
25
26
27 from sys import maxunicode
28 upper_re_s = "".join([unichr(c) for c in
29 xrange(maxunicode) if unichr(c).isupper()])
30
31
32 class TextileLexer(RegexLexer):
33 """
34 A lexer that highlights `Textile <https://github.com/textile/textile-spec>`_ syntax.
35
36 .. versionadded:: 2.2
37 """
38
39 name = 'Textile'
40 aliases = ['textile']
41 filenames = ['*.textile']
42 mimetypes = ['text/x-textile']
43
44 flags = re.MULTILINE | re.DOTALL | re.UNICODE
45
46 pnct_re_s = r'[-!"#$%&()*+,/:;<=>?@\'\[\\\]\.^_`{|}~]'
47 pnct_re_s2 = r'[-!"#$%&*+,/:;<=>?@\'\[\\\]\.^_`{|}~]'
48
49 url_re = r"""(?xi)
50 \b
51 ( # Capture 1: entire matched URL
52 (?:
53 [a-z][\w-]+: # URL protocol and colon
54 (?:
55 /{1,3} # 1-3 slashes
56 | # or
57 [a-z0-9%] # Single letter or digit or '%'
58 # (Trying not to match e.g. "URI::Escape")
59 )
60 | # or
61 www\d{0,3}[.] # "www.", "www1.", "www2." … "www999."
62 | # or
63 [a-z0-9.\-]+[.][a-z]{2,4}/ # looks like domain name followed by a slash
64 )
65 (?: # One or more:
66 [^\s()<>]+ # Run of non-space, non-()<>
67 | # or
68 \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
69 )+
70 (?: # End with:
71 \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
72 | # or
73 [^\s`!()\[\]{};:'".,<>?«»“”‘’] # not a space or one of these punct chars
74 )
75 )"""
76 url_term_re = r"""(?xi)
77 (?: # One or more:
78 [^\s()<>]+ # Run of non-space, non-()<>
79 | # or
80 \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
81 )+
82 (?: # End with:
83 \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
84 | # or
85 [^\s`!()\[\]{};:'".,<>?«»“”‘’] # not a space or one of these punct chars
86 )"""
87
88 tokens = {
89 'root': [
90 (r'<!--', Comment, 'comment'),
91
92 # Heading
93 (r'^(h[1-6]\b[^\n]*?\.)([^\n]*)$',
94 bygroups(Generic.Heading, Name.Namespace)),
95
96 # Links
97 (r'(\[)("{1,2})([^"]+)(\2)(:)(\S+)(\])',
98 bygroups(
99 Punctuation,
100 Keyword, Keyword, Keyword, Keyword, String,
101 Punctuation)),
102 (r'("{1,2})([^"]+)(\1)(:)(\S+%s)' % url_term_re,
103 bygroups(
104 Keyword, Keyword, Keyword, Keyword, String)),
105
106 # Block code
107 (r'^(bc|pre)\b[^\n]*?\.\.(\s|$)',
108 Generic.Subheading, 'extended blockcode'),
109 (r'^(bc|pre)\b[^\n]*?\.(\s|$)',
110 Generic.Subheading, 'blockcode'),
111
112 # Inline code
113 # python-textile allows newline...
114 (r'(@)([^@]+)(@)',
115 bygroups(Generic.Subheading, Comment.Preproc, Generic.Subheading)),
116
117 # Block quotation
118 (r'^(bq)\b[^\n]*?\.\.(:%s|\s|$)' % url_re,
119 Generic.Subheading, 'extended blockquote'),
120 (r'^(bq)\b[^\n]*?\.(:%s|\s|$)' % url_re,
121 Generic.Subheading, 'blockquote'),
122
123 # Inline citation
124 # python-textile doesn't allow newline...
125 (r'(\?\?)([^\n]+?)(\?\?)',
126 bygroups(Generic.Subheading, Comment.Special, Generic.Subheading)),
127
128 #
129 (r'^(p)\b[^\n]*?\.(\s|$)', Generic.Subheading),
130
131 # Insertions and deletions
132 # python-textile doesn't allow newline...
133 (r'\[\+[^\n]+\+\]', Generic.Inserted),
134 (r'(?<=\s)\+[^\n+]+\+(?=\s)', Generic.Inserted),
135 (r'\[\-[^\n]+\-\]', Generic.Deleted),
136 (r'(?<=\s)\-[^\n-]+\-(?=\s)', Generic.Deleted),
137
138 # Strong importance, Stress emphasis, Stylistic offset, Alternate voice
139 (r'(?<=\s|%s)(\*_)[^\n](_\*)(?=\s|%s)' % (
140 pnct_re_s, pnct_re_s), Generic.Strong),
141 (r'(?<=\s|%s)(_\*)[^\n](\*_)(?=\s|%s)' % (
142 pnct_re_s, pnct_re_s), Generic.Strong),
143 (r'(?<=\s|%s)(\*{1,2})[^\n]+\1(?=\s|%s)' % (
144 pnct_re_s, pnct_re_s), Generic.Strong),
145 (r'(?<=\s|%s)(_{1,2})[^\n]+\1(?=\s|%s)' % (
146 pnct_re_s, pnct_re_s), Generic.Emph),
147
148 # Glyphs
149 include('glyphs'),
150
151 (r'.', Text),
152 ],
153 'extended blockcode': [
154 # ``it encounters another block signature such as @p.@''
155 # fuh, such is such??
156 (r'\n(?=(?:p|pre|bc|h[1-6])\b[^\n]*?\.(:|\s|$))', Comment.Preproc, '#pop'),
157 (r'.', Comment.Preproc),
158 ],
159 'blockcode': [
160 (r'^\n', Comment.Preproc, '#pop'), # blank line
161 (r'.', Comment.Preproc),
162 ],
163 'extended blockquote': [
164 # ``it encounters another block signature such as @p.@''
165 # fuh, such is such??
166 (r'\n(?=(?:p|pre|bc|h[1-6])\b[^\n]*?\.(\s|$))', Comment.Special, '#pop'),
167 (r'.', Comment.Special),
168 ],
169 'blockquote': [
170 (r'^\n', Comment.Special, '#pop'), # blank line
171 (r'.', Comment.Special),
172 ],
173 'glyphs': [
174 # apostrophe's
175 #(r"(^|\w)'(\w)",),
176 # back in '88
177 #(r"(\s)'(\d+\w?)\b(?!')",),
178 # single closing
179 #(r"(^|\S)'(?=\s|%s|$)" % pnct_re_s,),
180 # single opening
181 #(r"'",),
182 # double closing
183 #(r'(^|\S)"(?=\s|%s|$)' % pnct_re_s,),
184 # double opening
185 #(r'"',),
186 # ellipsis
187 (r'(?=[^.]?)\.{3}', Name.Entity), #Q
188 # ampersand
189 (r'(\s+)(&)(\s+)', bygroups(Text, Name.Entity, Text)), #Q
190 # em dash
191 (r'(\s*)(--)(\s*)', bygroups(Text, Name.Entity, Text)), #Q
192 # en dash
193 (r'(\s+)(-)(\s+|$)', bygroups(Text, Name.Entity, Text)), #Q
194 # dimension sign
195 (r'(\d+)( *)(x)( *)(\d+)',
196 bygroups(Number, Text, Name.Entity, Text, Number)), #Q
197 # trademark
198 (r'\b( *)([([](?i)TM[])])', bygroups(Text, Name.Entity)), #Q
199 # registered
200 (r'\b( *)([([](?i)R[])])', bygroups(Text, Name.Entity)), #Q
201 # copyright
202 (r'\b( *)([([](?i)C[])])', bygroups(Text, Name.Entity)), #Q
203 # 1/2
204 (r'[([]1/2[])]', Name.Entity), #Q
205 # 1/4
206 (r'[([]1/4[])]', Name.Entity), #Q
207 # 3/4
208 (r'[([]3/4[])]', Name.Entity), #Q
209 # degrees
210 (r'[([](?i)o[])]', Name.Entity), #Q
211 # plus/minus
212 (r'[([]\+/\-[])]', Name.Entity), #Q
213 # 3+ uppercase acronym
214 (r'\b([%s][%s0-9]{2,})([(])([^)]*)([)])' % (upper_re_s, upper_re_s),
215 bygroups(Keyword, Punctuation, String, Punctuation)), #Q
216 # 3+ uppercase
217 (r"""(?:(?<=^)|(?<=\s)|(?<=[>(;-]))([%s]{3,})(\w*)(?=\s|%s|$)(?=[^">]*?(<|$))""" % (
218 upper_re_s, pnct_re_s),
219 bygroups(Keyword, Text)), #Q
220 ],
221 'comment': [
222 ('[^-]+', Comment),
223 ('-->', Comment, '#pop'),
224 ('-', Comment),
225 ],
226 }
リンクの仕様の完成度を高めようとしていた最中に今回の記事の発端の仕様に気付いた、ので、「引用符の数が偶数個で最後の引用符にコロンが続く」をどうしようか、と考えるとこまで行ってない。Pygments RegexLexer 単体のワンパスでは無理じゃないかな。入れ子の lexer にして、外側の lexer が「”: から後ろ向きに行頭まで対になる ” を全部まとめあげる」とでもする感じかな、多分。あるいはスタックに積んで保留にしとくんだろうね。
あ、ちなみに「caps」の仕様もおかしいです。振る舞いが未定義、ちゅうんかな。「HAL」には期待通り反応。python-textile は「CFString」に「<span class="caps">CFS</span>tring
」と反応、RedCloth は「<p>CFString</p>
」と反応、これは「DVDs」にどう反応すべきなのか、という悩ましい問題を抱えており、何しても解決出来ないと思う。「TCP/IP」に至っては、ともに「<p><span class="caps">TCP</span>/IP</p>
」と反応。これはもうさ、やろうとしたことに無理があった、ってことなんよ。こんなもん自動でやろうったって、うまくいかんさ。
2015-11-13 02:50追記:
イキオイで間違ったこと言ってるね。「引用符の数が偶数個で最後の引用符にコロンが続く」ではないよね。
1 "quote" text "quote" text "link":http://google.com
1 "He said it is "very unlikely" this works":http://slashdot.org/
この2例から、引用符の「囲み方」を識別しなければならない、ってこと。例えば
1 "He said it is "very unlikely "this works":http://slashdot.org/
ということ。空白の位置が一つ違うだけで意味が違うわけです。知恵熱出そうだし、これが理由でやめるわけでもないけど続ける気がないので、どうすればいいかは考えないけどね。いずれ近いものは必要になるかもなぁ。
なお、この問題も「何でも自動でやろうと目論んだ」のが破滅の原因、ね。「typography quotes」としても自動で反応しつつ、執筆者に明示的に記述させることなく「リンクテキストのための囲み」としても機能させようとしたために、二重引用符の機能が過負荷になっちゃってるんです。そしてこれの本当の問題はさ、「機械処理しにくい」ことにあるのではなくて、むしろ人間が振る舞いを想像出来なくなってしまっている、ということにある。なんか書いててわかってきたけど、いわゆるこれ、伝統的に御馴染みの問題ね。