FooBarWidget-mizuho 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. data/LICENSE.txt +20 -0
  2. data/README.markdown +68 -0
  3. data/Rakefile +9 -0
  4. data/asciidoc/BUGS +34 -0
  5. data/asciidoc/BUGS.txt +28 -0
  6. data/asciidoc/CHANGELOG +1585 -0
  7. data/asciidoc/CHANGELOG.txt +1595 -0
  8. data/asciidoc/COPYING +339 -0
  9. data/asciidoc/COPYRIGHT +18 -0
  10. data/asciidoc/INSTALL +82 -0
  11. data/asciidoc/INSTALL.txt +71 -0
  12. data/asciidoc/README +46 -0
  13. data/asciidoc/README.txt +36 -0
  14. data/asciidoc/a2x +641 -0
  15. data/asciidoc/asciidoc.conf +404 -0
  16. data/asciidoc/asciidoc.py +4255 -0
  17. data/asciidoc/common.aap +9 -0
  18. data/asciidoc/dblatex/asciidoc-dblatex.sty +18 -0
  19. data/asciidoc/dblatex/asciidoc-dblatex.xsl +17 -0
  20. data/asciidoc/dblatex/dblatex-readme.txt +22 -0
  21. data/asciidoc/doc/a2x.1 +246 -0
  22. data/asciidoc/doc/a2x.1.txt +195 -0
  23. data/asciidoc/doc/article.css-embedded.html +579 -0
  24. data/asciidoc/doc/article.html +62 -0
  25. data/asciidoc/doc/article.pdf +0 -0
  26. data/asciidoc/doc/article.txt +124 -0
  27. data/asciidoc/doc/asciidoc-revhistory.xml +27 -0
  28. data/asciidoc/doc/asciidoc.1 +161 -0
  29. data/asciidoc/doc/asciidoc.1.css-embedded.html +562 -0
  30. data/asciidoc/doc/asciidoc.1.css.html +212 -0
  31. data/asciidoc/doc/asciidoc.1.html +190 -0
  32. data/asciidoc/doc/asciidoc.1.txt +118 -0
  33. data/asciidoc/doc/asciidoc.conf +8 -0
  34. data/asciidoc/doc/asciidoc.css-embedded.html +7954 -0
  35. data/asciidoc/doc/asciidoc.css.html +7553 -0
  36. data/asciidoc/doc/asciidoc.dict +673 -0
  37. data/asciidoc/doc/asciidoc.html +3502 -0
  38. data/asciidoc/doc/asciidoc.txt +4757 -0
  39. data/asciidoc/doc/asciimath.txt +47 -0
  40. data/asciidoc/doc/book-multi.css-embedded.html +575 -0
  41. data/asciidoc/doc/book-multi.html +72 -0
  42. data/asciidoc/doc/book-multi.txt +159 -0
  43. data/asciidoc/doc/book.css-embedded.html +585 -0
  44. data/asciidoc/doc/book.html +60 -0
  45. data/asciidoc/doc/book.txt +133 -0
  46. data/asciidoc/doc/customers.csv +18 -0
  47. data/asciidoc/doc/faq.txt +262 -0
  48. data/asciidoc/doc/latex-backend.html +224 -0
  49. data/asciidoc/doc/latex-backend.txt +193 -0
  50. data/asciidoc/doc/latexmath.txt +35 -0
  51. data/asciidoc/doc/main.aap +293 -0
  52. data/asciidoc/doc/music-filter.html +513 -0
  53. data/asciidoc/doc/music-filter.pdf +0 -0
  54. data/asciidoc/doc/music-filter.txt +158 -0
  55. data/asciidoc/doc/source-highlight-filter.html +183 -0
  56. data/asciidoc/doc/source-highlight-filter.pdf +0 -0
  57. data/asciidoc/doc/source-highlight-filter.txt +174 -0
  58. data/asciidoc/docbook-xsl/asciidoc-docbook-xsl.txt +71 -0
  59. data/asciidoc/docbook-xsl/chunked.xsl +19 -0
  60. data/asciidoc/docbook-xsl/common.xsl +67 -0
  61. data/asciidoc/docbook-xsl/fo.xsl +117 -0
  62. data/asciidoc/docbook-xsl/htmlhelp.xsl +17 -0
  63. data/asciidoc/docbook-xsl/manpage.xsl +28 -0
  64. data/asciidoc/docbook-xsl/shaded-literallayout.patch +32 -0
  65. data/asciidoc/docbook-xsl/xhtml.xsl +14 -0
  66. data/asciidoc/docbook.conf +606 -0
  67. data/asciidoc/examples/website/CHANGELOG.html +3828 -0
  68. data/asciidoc/examples/website/INSTALL.html +163 -0
  69. data/asciidoc/examples/website/README-website.html +129 -0
  70. data/asciidoc/examples/website/README-website.txt +29 -0
  71. data/asciidoc/examples/website/README.html +125 -0
  72. data/asciidoc/examples/website/a2x.1.html +395 -0
  73. data/asciidoc/examples/website/asciidoc-docbook-xsl.html +165 -0
  74. data/asciidoc/examples/website/asciimath.html +157 -0
  75. data/asciidoc/examples/website/build-website.sh +25 -0
  76. data/asciidoc/examples/website/downloads.html +219 -0
  77. data/asciidoc/examples/website/downloads.txt +98 -0
  78. data/asciidoc/examples/website/faq.html +372 -0
  79. data/asciidoc/examples/website/index.html +398 -0
  80. data/asciidoc/examples/website/index.txt +222 -0
  81. data/asciidoc/examples/website/latex-backend.html +640 -0
  82. data/asciidoc/examples/website/latexmath.html +119 -0
  83. data/asciidoc/examples/website/layout1.conf +161 -0
  84. data/asciidoc/examples/website/layout1.css +65 -0
  85. data/asciidoc/examples/website/layout2.conf +158 -0
  86. data/asciidoc/examples/website/layout2.css +93 -0
  87. data/asciidoc/examples/website/manpage.html +266 -0
  88. data/asciidoc/examples/website/music-filter.html +242 -0
  89. data/asciidoc/examples/website/music1.abc +12 -0
  90. data/asciidoc/examples/website/music1.png +0 -0
  91. data/asciidoc/examples/website/music2.ly +9 -0
  92. data/asciidoc/examples/website/music2.png +0 -0
  93. data/asciidoc/examples/website/source-highlight-filter.html +251 -0
  94. data/asciidoc/examples/website/support.html +78 -0
  95. data/asciidoc/examples/website/support.txt +5 -0
  96. data/asciidoc/examples/website/userguide.html +7597 -0
  97. data/asciidoc/examples/website/version9.html +143 -0
  98. data/asciidoc/examples/website/version9.txt +48 -0
  99. data/asciidoc/filters/code-filter-readme.txt +37 -0
  100. data/asciidoc/filters/code-filter-test-c++.txt +7 -0
  101. data/asciidoc/filters/code-filter-test.txt +15 -0
  102. data/asciidoc/filters/code-filter.conf +8 -0
  103. data/asciidoc/filters/code-filter.py +239 -0
  104. data/asciidoc/filters/music-filter-test.txt +40 -0
  105. data/asciidoc/filters/music-filter.conf +40 -0
  106. data/asciidoc/filters/music2png.py +189 -0
  107. data/asciidoc/filters/source-highlight-filter-test.txt +19 -0
  108. data/asciidoc/filters/source-highlight-filter.conf +100 -0
  109. data/asciidoc/help.conf +213 -0
  110. data/asciidoc/html4.conf +363 -0
  111. data/asciidoc/images/highlighter.png +0 -0
  112. data/asciidoc/images/icons/README +5 -0
  113. data/asciidoc/images/icons/callouts/1.png +0 -0
  114. data/asciidoc/images/icons/callouts/10.png +0 -0
  115. data/asciidoc/images/icons/callouts/11.png +0 -0
  116. data/asciidoc/images/icons/callouts/12.png +0 -0
  117. data/asciidoc/images/icons/callouts/13.png +0 -0
  118. data/asciidoc/images/icons/callouts/14.png +0 -0
  119. data/asciidoc/images/icons/callouts/15.png +0 -0
  120. data/asciidoc/images/icons/callouts/2.png +0 -0
  121. data/asciidoc/images/icons/callouts/3.png +0 -0
  122. data/asciidoc/images/icons/callouts/4.png +0 -0
  123. data/asciidoc/images/icons/callouts/5.png +0 -0
  124. data/asciidoc/images/icons/callouts/6.png +0 -0
  125. data/asciidoc/images/icons/callouts/7.png +0 -0
  126. data/asciidoc/images/icons/callouts/8.png +0 -0
  127. data/asciidoc/images/icons/callouts/9.png +0 -0
  128. data/asciidoc/images/icons/caution.png +0 -0
  129. data/asciidoc/images/icons/example.png +0 -0
  130. data/asciidoc/images/icons/home.png +0 -0
  131. data/asciidoc/images/icons/important.png +0 -0
  132. data/asciidoc/images/icons/next.png +0 -0
  133. data/asciidoc/images/icons/note.png +0 -0
  134. data/asciidoc/images/icons/prev.png +0 -0
  135. data/asciidoc/images/icons/tip.png +0 -0
  136. data/asciidoc/images/icons/up.png +0 -0
  137. data/asciidoc/images/icons/warning.png +0 -0
  138. data/asciidoc/images/smallnew.png +0 -0
  139. data/asciidoc/images/tiger.png +0 -0
  140. data/asciidoc/install.sh +55 -0
  141. data/asciidoc/javascripts/ASCIIMathML.js +938 -0
  142. data/asciidoc/javascripts/LaTeXMathML.js +1223 -0
  143. data/asciidoc/javascripts/toc.js +69 -0
  144. data/asciidoc/lang-es.conf +15 -0
  145. data/asciidoc/latex.conf +663 -0
  146. data/asciidoc/linuxdoc.conf +285 -0
  147. data/asciidoc/math.conf +50 -0
  148. data/asciidoc/stylesheets/docbook-xsl.css +271 -0
  149. data/asciidoc/stylesheets/xhtml-deprecated-manpage.css +21 -0
  150. data/asciidoc/stylesheets/xhtml-deprecated.css +247 -0
  151. data/asciidoc/stylesheets/xhtml11-manpage.css +18 -0
  152. data/asciidoc/stylesheets/xhtml11-quirks.css +49 -0
  153. data/asciidoc/stylesheets/xhtml11.css +284 -0
  154. data/asciidoc/t.conf +20 -0
  155. data/asciidoc/text.conf +16 -0
  156. data/asciidoc/vim/ftdetect/asciidoc_filetype.vim +53 -0
  157. data/asciidoc/vim/syntax/asciidoc.vim +139 -0
  158. data/asciidoc/xhtml-deprecated-css.conf +235 -0
  159. data/asciidoc/xhtml-deprecated.conf +351 -0
  160. data/asciidoc/xhtml11-quirks.conf +57 -0
  161. data/asciidoc/xhtml11.conf +514 -0
  162. data/bin/mizuho +40 -0
  163. data/lib/mizuho/chapter.rb +54 -0
  164. data/lib/mizuho/generator.rb +106 -0
  165. data/lib/mizuho/heading.rb +46 -0
  166. data/lib/mizuho/parser.rb +99 -0
  167. data/lib/mizuho/template.rb +50 -0
  168. data/mizuho.gemspec +34 -0
  169. data/templates/asciidoc.css +358 -0
  170. data/templates/asciidoc.html.erb +86 -0
  171. data/templates/manualsonrails.css +165 -0
  172. data/templates/manualsonrails.html.erb +97 -0
  173. data/test/parser_spec.rb +190 -0
  174. data/test/spec_helper.rb +43 -0
  175. metadata +234 -0
@@ -0,0 +1,404 @@
1
+ #
2
+ # asciidoc.conf
3
+ #
4
+ # Asciidoc global configuration file.
5
+ # Contains backend independent configuration settings that are applied to all
6
+ # AsciiDoc documents.
7
+ #
8
+
9
+ [miscellaneous]
10
+ tabsize=8
11
+ textwidth=70
12
+ newline=\r\n
13
+
14
+ [attributes]
15
+ sectids=
16
+ iconsdir=./images/icons
17
+ encoding=UTF-8
18
+ quirks=
19
+ empty=
20
+ # Attribute and AttributeList element patterns.
21
+ attributeentry-pattern=^:(?P<attrname>[a-zA-Z0-9].*?):(?P<attrvalue>.*)$
22
+ attributelist-pattern=(?u)(^\[\[(?P<id>[\w\-_]+)\]\]$)|(^\[(?P<attrlist>.*)\]$)
23
+ # Substitution attributes for escaping AsciiDoc processing.
24
+ amp=&
25
+ lt=<
26
+ gt=>
27
+ brvbar=|
28
+ nbsp=&#160;
29
+ backslash=\
30
+ two_colons=::
31
+ two_semicolons=;;
32
+ # Captions, used by XHTML backends.
33
+ # The reason these language specific attributes are not in a lang-en.conf
34
+ # file is so that the output will fall back to English if the specified
35
+ # language file is missing.
36
+ caution_caption=Caution
37
+ important_caption=Important
38
+ note_caption=Note
39
+ tip_caption=Tip
40
+ warning_caption=Warning
41
+ figure_caption="Figure: "
42
+ table_caption="Table: "
43
+ toc_title=Table of Contents
44
+
45
+ [titles]
46
+ subs=specialcharacters,quotes,replacements,macros,attributes
47
+ # Double-line title pattern and underlines.
48
+ sectiontitle=^(?P<title>.*?)$
49
+ underlines="==","--","~~","^^","++"
50
+ # Single-line title patterns.
51
+ sect0=^= +(?P<title>[\S].*?)( +=)?$
52
+ sect1=^== +(?P<title>[\S].*?)( +==)?$
53
+ sect2=^=== +(?P<title>[\S].*?)( +===)?$
54
+ sect3=^==== +(?P<title>[\S].*?)( +====)?$
55
+ sect4=^===== +(?P<title>[\S].*?)( +=====)?$
56
+ blocktitle=^\.(?P<title>([^.\s].*)|(\.[^.\s].*))$
57
+
58
+ [specialcharacters]
59
+ &=&amp;
60
+ <=&lt;
61
+ >=&gt;
62
+
63
+ [quotes]
64
+ # Constrained quotes.
65
+ *=strong
66
+ '=emphasis
67
+ `=monospaced
68
+ ``|''=quoted
69
+ ifdef::asciidoc7compatible[]
70
+ \##=unquoted
71
+ endif::asciidoc7compatible[]
72
+ ifndef::asciidoc7compatible[]
73
+ \#=unquoted
74
+ _=emphasis
75
+ +=monospaced
76
+ # Unconstrained quotes.
77
+ **=#strong
78
+ __=#emphasis
79
+ ++=#monospaced
80
+ \##=#unquoted
81
+ ^=#superscript
82
+ ~=#subscript
83
+ endif::asciidoc7compatible[]
84
+
85
+ [specialwords]
86
+ emphasizedwords=
87
+ strongwords=
88
+ monospacedwords=
89
+
90
+ [tags]
91
+ # $$ inline passthrough.
92
+ passthrough=|
93
+
94
+ [replacements]
95
+ # Replacements performed in order of configuration file entry. The first entry
96
+ # of each replacement pair performs the (non-escaped) replacement, the second
97
+ # strips the backslash from the esaped replacement.
98
+
99
+ # (C) Copyright (entity reference &copy;)
100
+ (?<!\\)\(C\)=&#169;
101
+ \\\(C\)=(C)
102
+
103
+ # (R) registered trade mark (entity reference &reg;
104
+ (?<!\\)\(R\)=&#174;
105
+ \\\(R\)=(R)
106
+
107
+ # (TM) Trademark (entity reference &trade;)
108
+ (?<!\\)\(TM\)=&#8482;
109
+ \\\(TM\)=(TM)
110
+
111
+ # -- Spaced and unspaced em dashes (entity reference &mdash;)
112
+ # But disallow unspaced in man pages because double-dash option name prefixes
113
+ # are pervasive.
114
+ ifndef::doctype-manpage[]
115
+ (^|[^-\\])--($|[^-])=\1&#8212;\2
116
+ endif::doctype-manpage[]
117
+ ifdef::doctype-manpage[]
118
+ (^|\s*[^\S\\])--($|\s+)=\1&#8212;\2
119
+ endif::doctype-manpage[]
120
+ \\--(?!-)=--
121
+
122
+ # ... Ellipsis (entity reference &hellip;)
123
+ (?<!\\)\.\.\.=&#8230;
124
+ \\\.\.\.=...
125
+
126
+ # Arrows from the Arrows block of Unicode.
127
+ # -> right arrow
128
+ (?<!\\)-&gt;=&#8594;
129
+ \\-&gt;=-&gt;
130
+ # => right double arrow
131
+ (?<!\\)\=&gt;=&#8658;
132
+ \\\=&gt;==&gt;
133
+ # <- left arrow
134
+ (?<!\\)&lt;-=&#8592;
135
+ \\&lt;-=&lt;-
136
+ # <= left double arrow
137
+ (?<!\\)&lt;\==&#8656;
138
+ \\&lt;\==&lt;=
139
+
140
+
141
+ # Paragraphs.
142
+ [paradef-default]
143
+ delimiter=(?s)(?P<text>\S.*)
144
+ template=paragraph
145
+ posattrs=style
146
+ verse-style=template="verseparagraph"
147
+ NOTE-style=template="admonitionparagraph",name="note",caption="{note_caption}"
148
+ TIP-style=template="admonitionparagraph",name="tip",caption="{tip_caption}"
149
+ IMPORTANT-style=template="admonitionparagraph",name="important",caption="{important_caption}"
150
+ WARNING-style=template="admonitionparagraph",name="warning",caption="{warning_caption}"
151
+ CAUTION-style=template="admonitionparagraph",name="caution",caption="{caution_caption}"
152
+
153
+ [paradef-literal]
154
+ delimiter=(?s)(?P<text>\s+.*)
155
+ options=listelement
156
+ template=literalparagraph
157
+ subs=verbatim
158
+
159
+ [paradef-admonition]
160
+ delimiter=(?s)^\s*(?P<style>NOTE|TIP|IMPORTANT|WARNING|CAUTION):\s+(?P<text>.+)
161
+ NOTE-style=template="admonitionparagraph",name="note",caption="{note_caption}"
162
+ TIP-style=template="admonitionparagraph",name="tip",caption="{tip_caption}"
163
+ IMPORTANT-style=template="admonitionparagraph",name="important",caption="{important_caption}"
164
+ WARNING-style=template="admonitionparagraph",name="warning",caption="{warning_caption}"
165
+ CAUTION-style=template="admonitionparagraph",name="caution",caption="{caution_caption}"
166
+
167
+ [macros]
168
+ # Inline macros.
169
+ # Backslash prefix required for escape processing.
170
+ # (?s) re flag for line spanning.
171
+
172
+ # URLs, images and links with attribute list. Explicit so they can be nested.
173
+ (?su)[\\]?(?P<name>http|https|ftp|file|mailto|callto|image|link):(?P<target>\S*?)(\[(?P<attrlist>.*?)\])=
174
+
175
+ # These URL types don't require any special attribute list formatting.
176
+ (?su)(?<!\S)[\\]?(?P<name>http|https|ftp|file):(?P<target>//\S*[\w/])=
177
+ # Allow a leading parenthesis.
178
+ (?su)(?<\=\()[\\]?(?P<name>http|https|ftp|file):(?P<target>//\S*[\w/])=
179
+ # Allow <> brackets.
180
+ (?su)[\\]?&lt;(?P<name>http|https|ftp|file):(?P<target>//\S*[\w/])&gt;=
181
+
182
+ # Email addresses don't require special attribute list formatting.
183
+ # The before ">: and after "< character exclusions stop multiple substitution.
184
+ (?su)(?<![">:\w])[\\]?(?P<target>\w[\w._-]*@[\w._-]*\w)(?!["<\w.])=mailto
185
+
186
+ # Anchor: [[[id]]]. Bibliographic anchor.
187
+ (?su)[\\]?\[\[\[(?P<attrlist>[\w][\w-]*?)\]\]\]=anchor3
188
+ # Anchor: [[id,xreflabel]]
189
+ (?su)[\\]?\[\[(?P<attrlist>[\w"].*?)\]\]=anchor2
190
+ # Link: <<id,text>>
191
+ (?su)[\\]?&lt;&lt;(?P<attrlist>[\w"].*?)&gt;&gt;=xref2
192
+
193
+ ifdef::asciidoc7compatible[]
194
+ # Index term: ++primary,secondary,tertiary++
195
+ (?su)(?<!\S)[\\]?\+\+(?P<attrlist>[^+].*?)\+\+(?!\+)=indexterm
196
+ # Index term: +primary+
197
+ # Follows ++...++ macro otherwise it will match them.
198
+ (?<!\S)[\\]?\+(?P<attrlist>[^\s\+][^+].*?)\+(?!\+)=indexterm2
199
+ endif::asciidoc7compatible[]
200
+
201
+ ifndef::asciidoc7compatible[]
202
+ # Index term: (((primary,secondary,tertiary)))
203
+ (?su)(?<!\()[\\]?\(\(\((?P<attrlist>[^(].*?)\)\)\)(?!\))=indexterm
204
+ # Index term: ((primary))
205
+ # Follows (((...))) macro otherwise it will match them.
206
+ (?<!\()[\\]?\(\((?P<attrlist>[^\s\(][^(].*?)\)\)(?!\))=indexterm2
207
+ endif::asciidoc7compatible[]
208
+
209
+ # Callout
210
+ [\\]?&lt;(?P<index>\d+)&gt;=callout
211
+
212
+ # Default inline macro (listed last as a catchall)).
213
+ (?su)[\\]?(?P<name>\w(\w|-)*?):(?P<target>\S*?)(\[(?P<attrlist>.*?)\])=
214
+
215
+ # Block macros.
216
+ (?u)^(?P<name>\w(\w|-)*?)::(?P<target>\S*?)(\[(?P<attrlist>.*?)\])$=#
217
+ ^'{4,}$=#ruler
218
+ ^//([^/].*|)$=#comment
219
+
220
+ # System macros.
221
+ # This default system macro is hardwired into asciidoc.
222
+ #(?u)^(?P<name>\w(\w|-)*?)::(?P<target>\S*?)(\[(?P<attrlist>.*?)\])$=+
223
+
224
+ # Delimited blocks.
225
+ [blockdef-comment]
226
+ delimiter=^/{4,}
227
+ options=skip
228
+
229
+ [comment-blockmacro]
230
+ # Outputs nothing.
231
+
232
+ [blockdef-sidebar]
233
+ delimiter=^\*{4,}$
234
+ template=sidebarblock
235
+ options=sectionbody
236
+
237
+ [blockdef-list]
238
+ delimiter=^--$
239
+ template=listblock
240
+ options=list
241
+
242
+ [listblock]
243
+ |
244
+
245
+ [blockdef-passthrough]
246
+ delimiter=^\+{4,}$
247
+ template=passthroughblock
248
+ subs=attributes,macros
249
+
250
+ [blockdef-listing]
251
+ delimiter=^-{4,}$
252
+ template=listingblock
253
+ subs=verbatim
254
+ posattrs=style
255
+
256
+ [blockdef-literal]
257
+ delimiter=^\.{4,}$
258
+ template=literalblock
259
+ subs=verbatim
260
+ posattrs=style
261
+ # DEPRECATED: Use verse style on quote blocks instead.
262
+ verse-style=template="verseblock",subs="normal"
263
+
264
+ [blockdef-quote]
265
+ delimiter=^_{4,}$
266
+ subs=normal
267
+ style=quote
268
+ posattrs=style,attribution,citetitle
269
+ quote-style=template="quoteblock",options=("sectionbody",)
270
+ verse-style=template="verseblock"
271
+
272
+ [blockdef-example]
273
+ delimiter=^={4,}$
274
+ template=exampleblock
275
+ options=sectionbody
276
+ posattrs=style
277
+ NOTE-style=template="admonitionblock",name="note",caption="{note_caption}"
278
+ TIP-style=template="admonitionblock",name="tip",caption="{tip_caption}"
279
+ IMPORTANT-style=template="admonitionblock",name="important",caption="{important_caption}"
280
+ WARNING-style=template="admonitionblock",name="warning",caption="{warning_caption}"
281
+ CAUTION-style=template="admonitionblock",name="caution",caption="{caution_caption}"
282
+
283
+ # For use by custom filters.
284
+ [blockdef-filter]
285
+ delimiter=^~{4,}$
286
+ template=listingblock
287
+ subs=none
288
+ posattrs=style
289
+
290
+
291
+ # Lists.
292
+ [listdef-bulleted]
293
+ type=bulleted
294
+ delimiter=^\s*- +(?P<text>.+)$
295
+ listtag=ilist
296
+ itemtag=ilistitem
297
+ texttag=ilisttext
298
+
299
+ [listdef-bulleted2]
300
+ type=bulleted
301
+ delimiter=^\s*\* +(?P<text>.+)$
302
+ listtag=ilist
303
+ itemtag=ilistitem
304
+ texttag=ilisttext
305
+
306
+ [listdef-numbered]
307
+ type=numbered
308
+ delimiter=^\s*(?P<index>\d*)\. +(?P<text>.+)$
309
+ listtag=olist
310
+ itemtag=olistitem
311
+ texttag=olisttext
312
+
313
+ [listdef-numbered2]
314
+ type=numbered
315
+ delimiter=^\s*(?P<index>[.a-z])\. +(?P<text>.+)$
316
+ listtag=olist2
317
+ itemtag=olistitem
318
+ texttag=olisttext
319
+
320
+ [listdef-vlabeled]
321
+ type=labeled
322
+ delimiter=^\s*(?P<label>.*\S)::$
323
+ listtag=vlist
324
+ itemtag=vlistitem
325
+ texttag=vlisttext
326
+ entrytag=vlistentry
327
+ labeltag=vlistterm
328
+
329
+ [listdef-vlabeled2]
330
+ type=labeled
331
+ delimiter=^\s*(?P<label>.*\S);;$
332
+ listtag=vlist
333
+ itemtag=vlistitem
334
+ texttag=vlisttext
335
+ entrytag=vlistentry
336
+ labeltag=vlistterm
337
+
338
+ [listdef-hlabeled]
339
+ type=labeled
340
+ delimiter=^\s*(?P<label>.*\S)((::\s*\\)|(::\s+(?P<text>.+)))$
341
+ listtag=hlist
342
+ itemtag=hlistitem
343
+ texttag=hlisttext
344
+ entrytag=hlistentry
345
+ labeltag=hlistterm
346
+
347
+ [listdef-hlabeled2]
348
+ type=labeled
349
+ delimiter=^\s*(?P<label>.*\S)((;;\s*\\)|(;;\s+(?P<text>.+)))$
350
+ listtag=hlist
351
+ itemtag=hlistitem
352
+ texttag=hlisttext
353
+ entrytag=hlistentry
354
+ labeltag=hlistterm
355
+
356
+
357
+ # Question and Answer list.
358
+ [listdef-qanda]
359
+ type=labeled
360
+ delimiter=^\s*(?P<label>.*\S)\?\?$
361
+ listtag=qlist
362
+ itemtag=qlistitem
363
+ texttag=qlisttext
364
+ entrytag=qlistentry
365
+ labeltag=qlistterm
366
+
367
+ # Bibliography list.
368
+ [listdef-bibliography]
369
+ type=bulleted
370
+ delimiter=^\+ +(?P<text>.+)$
371
+ listtag=blist
372
+ itemtag=blistitem
373
+ texttag=blisttext
374
+
375
+ # Glossary list.
376
+ [listdef-glossary]
377
+ type=labeled
378
+ delimiter=^(?P<label>.*\S):-$
379
+ listtag=glist
380
+ itemtag=glistitem
381
+ texttag=glisttext
382
+ entrytag=glistentry
383
+ labeltag=glistterm
384
+
385
+ # Callout list.
386
+ [listdef-callout]
387
+ type=callout
388
+ delimiter=^<?(?P<index>\d*)> +(?P<text>.+)$
389
+ listtag=colist
390
+ itemtag=colistitem
391
+ texttag=colisttext
392
+
393
+ # Tables.
394
+ [tabledef-default]
395
+ fillchar=-
396
+ format=fixed
397
+
398
+ [tabledef-csv]
399
+ fillchar=~
400
+ format=csv
401
+
402
+ [tabledef-dsv]
403
+ fillchar=_
404
+ format=dsv
@@ -0,0 +1,4255 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ asciidoc - converts an AsciiDoc text file to DocBook, HTML or LinuxDoc
4
+
5
+ Copyright (C) 2002-2008 Stuart Rackham. Free use of this software is granted
6
+ under the terms of the GNU General Public License (GPL).
7
+ """
8
+
9
+ import sys, os, re, time, traceback, tempfile, popen2, codecs, locale
10
+ from types import *
11
+
12
+ VERSION = '8.2.7' # See CHANGLOG file for version history.
13
+
14
+ #---------------------------------------------------------------------------
15
+ # Program onstants.
16
+ #---------------------------------------------------------------------------
17
+ DEFAULT_BACKEND = 'xhtml11'
18
+ DEFAULT_DOCTYPE = 'article'
19
+ # Allowed substitution options for List, Paragraph and DelimitedBlock
20
+ # definition subs entry.
21
+ SUBS_OPTIONS = ('specialcharacters','quotes','specialwords',
22
+ 'replacements', 'attributes','macros','callouts','normal','verbatim',
23
+ 'none','passthroughs','replacements2')
24
+ # Default value for unspecified subs and presubs configuration file entries.
25
+ SUBS_NORMAL = ('specialcharacters','quotes','attributes',
26
+ 'specialwords','replacements','macros','passthroughs')
27
+ SUBS_VERBATIM = ('specialcharacters','callouts')
28
+
29
+ NAME_RE = r'(?u)[^\W\d][-\w]*' # Valid section or attrbibute name.
30
+
31
+
32
+ #---------------------------------------------------------------------------
33
+ # Utility functions and classes.
34
+ #---------------------------------------------------------------------------
35
+
36
+ class EAsciiDoc(Exception):
37
+ pass
38
+
39
+
40
+ from UserDict import UserDict
41
+
42
+ class OrderedDict(UserDict):
43
+ """
44
+ Dictionary ordered by insertion order.
45
+ Python Cookbook: Ordered Dictionary, Submitter: David Benjamin.
46
+ http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747
47
+ """
48
+ def __init__(self, d=None):
49
+ self._keys = []
50
+ UserDict.__init__(self, d)
51
+ def __delitem__(self, key):
52
+ UserDict.__delitem__(self, key)
53
+ self._keys.remove(key)
54
+ def __setitem__(self, key, item):
55
+ UserDict.__setitem__(self, key, item)
56
+ if key not in self._keys: self._keys.append(key)
57
+ def clear(self):
58
+ UserDict.clear(self)
59
+ self._keys = []
60
+ def copy(self):
61
+ d = UserDict.copy(self)
62
+ d._keys = self._keys[:]
63
+ return d
64
+ def items(self):
65
+ return zip(self._keys, self.values())
66
+ def keys(self):
67
+ return self._keys
68
+ def popitem(self):
69
+ try:
70
+ key = self._keys[-1]
71
+ except IndexError:
72
+ raise KeyError('dictionary is empty')
73
+ val = self[key]
74
+ del self[key]
75
+ return (key, val)
76
+ def setdefault(self, key, failobj = None):
77
+ UserDict.setdefault(self, key, failobj)
78
+ if key not in self._keys: self._keys.append(key)
79
+ def update(self, d=None, **kwargs):
80
+ if d is None:
81
+ d = kwargs
82
+ UserDict.update(self, d)
83
+ for key in d.keys():
84
+ if key not in self._keys: self._keys.append(key)
85
+ def values(self):
86
+ return map(self.get, self._keys)
87
+
88
+ def print_stderr(line):
89
+ sys.stderr.write(line+os.linesep)
90
+
91
+ def verbose(msg,linenos=True):
92
+ if config.verbose:
93
+ console(msg,linenos=linenos)
94
+
95
+ def warning(msg,linenos=True):
96
+ console(msg,'WARNING: ',linenos)
97
+ document.has_warnings = True
98
+
99
+ def deprecated(old, new, linenos=True):
100
+ console('%s: %s' % (old,new), 'DEPRECATED: ', linenos)
101
+
102
+ def error(msg, cursor=None):
103
+ """Report fatal error but don't exit application, continue in the hope of
104
+ reporting all fatal errors finishing with a non-zero exit code."""
105
+ console(msg,'ERROR: ', cursor=cursor)
106
+ document.has_errors = True
107
+
108
+ def console(msg, prefix='', linenos=True, cursor=None):
109
+ """Print message to stderr. 'offset' is added to reported line number for
110
+ warnings emitted when reading ahead."""
111
+ s = prefix
112
+ if linenos and reader.cursor:
113
+ if not cursor:
114
+ cursor = reader.cursor
115
+ s = s + '%s: line %d: ' % (os.path.basename(cursor[0]),cursor[1])
116
+ s = s + msg
117
+ print_stderr(s)
118
+
119
+ def file_in(fname, directory):
120
+ """Return True if file fname resides inside directory."""
121
+ assert os.path.isfile(fname)
122
+ # Empty directory (not to be confused with None) is the current directory.
123
+ if directory == '':
124
+ directory = os.getcwd()
125
+ else:
126
+ assert os.path.isdir(directory)
127
+ directory = os.path.abspath(directory)
128
+ fname = os.path.realpath(fname)
129
+ return os.path.commonprefix((directory, fname)) == directory
130
+
131
+ def safe():
132
+ return document.safe
133
+
134
+ def is_safe_file(fname, directory=None):
135
+ # A safe file must reside in directory directory (defaults to the source
136
+ # file directory).
137
+ if directory is None:
138
+ if document.infile == '<stdin>':
139
+ return not safe()
140
+ directory = os.path.dirname(document.infile)
141
+ elif directory == '':
142
+ directory = '.'
143
+ return not safe() or file_in(fname, directory)
144
+
145
+ # Return file name which must reside in the parent file directory.
146
+ # Return None if file is not found or not safe.
147
+ def safe_filename(fname, parentdir):
148
+ if not os.path.isabs(fname):
149
+ # Include files are relative to parent document
150
+ # directory.
151
+ fname = os.path.join(parentdir,fname)
152
+ if not os.path.isfile(fname):
153
+ warning('include file not found: %s' % fname)
154
+ return None
155
+ if not is_safe_file(fname, parentdir):
156
+ unsafe_error('include file: %s' % fname)
157
+ return None
158
+ return fname
159
+
160
+ def unsafe_error(msg):
161
+ error('unsafe: '+msg)
162
+
163
+ def syseval(cmd):
164
+ # Run shell command and return stdout.
165
+ child = os.popen(cmd)
166
+ data = child.read()
167
+ err = child.close()
168
+ if not err:
169
+ return data
170
+ else:
171
+ return ''
172
+
173
+ def assign(dst,src):
174
+ """Assign all attributes from 'src' object to 'dst' object."""
175
+ for a,v in src.__dict__.items():
176
+ setattr(dst,a,v)
177
+
178
+ def strip_quotes(s):
179
+ """Trim white space and, if necessary, quote characters from s."""
180
+ s = s.strip()
181
+ # Strip quotation mark characters from quoted strings.
182
+ if len(s) >= 3 and s[0] == '"' and s[-1] == '"':
183
+ s = s[1:-1]
184
+ return s
185
+
186
+ def is_regexp(s):
187
+ """Return True if s is a valid regular expression else return False."""
188
+ try: re.compile(s)
189
+ except: return False
190
+ else: return True
191
+
192
+ def join_regexp(relist):
193
+ """Join list of regular expressions re1,re2,... to single regular
194
+ expression (re1)|(re2)|..."""
195
+ if len(relist) == 0:
196
+ return None
197
+ result = []
198
+ # Delete named groups to avoid ambiguity.
199
+ for s in relist:
200
+ result.append(re.sub(r'\?P<\S+?>','',s))
201
+ result = ')|('.join(result)
202
+ result = '('+result+')'
203
+ return result
204
+
205
+ def validate(value,rule,errmsg):
206
+ """Validate value against rule expression. Throw EAsciiDoc exception with
207
+ errmsg if validation fails."""
208
+ try:
209
+ if not eval(rule.replace('$',str(value))):
210
+ raise EAsciiDoc,errmsg
211
+ except:
212
+ raise EAsciiDoc,errmsg
213
+ return value
214
+
215
+ def join_lines(lines):
216
+ """Return a list in which lines terminated with the backslash line
217
+ continuation character are joined."""
218
+ result = []
219
+ s = ''
220
+ continuation = False
221
+ for line in lines:
222
+ if line and line[-1] == '\\':
223
+ s = s + line[:-1]
224
+ continuation = True
225
+ continue
226
+ if continuation:
227
+ result.append(s+line)
228
+ s = ''
229
+ continuation = False
230
+ else:
231
+ result.append(line)
232
+ if continuation:
233
+ result.append(s)
234
+ return result
235
+
236
+ def dovetail(lines1, lines2):
237
+ """Append list or tuple of strings 'lines2' to list 'lines1'. Join the
238
+ last string in 'lines1' with the first string in 'lines2' into a single
239
+ string."""
240
+ assert isinstance(lines1,list) or isinstance(lines1,tuple)
241
+ assert isinstance(lines2,list) or isinstance(lines2,tuple)
242
+ if not lines1 or not lines2:
243
+ return list(lines1) + list(lines2)
244
+ result = list(lines1[:-1])
245
+ result.append(lines1[-1] + lines2[0])
246
+ result += list(lines2[1:])
247
+ return result
248
+
249
+ def dovetail_tags(stag,content,etag):
250
+ """Merge the end tag with the first content line and the last
251
+ content line with the end tag. This ensures verbatim elements don't
252
+ include extraneous opening and closing line breaks."""
253
+ return dovetail(dovetail(stag,content), etag)
254
+
255
+ def parse_attributes(attrs,dict):
256
+ """Update a dictionary with name/value attributes from the attrs string.
257
+ The attrs string is a comma separated list of values and keyword name=value
258
+ pairs. Values must preceed keywords and are named '1','2'... The entire
259
+ attributes list is named '0'. If keywords are specified string values must
260
+ be quoted. Examples:
261
+
262
+ attrs: ''
263
+ dict: {}
264
+
265
+ attrs: 'hello,world'
266
+ dict: {'2': 'world', '0': 'hello,world', '1': 'hello'}
267
+
268
+ attrs: '"hello", planet="earth"'
269
+ dict: {'planet': 'earth', '0': '"hello",planet="earth"', '1': 'hello'}
270
+ """
271
+ def f(*args,**keywords):
272
+ # Name and add aguments '1','2'... to keywords.
273
+ for i in range(len(args)):
274
+ if not keywords.has_key(str(i+1)):
275
+ keywords[str(i+1)] = args[i]
276
+ return keywords
277
+
278
+ if not attrs:
279
+ return
280
+ dict['0'] = attrs
281
+ # Replace line separators with spaces so line spanning works.
282
+ s = re.sub(r'\s', ' ', attrs)
283
+ try:
284
+ d = eval('f('+s+')')
285
+ # Attributes must evaluate to strings, numbers or None.
286
+ for v in d.values():
287
+ if not (isinstance(v,str) or isinstance(v,int) or isinstance(v,float) or v is None):
288
+ raise
289
+ dict.update(d)
290
+ except:
291
+ # Try quoting the attrs.
292
+ s = s.replace('"',r'\"') # Escape double-quotes.
293
+ s = s.split(',')
294
+ s = map(lambda x: '"' + x.strip() + '"', s)
295
+ s = ','.join(s)
296
+ try:
297
+ d = eval('f('+s+')')
298
+ except:
299
+ return # If there's a syntax error leave with {0}=attrs.
300
+ for k in d.keys(): # Drop any empty positional arguments.
301
+ if d[k] == '': del d[k]
302
+ dict.update(d)
303
+ assert len(d) > 0
304
+
305
+ def parse_named_attributes(s,attrs):
306
+ """Update a attrs dictionary with name="value" attributes from the s string.
307
+ Returns False if invalid syntax.
308
+ Example:
309
+ attrs: 'star="sun",planet="earth"'
310
+ dict: {'planet':'earth', 'star':'sun'}
311
+ """
312
+ def f(**keywords): return keywords
313
+
314
+ try:
315
+ d = eval('f('+s+')')
316
+ attrs.update(d)
317
+ return True
318
+ except:
319
+ return False
320
+
321
+ def parse_list(s):
322
+ """Parse comma separated string of Python literals. Return a tuple of of
323
+ parsed values."""
324
+ try:
325
+ result = eval('tuple(['+s+'])')
326
+ except:
327
+ raise EAsciiDoc,'malformed list: '+s
328
+ return result
329
+
330
+ def parse_options(options,allowed,errmsg):
331
+ """Parse comma separated string of unquoted option names and return as a
332
+ tuple of valid options. 'allowed' is a list of allowed option values.
333
+ If allowed=() then all legitimate names are allowed.
334
+ 'errmsg' is an error message prefix if an illegal option error is thrown."""
335
+ result = []
336
+ if options:
337
+ for s in re.split(r'\s*,\s*',options):
338
+ if (allowed and s not in allowed) or (s == '' or not is_name(s)):
339
+ raise EAsciiDoc,'%s: %s' % (errmsg,s)
340
+ result.append(s)
341
+ return tuple(result)
342
+
343
+ def symbolize(s):
344
+ """Drop non-symbol characters and convert to lowercase."""
345
+ return re.sub(r'(?u)[^\w\-_]', '', s).lower()
346
+
347
+ def is_name(s):
348
+ """Return True if s is valid attribute, macro or tag name
349
+ (starts with alpha containing alphanumeric and dashes only)."""
350
+ return re.match(NAME_RE,s) is not None
351
+
352
+ def subs_quotes(text):
353
+ """Quoted text is marked up and the resulting text is
354
+ returned."""
355
+ # The quote patterns are iterated in reverse sort order to avoid ambiguity.
356
+ # So, for example, __ is processed before _.
357
+ keys = config.quotes.keys()
358
+ keys.sort()
359
+ keys.reverse()
360
+ for q in keys:
361
+ i = q.find('|')
362
+ if i != -1 and q != '|' and q != '||':
363
+ lq = q[:i] # Left quote.
364
+ rq = q[i+1:] # Right quote.
365
+ else:
366
+ lq = rq = q
367
+ tag = config.quotes[q]
368
+ # Unconstrained quotes prefix the tag name with a hash.
369
+ if tag[0] == '#':
370
+ tag = tag[1:]
371
+ # Unconstrained quotes can appear anywhere.
372
+ reo = re.compile(r'(?msu)(^|.)(\[(?P<attrs>[^[]+?)\])?' \
373
+ + r'(?:' + re.escape(lq) + r')' \
374
+ + r'(?P<content>.+?)(?:'+re.escape(rq)+r')')
375
+ else:
376
+ # The text within constrained quotes must be bounded by white space.
377
+ # Non-word (\W) characters are allowed at boundaries to accomodate
378
+ # enveloping quotes.
379
+ reo = re.compile(r'(?msu)(^|\W)(\[(?P<attrs>[^[]+?)\])?' \
380
+ + r'(?:' + re.escape(lq) + r')' \
381
+ + r'(?P<content>.+?)(?:'+re.escape(rq)+r')(?=\W|$)')
382
+ pos = 0
383
+ while True:
384
+ mo = reo.search(text,pos)
385
+ if not mo: break
386
+ if text[mo.start()] == '\\':
387
+ pos = mo.end()
388
+ else:
389
+ attrs = {}
390
+ parse_attributes(mo.group('attrs'), attrs)
391
+ stag,etag = config.tag(tag, attrs)
392
+ s = mo.group(1) + stag + mo.group('content') + etag
393
+ text = text[:mo.start()] + s + text[mo.end():]
394
+ pos = mo.start() + len(s)
395
+ # Unescape escaped quotes.
396
+ text = text.replace('\\'+lq, lq)
397
+ if lq != rq:
398
+ text = text.replace('\\'+rq, rq)
399
+ return text
400
+
401
+ def subs_tag(tag,dict={}):
402
+ """Perform attribute substitution and split tag string returning start, end
403
+ tag tuple (c.f. Config.tag())."""
404
+ s = subs_attrs(tag,dict)
405
+ if not s:
406
+ warning('tag "%s" dropped: contains undefined attribute' % tag)
407
+ return [None,None]
408
+ result = s.split('|')
409
+ if len(result) == 1:
410
+ return result+[None]
411
+ elif len(result) == 2:
412
+ return result
413
+ else:
414
+ raise EAsciiDoc,'malformed tag: %s' % tag
415
+
416
+ def parse_entry(entry, dict=None, unquote=False, unique_values=False,
417
+ allow_name_only=False, escape_delimiter=True):
418
+ """Parse name=value entry to dictionary 'dict'. Return tuple (name,value)
419
+ or None if illegal entry.
420
+ If name= then value is set to ''.
421
+ If name and allow_name_only=True then value is set to ''.
422
+ If name! and allow_name_only=True then value is set to None.
423
+ Leading and trailing white space is striped from 'name' and 'value'.
424
+ 'name' can contain any printable characters.
425
+ If the '=' delimiter character is allowed in the 'name' then
426
+ it must be escaped with a backslash and escape_delimiter must be True.
427
+ If 'unquote' is True leading and trailing double-quotes are stripped from
428
+ 'name' and 'value'.
429
+ If unique_values' is True then dictionary entries with the same value are
430
+ removed before the parsed entry is added."""
431
+ if escape_delimiter:
432
+ mo = re.search(r'(?:[^\\](=))',entry)
433
+ else:
434
+ mo = re.search(r'(=)',entry)
435
+ if mo: # name=value entry.
436
+ if mo.group(1):
437
+ name = entry[:mo.start(1)]
438
+ if escape_delimiter:
439
+ name = name.replace(r'\=','=') # Unescape \= in name.
440
+ value = entry[mo.end(1):]
441
+ elif allow_name_only and entry: # name or name! entry.
442
+ name = entry
443
+ if name[-1] == '!':
444
+ name = name[:-1]
445
+ value = None
446
+ else:
447
+ value = ''
448
+ else:
449
+ return None
450
+ if unquote:
451
+ name = strip_quotes(name)
452
+ if value is not None:
453
+ value = strip_quotes(value)
454
+ else:
455
+ name = name.strip()
456
+ if value is not None:
457
+ value = value.strip()
458
+ if not name:
459
+ return None
460
+ if dict is not None:
461
+ if unique_values:
462
+ for k,v in dict.items():
463
+ if v == value: del dict[k]
464
+ dict[name] = value
465
+ return name,value
466
+
467
+ def parse_entries(entries, dict, unquote=False, unique_values=False,
468
+ allow_name_only=False,escape_delimiter=True):
469
+ """Parse name=value entries from from lines of text in 'entries' into
470
+ dictionary 'dict'. Blank lines are skipped."""
471
+ for entry in entries:
472
+ if entry and not parse_entry(entry, dict, unquote, unique_values,
473
+ allow_name_only, escape_delimiter):
474
+ raise EAsciiDoc,'malformed section entry: %s' % entry
475
+
476
+ def load_sections(sections, fname, dir=None, namepat=NAME_RE):
477
+ """Loads sections dictionary with sections from file fname.
478
+ Existing sections are overlaid. Silently skips missing configuration
479
+ files."""
480
+ if dir:
481
+ fname = os.path.join(dir, fname)
482
+ # Sliently skip missing configuration file.
483
+ if not os.path.isfile(fname):
484
+ return
485
+ reo = re.compile(r'^\[(?P<section>'+namepat+')\]\s*$')
486
+ section,contents = '',[]
487
+ for line in open(fname):
488
+ if line and line[0] == '#': # Skip comment lines.
489
+ continue
490
+ line = line.rstrip()
491
+ found = reo.findall(line)
492
+ if found:
493
+ if section: # Store previous section.
494
+ sections[section] = contents
495
+ section = found[0].lower()
496
+ contents = []
497
+ else:
498
+ contents.append(line)
499
+ if section and contents: # Store last section.
500
+ sections[section] = contents
501
+
502
+ def dump_section(name,dict,f=sys.stdout):
503
+ """Write parameters in 'dict' as in configuration file section format with
504
+ section 'name'."""
505
+ f.write('[%s]%s' % (name,writer.newline))
506
+ for k,v in dict.items():
507
+ k = str(k)
508
+ k = k.replace('=',r'\=') # Escape = in name.
509
+ # Quote if necessary.
510
+ if len(k) != len(k.strip()):
511
+ k = '"'+k+'"'
512
+ if v and len(v) != len(v.strip()):
513
+ v = '"'+v+'"'
514
+ if v is None:
515
+ # Don't dump undefined attributes.
516
+ continue
517
+ else:
518
+ s = k+'='+v
519
+ if s[0] == '#':
520
+ s = '\\' + s # Escape so not treated as comment lines.
521
+ f.write('%s%s' % (s,writer.newline))
522
+ f.write(writer.newline)
523
+
524
+ def update_attrs(attrs,dict):
525
+ """Update 'attrs' dictionary with parsed attributes in dictionary 'dict'."""
526
+ for k,v in dict.items():
527
+ if not is_name(k):
528
+ raise EAsciiDoc,'illegal attribute name: %s' % k
529
+ attrs[k] = v
530
+
531
+ def filter_lines(filter_cmd, lines, dict={}):
532
+ """
533
+ Run 'lines' through the 'filter_cmd' shell command and return the result.
534
+ The 'dict' dictionary contains additional filter attributes.
535
+ """
536
+ # BUG: Has problems finding filters with spaces in command name.
537
+ if not filter_cmd:
538
+ return lines
539
+ # Perform attributes substitution on the filter command.
540
+ s = subs_attrs(filter_cmd, dict)
541
+ if not s:
542
+ raise EAsciiDoc,'missing filter attribute: %s' % filter_cmd
543
+ filter_cmd = s
544
+ # Search for the filter command in both user and application 'filters'
545
+ # sub-directories.
546
+ mo = re.match(r'^(?P<cmd>\S+)(?P<tail>.*)$', filter_cmd)
547
+ cmd = mo.group('cmd')
548
+ found = False
549
+ if not os.path.dirname(cmd):
550
+ # Check in asciidoc user and application directories for unqualified
551
+ # file name.
552
+ if USER_DIR:
553
+ cmd2 = os.path.join(USER_DIR,'filters',cmd)
554
+ if os.path.isfile(cmd2):
555
+ found = True
556
+ if not found:
557
+ cmd2 = os.path.join(CONF_DIR,'filters',cmd)
558
+ if os.path.isfile(cmd2):
559
+ found = True
560
+ if not found:
561
+ cmd2 = os.path.join(APP_DIR,'filters',cmd)
562
+ if os.path.isfile(cmd2):
563
+ found = True
564
+ if found:
565
+ cmd = cmd2
566
+ else:
567
+ if os.__dict__.has_key('uname') and os.uname()[0][:6] == 'CYGWIN':
568
+ # popen2() does not like non-drive letter path names under
569
+ # Cygwin.
570
+ s = syseval('cygpath -m ' + cmd).strip()
571
+ if s:
572
+ cmd = s
573
+ if os.path.isfile(cmd):
574
+ found = True
575
+ else:
576
+ warning('filter not found: %s' % cmd)
577
+ if found:
578
+ filter_cmd = '"' + cmd + '"' + mo.group('tail')
579
+ verbose('filtering: ' + filter_cmd)
580
+ if sys.platform == 'win32':
581
+ # Paul Melis's <p.e.c.melis@rug.nl> patch for filters on Win32
582
+ # This workaround is necessary because Windows select() doesn't
583
+ # work with regular files.
584
+ fd,tmp = tempfile.mkstemp()
585
+ os.close(fd)
586
+ try:
587
+ try:
588
+ # Windows doesn't like running scripts directly so explicitly
589
+ # specify interpreter.
590
+ if found:
591
+ if cmd[-3:] == '.py':
592
+ filter_cmd = 'python ' + filter_cmd
593
+ elif cmd[-3:] == '.rb':
594
+ filter_cmd = 'ruby ' + filter_cmd
595
+ w = os.popen(filter_cmd + ' > "%s"' % tmp, 'w')
596
+ i = 0
597
+ while i < len(lines):
598
+ line = lines[i]
599
+ w.write(line + os.linesep)
600
+ i = i + 1
601
+ w.close()
602
+ result = []
603
+ for s in open(tmp, 'rt'):
604
+ result.append(s.rstrip())
605
+ except:
606
+ raise EAsciiDoc,'filter error: %s' % filter_cmd
607
+ finally:
608
+ os.unlink(tmp)
609
+ else:
610
+ try:
611
+ import select
612
+ result = []
613
+ r,w = popen2.popen2(filter_cmd)
614
+ # Polled I/O loop to alleviate full buffer deadlocks.
615
+ i = 0
616
+ while i < len(lines):
617
+ line = lines[i]
618
+ if select.select([],[w.fileno()],[],0)[1]:
619
+ w.write(line+os.linesep) # Use platform line terminator.
620
+ i = i+1
621
+ if select.select([r.fileno()],[],[],0)[0]:
622
+ s = r.readline()
623
+ if not s: break # Exit if filter output closes.
624
+ result.append(s.rstrip())
625
+ w.close()
626
+ for s in r:
627
+ result.append(s.rstrip())
628
+ r.close()
629
+ except:
630
+ raise EAsciiDoc,'filter error: %s' % filter_cmd
631
+ # There's no easy way to guage whether popen2() found and executed the
632
+ # filter, so guess that if it produced no output there is probably a
633
+ # problem.
634
+ if lines and not result:
635
+ warning('no output from filter: %s' % filter_cmd)
636
+ return result
637
+
638
+ def system(name, args, is_macro=False):
639
+ """Evaluate a system attribute ({name:args}) or system block macro
640
+ (name::[args]). If is_macro is True then we are processing a system
641
+ block macro otherwise it's a system attribute.
642
+ NOTE: The include1 attribute is used internally by the include1::[] macro
643
+ and is not for public use."""
644
+ if is_macro:
645
+ syntax = '%s::[%s]'
646
+ separator = '\n'
647
+ else:
648
+ syntax = '{%s:%s}'
649
+ separator = writer.newline
650
+ if name not in ('eval','sys','sys2','include','include1'):
651
+ msg = 'illegal '+syntax % (name,args)
652
+ if is_macro:
653
+ msg += ': macro name'
654
+ else:
655
+ msg += ': executable attribute name'
656
+ warning(msg)
657
+ return None
658
+ if name != 'include1':
659
+ verbose(('evaluating: '+syntax) % (name,args))
660
+ if safe() and name not in ('include','include1'):
661
+ unsafe_error(syntax % (name,args))
662
+ return None
663
+ result = None
664
+ if name == 'eval':
665
+ try:
666
+ result = eval(args)
667
+ if result is True:
668
+ result = ''
669
+ elif result is False:
670
+ result = None
671
+ elif result is not None:
672
+ result = str(result)
673
+ except:
674
+ warning((syntax+': expression evaluation error') % (name,args))
675
+ elif name in ('sys','sys2'):
676
+ result = ''
677
+ fd,tmp = tempfile.mkstemp()
678
+ os.close(fd)
679
+ try:
680
+ cmd = args
681
+ cmd = cmd + (' > %s' % tmp)
682
+ if name == 'sys2':
683
+ cmd = cmd + ' 2>&1'
684
+ if os.system(cmd):
685
+ warning((syntax+': non-zero exit status') % (name,args))
686
+ try:
687
+ if os.path.isfile(tmp):
688
+ lines = [s.rstrip() for s in open(tmp)]
689
+ else:
690
+ lines = []
691
+ except:
692
+ raise EAsciiDoc,(syntax+': temp file read error') % (name,args)
693
+ result = separator.join(lines)
694
+ finally:
695
+ if os.path.isfile(tmp):
696
+ os.remove(tmp)
697
+ elif name == 'include':
698
+ if not os.path.exists(args):
699
+ warning((syntax+': file does not exist') % (name,args))
700
+ elif not is_safe_file(args):
701
+ unsafe_error(syntax % (name,args))
702
+ else:
703
+ result = [s.rstrip() for s in open(args)]
704
+ if result:
705
+ result = subs_attrs(result)
706
+ result = separator.join(result)
707
+ result = result.expandtabs(reader.tabsize)
708
+ else:
709
+ result = ''
710
+ elif name == 'include1':
711
+ result = separator.join(config.include1[args])
712
+ else:
713
+ assert False
714
+ return result
715
+
716
+ def subs_attrs(lines, dictionary=None):
717
+ """Substitute 'lines' of text with attributes from the global
718
+ document.attributes dictionary and from t'dictionary' ('dictionary'
719
+ entries take precedence). Return a tuple of the substituted lines. 'lines'
720
+ containing undefined attributes are deleted. If 'lines' is a string then
721
+ return a string.
722
+
723
+ - Attribute references are substituted in the following order: simple,
724
+ conditional, system.
725
+ - Attribute references inside 'dictionary' entry values are substituted.
726
+ """
727
+
728
+ def end_brace(text,start):
729
+ """Return index following end brace that matches brace at start in
730
+ text."""
731
+ assert text[start] == '{'
732
+ n = 0
733
+ result = start
734
+ for c in text[start:]:
735
+ # Skip braces that are followed by a backslash.
736
+ if result == len(text)-1 or text[result+1] != '\\':
737
+ if c == '{': n = n + 1
738
+ elif c == '}': n = n - 1
739
+ result = result + 1
740
+ if n == 0: break
741
+ return result
742
+
743
+ if isinstance(lines,StringType):
744
+ string_result = True
745
+ lines = [lines]
746
+ else:
747
+ string_result = False
748
+ lines = list(lines)
749
+ if dictionary is None:
750
+ attrs = document.attributes
751
+ else:
752
+ # Remove numbered document attributes so they don't clash with
753
+ # attribute list positional attributes.
754
+ attrs = {}
755
+ for k,v in document.attributes.items():
756
+ if not re.match(r'^\d+$', k):
757
+ attrs[k] = v
758
+ # Substitute attribute references inside dictionary values.
759
+ dictionary = dictionary.copy()
760
+ for k,v in dictionary.items():
761
+ if v is None:
762
+ del dictionary[k]
763
+ else:
764
+ v = subs_attrs(str(v))
765
+ if v is None:
766
+ del dictionary[k]
767
+ else:
768
+ dictionary[k] = v
769
+ attrs.update(dictionary)
770
+ # Substitute all attributes in all lines.
771
+ for i in range(len(lines)-1,-1,-1): # Reverse iterate lines.
772
+ text = lines[i]
773
+ # Make it easier for regular expressions.
774
+ text = text.replace('\\{','{\\')
775
+ text = text.replace('\\}','}\\')
776
+ # Expand simple attributes ({name}).
777
+ # Nested attributes not allowed.
778
+ reo = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w]*?)\}(?!\\)')
779
+ pos = 0
780
+ while True:
781
+ mo = reo.search(text,pos)
782
+ if not mo: break
783
+ s = attrs.get(mo.group('name'))
784
+ if s is None:
785
+ pos = mo.end()
786
+ else:
787
+ s = str(s)
788
+ text = text[:mo.start()] + s + text[mo.end():]
789
+ pos = mo.start() + len(s)
790
+ # Expand conditional attributes.
791
+ reo = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w]*?)' \
792
+ r'(?P<op>\=|\?|!|#|%|@|\$)' \
793
+ r'(?P<value>.*?)\}(?!\\)')
794
+ pos = 0
795
+ while True:
796
+ mo = reo.search(text,pos)
797
+ if not mo: break
798
+ attr = mo.group()
799
+ name = mo.group('name')
800
+ lval = attrs.get(name)
801
+ op = mo.group('op')
802
+ # mo.end() is not good enough because '{x={y}}' matches '{x={y}'.
803
+ end = end_brace(text,mo.start())
804
+ rval = text[mo.start('value'):end-1]
805
+ if lval is None:
806
+ if op == '=': s = rval
807
+ elif op == '?': s = ''
808
+ elif op == '!': s = rval
809
+ elif op == '#': s = '{'+name+'}' # So the line is dropped.
810
+ elif op == '%': s = rval
811
+ elif op in ('@','$'):
812
+ s = '{'+name+'}' # So the line is dropped.
813
+ else:
814
+ assert False, 'illegal attribute: %s' % attr
815
+ else:
816
+ if op == '=': s = lval
817
+ elif op == '?': s = rval
818
+ elif op == '!': s = ''
819
+ elif op == '#': s = rval
820
+ elif op == '%': s = '{zzzzz}' # So the line is dropped.
821
+ elif op in ('@','$'):
822
+ v = re.split(r'(?<!\\):',rval)
823
+ if len(v) not in (2,3):
824
+ error('illegal attribute syntax: %s' % attr)
825
+ s = ''
826
+ elif not is_regexp('^'+v[0]+'$'):
827
+ error('illegal attribute regexp: %s' % attr)
828
+ s = ''
829
+ else:
830
+ v = [s.replace('\\:',':') for s in v]
831
+ re_mo = re.match('^'+v[0]+'$',lval)
832
+ if op == '@':
833
+ if re_mo:
834
+ s = v[1] # {<name>@<re>:<v1>[:<v2>]}
835
+ else:
836
+ if len(v) == 3: # {<name>@<re>:<v1>:<v2>}
837
+ s = v[2]
838
+ else: # {<name>@<re>:<v1>}
839
+ s = ''
840
+ else:
841
+ if re_mo:
842
+ if len(v) == 2: # {<name>$<re>:<v1>}
843
+ s = v[1]
844
+ elif v[1] == '': # {<name>$<re>::<v2>}
845
+ s = '{zzzzz}' # So the line is dropped.
846
+ else: # {<name>$<re>:<v1>:<v2>}
847
+ s = v[1]
848
+ else:
849
+ if len(v) == 2: # {<name>$<re>:<v1>}
850
+ s = '{zzzzz}' # So the line is dropped.
851
+ else: # {<name>$<re>:<v1>:<v2>}
852
+ s = v[2]
853
+ else:
854
+ assert False, 'illegal attribute: %s' % attr
855
+ s = str(s)
856
+ text = text[:mo.start()] + s + text[end:]
857
+ pos = mo.start() + len(s)
858
+ # Drop line if it contains unsubstituted {name} references.
859
+ skipped = re.search(r'(?su)\{[^\\\W][-\w]*?\}(?!\\)', text)
860
+ if skipped:
861
+ del lines[i]
862
+ continue;
863
+ # Expand system attributes.
864
+ reo = re.compile(r'(?su)\{(?P<action>[^\\\W][-\w]*?):(?P<expr>.*?)\}(?!\\)')
865
+ skipped = False
866
+ pos = 0
867
+ while True:
868
+ mo = reo.search(text,pos)
869
+ if not mo: break
870
+ expr = mo.group('expr')
871
+ expr = expr.replace('{\\','{')
872
+ expr = expr.replace('}\\','}')
873
+ s = system(mo.group('action'),expr)
874
+ if s is None:
875
+ skipped = True
876
+ break
877
+ text = text[:mo.start()] + s + text[mo.end():]
878
+ pos = mo.start() + len(s)
879
+ # Drop line if the action returns None.
880
+ if skipped:
881
+ del lines[i]
882
+ continue;
883
+ # Remove backslash from escaped entries.
884
+ text = text.replace('{\\','{')
885
+ text = text.replace('}\\','}')
886
+ lines[i] = text
887
+ if string_result:
888
+ if lines:
889
+ return '\n'.join(lines)
890
+ else:
891
+ return None
892
+ else:
893
+ return tuple(lines)
894
+
895
+ def char_encoding():
896
+ encoding = document.attributes.get('encoding')
897
+ if encoding:
898
+ try:
899
+ codecs.lookup(encoding)
900
+ except LookupError,e:
901
+ raise EAsciiDoc,str(e)
902
+ return encoding
903
+
904
+ def char_len(s):
905
+ return len(char_decode(s))
906
+
907
+ def char_decode(s):
908
+ if char_encoding():
909
+ try:
910
+ return s.decode(char_encoding())
911
+ except Exception:
912
+ raise EAsciiDoc, \
913
+ "'%s' codec can't decode \"%s\"" % (char_encoding(), s)
914
+ else:
915
+ return s
916
+
917
+ def char_encode(s):
918
+ if char_encoding():
919
+ return s.encode(char_encoding())
920
+ else:
921
+ return s
922
+
923
+ class Lex:
924
+ """Lexical analysis routines. Static methods and attributes only."""
925
+ prev_element = None
926
+ prev_cursor = None
927
+ def __init__(self):
928
+ raise AssertionError,'no class instances allowed'
929
+ def next():
930
+ """Returns class of next element on the input (None if EOF). The
931
+ reader is assumed to be at the first line following a previous element,
932
+ end of file or line one. Exits with the reader pointing to the first
933
+ line of the next element or EOF (leading blank lines are skipped)."""
934
+ reader.skip_blank_lines()
935
+ if reader.eof(): return None
936
+ # Optimization: If we've already checked for an element at this
937
+ # position return the element.
938
+ if Lex.prev_element and Lex.prev_cursor == reader.cursor:
939
+ return Lex.prev_element
940
+ result = None
941
+ # Check for Title.
942
+ if not result and Title.isnext():
943
+ result = Title
944
+ # Check for Block Macro.
945
+ if not result and macros.isnext():
946
+ result = macros.current
947
+ # Check for List.
948
+ if not result and lists.isnext():
949
+ result = lists.current
950
+ # Check for DelimitedBlock.
951
+ if not result and blocks.isnext():
952
+ # Skip comment blocks.
953
+ if 'skip' in blocks.current.options:
954
+ blocks.current.translate()
955
+ return Lex.next()
956
+ else:
957
+ result = blocks.current
958
+ # Check for Table.
959
+ if not result and tables.isnext():
960
+ result = tables.current
961
+ # Check for AttributeEntry.
962
+ if not result and AttributeEntry.isnext():
963
+ result = AttributeEntry
964
+ # Check for AttributeList.
965
+ if not result and AttributeList.isnext():
966
+ result = AttributeList
967
+ # Check for BlockTitle.
968
+ if not result and BlockTitle.isnext():
969
+ result = BlockTitle
970
+ # If it's none of the above then it must be an Paragraph.
971
+ if not result:
972
+ if not paragraphs.isnext():
973
+ raise EAsciiDoc,'paragraph expected'
974
+ result = paragraphs.current
975
+ # Cache answer.
976
+ Lex.prev_cursor = reader.cursor
977
+ Lex.prev_element = result
978
+ return result
979
+ next = staticmethod(next)
980
+
981
+ # Extract the passthrough text and replace with temporary placeholders.
982
+ def extract_passthroughs(text, passthroughs):
983
+ # +++ passthrough.
984
+ lq1 = r'(?P<lq>\+{3})'
985
+ rq1 = r'\+{3}'
986
+ reo1 = re.compile(r'(?msu)(^|[^\w+])(' + lq1 + r')' \
987
+ + r'(?P<content>.+?)(' + rq1 + r')(?=[^\w+]|$)')
988
+ # $$ passthrough.
989
+ lq2 = r'(\[(?P<attrs>[^[]+?)\])?(?P<lq>\${2})'
990
+ rq2 = r'\${2}'
991
+ reo2 = re.compile(r'(?msu)(^|[^\w$\]])(' + lq2 + r')' \
992
+ + r'(?P<content>.+?)(' + rq2 + r')(?=[^\w$]|$)')
993
+ reo = reo1
994
+ pos = 0
995
+ while True:
996
+ mo = reo.search(text,pos)
997
+ if not mo:
998
+ if reo == reo1:
999
+ reo = reo2
1000
+ pos = 0
1001
+ continue
1002
+ else:
1003
+ break
1004
+ if text[mo.start()] == '\\':
1005
+ pos = mo.end()
1006
+ else:
1007
+ content = mo.group('content')
1008
+ if mo.group('lq') == '$$':
1009
+ content = config.subs_specialchars(content)
1010
+ attrs = {}
1011
+ parse_attributes(mo.group('attrs'), attrs)
1012
+ stag,etag = config.tag('$$passthrough', attrs)
1013
+ if not stag:
1014
+ etag = '' # Drop end tag if start tag has been.
1015
+ content = stag + content + etag
1016
+ passthroughs.append(content)
1017
+ # Tabs are expanded when the source is read so using them here
1018
+ # guarantees the placeholders are unambiguous.
1019
+ s = mo.group(1) + '\t' + str(len(passthroughs)-1) + '\t'
1020
+ text = text[:mo.start()] + s + text[mo.end():]
1021
+ pos = mo.start() + len(s)
1022
+ # Unescape escaped passthroughs.
1023
+ text = text.replace('\\+++', '+++')
1024
+ text = text.replace('\\$$', '$$')
1025
+ return text
1026
+ extract_passthroughs = staticmethod(extract_passthroughs)
1027
+
1028
+ # Replace passthough placeholders with the original passthrough text.
1029
+ def restore_passthroughs(text, passthroughs):
1030
+ for i,v in enumerate(passthroughs):
1031
+ text = text.replace('\t'+str(i)+'\t', passthroughs[i], 1)
1032
+ return text
1033
+ restore_passthroughs = staticmethod(restore_passthroughs)
1034
+
1035
+ def subs_1(s,options):
1036
+ """Perform substitution specified in 'options' (in 'options' order) on
1037
+ a single line 's' of text. Returns the substituted string.
1038
+ Does not process 'attributes' or 'passthroughs' substitutions."""
1039
+ if not s:
1040
+ return s
1041
+ result = s
1042
+ for o in options:
1043
+ if o == 'specialcharacters':
1044
+ result = config.subs_specialchars(result)
1045
+ # Quoted text.
1046
+ elif o == 'quotes':
1047
+ result = subs_quotes(result)
1048
+ # Special words.
1049
+ elif o == 'specialwords':
1050
+ result = config.subs_specialwords(result)
1051
+ # Replacements.
1052
+ elif o in ('replacements','replacements2'):
1053
+ result = config.subs_replacements(result,o)
1054
+ # Inline macros.
1055
+ elif o == 'macros':
1056
+ result = macros.subs(result)
1057
+ elif o == 'callouts':
1058
+ result = macros.subs(result,callouts=True)
1059
+ else:
1060
+ raise EAsciiDoc,'illegal substitution option: %s' % o
1061
+ return result
1062
+ subs_1 = staticmethod(subs_1)
1063
+
1064
+ def subs(lines,options):
1065
+ """Perform inline processing specified by 'options' (in 'options'
1066
+ order) on sequence of 'lines'."""
1067
+ if len(options) == 1:
1068
+ if options[0] == 'none':
1069
+ options = ()
1070
+ elif options[0] == 'normal':
1071
+ options = config.subsnormal
1072
+ elif options[0] == 'verbatim':
1073
+ options = config.subsverbatim
1074
+ if not lines or not options:
1075
+ return lines
1076
+ # Join lines so quoting can span multiple lines.
1077
+ para = '\n'.join(lines)
1078
+ if 'passthroughs' in options:
1079
+ passthroughs = []
1080
+ para = Lex.extract_passthroughs(para,passthroughs)
1081
+ for o in options:
1082
+ if o == 'attributes':
1083
+ # If we don't substitute attributes line-by-line then a single
1084
+ # undefined attribute will drop the entire paragraph.
1085
+ lines = subs_attrs(para.split('\n'))
1086
+ para = '\n'.join(lines)
1087
+ elif o != 'passthroughs':
1088
+ para = Lex.subs_1(para,(o,))
1089
+ if 'passthroughs' in options:
1090
+ para = Lex.restore_passthroughs(para,passthroughs)
1091
+ return para.splitlines()
1092
+ subs = staticmethod(subs)
1093
+
1094
+ def set_margin(lines, margin=0):
1095
+ """Utility routine that sets the left margin to 'margin' space in a
1096
+ block of non-blank lines."""
1097
+ # Calculate width of block margin.
1098
+ lines = list(lines)
1099
+ width = len(lines[0])
1100
+ for s in lines:
1101
+ i = re.search(r'\S',s).start()
1102
+ if i < width: width = i
1103
+ # Strip margin width from all lines.
1104
+ for i in range(len(lines)):
1105
+ lines[i] = ' '*margin + lines[i][width:]
1106
+ return lines
1107
+ set_margin = staticmethod(set_margin)
1108
+
1109
+ #---------------------------------------------------------------------------
1110
+ # Document element classes parse AsciiDoc reader input and write DocBook writer
1111
+ # output.
1112
+ #---------------------------------------------------------------------------
1113
+ class Document:
1114
+ def __init__(self):
1115
+ self.doctype = None # 'article','manpage' or 'book'.
1116
+ self.backend = None # -b option argument.
1117
+ self.infile = None # Source file name.
1118
+ self.outfile = None # Output file name.
1119
+ self.attributes = {}
1120
+ self.level = 0 # 0 => front matter. 1,2,3 => sect1,2,3.
1121
+ self.has_errors = False # Set true if processing errors were flagged.
1122
+ self.has_warnings = False # Set true if warnings were flagged.
1123
+ self.safe = True # Default safe mode.
1124
+ def init_attrs(self):
1125
+ # Set implicit attributes.
1126
+ d = time.localtime(time.time())
1127
+ self.attributes['localdate'] = time.strftime('%Y-%m-%d',d)
1128
+ s = time.strftime('%H:%M:%S',d)
1129
+ if time.daylight:
1130
+ self.attributes['localtime'] = s + ' ' + time.tzname[1]
1131
+ else:
1132
+ self.attributes['localtime'] = s + ' ' + time.tzname[0]
1133
+ # Attempt to convert the localtime to the output encoding.
1134
+ try:
1135
+ self.attributes['localtime'] = char_encode(
1136
+ self.attributes['localtime'].decode(
1137
+ locale.getdefaultlocale()[1]
1138
+ )
1139
+ )
1140
+ except:
1141
+ pass
1142
+ self.attributes['asciidoc-version'] = VERSION
1143
+ self.attributes['backend'] = document.backend
1144
+ self.attributes['doctype'] = document.doctype
1145
+ self.attributes['backend-'+document.backend] = ''
1146
+ self.attributes['doctype-'+document.doctype] = ''
1147
+ self.attributes[document.backend+'-'+document.doctype] = ''
1148
+ self.attributes['asciidoc-dir'] = APP_DIR
1149
+ self.attributes['user-dir'] = USER_DIR
1150
+ if self.infile != '<stdin>':
1151
+ self.attributes['infile'] = self.infile
1152
+ self.attributes['indir'] = os.path.dirname(self.infile)
1153
+ self.attributes['docdir'] = os.path.dirname(self.infile) #DEPRECATED
1154
+ self.attributes['docname'] = os.path.splitext(
1155
+ os.path.basename(self.infile))[0]
1156
+ if config.verbose:
1157
+ self.attributes['verbose'] = ''
1158
+ # Update with configuration file attributes.
1159
+ self.attributes.update(config.conf_attrs)
1160
+ # Update with command-line attributes.
1161
+ self.attributes.update(config.cmd_attrs)
1162
+ # Extract miscellaneous configuration section entries from attributes.
1163
+ config.load_miscellaneous(config.conf_attrs)
1164
+ config.load_miscellaneous(config.cmd_attrs)
1165
+ self.attributes['newline'] = config.newline # Use raw (unescaped) value.
1166
+ if self.outfile:
1167
+ if self.outfile != '<stdout>':
1168
+ self.attributes['outfile'] = self.outfile
1169
+ self.attributes['outdir'] = os.path.dirname(self.outfile)
1170
+ self.attributes['docname'] = os.path.splitext(
1171
+ os.path.basename(self.outfile))[0]
1172
+ ext = os.path.splitext(self.outfile)[1][1:]
1173
+ elif config.outfilesuffix:
1174
+ ext = config.outfilesuffix[1:]
1175
+ else:
1176
+ ext = ''
1177
+ if ext:
1178
+ self.attributes['filetype'] = ext
1179
+ self.attributes['filetype-'+ext] = ''
1180
+ def translate(self):
1181
+ assert self.doctype in ('article','manpage','book'), \
1182
+ 'illegal document type'
1183
+ assert self.level == 0
1184
+ config.expand_all_templates()
1185
+ # Skip leading comment block.
1186
+ if blocks.isnext() and 'skip' in blocks.current.options:
1187
+ blocks.current.translate()
1188
+ # Skip leading comment lines.
1189
+ while macros.isnext() and macros.current.name == 'comment':
1190
+ macros.current.translate()
1191
+ # Skip leading attribute entries.
1192
+ AttributeEntry.translate_all()
1193
+ # Process document header.
1194
+ has_header = Lex.next() is Title and Title.level == 0
1195
+ if self.doctype == 'manpage' and not has_header:
1196
+ error('manpage document title is mandatory')
1197
+ if has_header:
1198
+ Header.translate()
1199
+ # Command-line entries override header derived entries.
1200
+ self.attributes.update(config.cmd_attrs)
1201
+ if config.header_footer:
1202
+ hdr = config.subs_section('header',{})
1203
+ writer.write(hdr)
1204
+ if self.doctype in ('article','book'):
1205
+ # Translate 'preamble' (untitled elements between header
1206
+ # and first section title).
1207
+ if Lex.next() is not Title:
1208
+ stag,etag = config.section2tags('preamble')
1209
+ writer.write(stag)
1210
+ Section.translate_body()
1211
+ writer.write(etag)
1212
+ else:
1213
+ # Translate manpage SYNOPSIS.
1214
+ if Lex.next() is not Title:
1215
+ error('SYNOPSIS section expected')
1216
+ else:
1217
+ Title.translate()
1218
+ if Title.dict['title'].upper() <> 'SYNOPSIS':
1219
+ error('second section must be named SYNOPSIS')
1220
+ if Title.level != 1:
1221
+ error('SYNOPSIS section title must be at level 1')
1222
+ d = {}
1223
+ d.update(Title.dict)
1224
+ AttributeList.consume(d)
1225
+ stag,etag = config.section2tags('sect-synopsis',d)
1226
+ writer.write(stag)
1227
+ Section.translate_body()
1228
+ writer.write(etag)
1229
+ else:
1230
+ if config.header_footer:
1231
+ hdr = config.subs_section('header',{})
1232
+ writer.write(hdr)
1233
+ if Lex.next() is not Title:
1234
+ Section.translate_body()
1235
+ # Process remaining sections.
1236
+ while not reader.eof():
1237
+ if Lex.next() is not Title:
1238
+ raise EAsciiDoc,'section title expected'
1239
+ Section.translate()
1240
+ Section.setlevel(0) # Write remaining unwritten section close tags.
1241
+ # Substitute document parameters and write document footer.
1242
+ if config.header_footer:
1243
+ ftr = config.subs_section('footer',{})
1244
+ writer.write(ftr)
1245
+ def parse_author(self,s):
1246
+ """ Return False if the author is malformed."""
1247
+ attrs = self.attributes # Alias for readability.
1248
+ s = s.strip()
1249
+ mo = re.match(r'^(?P<name1>[^<>\s]+)'
1250
+ '(\s+(?P<name2>[^<>\s]+))?'
1251
+ '(\s+(?P<name3>[^<>\s]+))?'
1252
+ '(\s+<(?P<email>\S+)>)?$',s)
1253
+ if not mo:
1254
+ error('malformed author: %s' % s)
1255
+ return False
1256
+ firstname = mo.group('name1')
1257
+ if mo.group('name3'):
1258
+ middlename = mo.group('name2')
1259
+ lastname = mo.group('name3')
1260
+ else:
1261
+ middlename = None
1262
+ lastname = mo.group('name2')
1263
+ firstname = firstname.replace('_',' ')
1264
+ if middlename:
1265
+ middlename = middlename.replace('_',' ')
1266
+ if lastname:
1267
+ lastname = lastname.replace('_',' ')
1268
+ email = mo.group('email')
1269
+ if firstname:
1270
+ attrs['firstname'] = firstname
1271
+ if middlename:
1272
+ attrs['middlename'] = middlename
1273
+ if lastname:
1274
+ attrs['lastname'] = lastname
1275
+ if email:
1276
+ attrs['email'] = email
1277
+ return True
1278
+ def process_author_names(self):
1279
+ """ Calculate any missing author related attributes."""
1280
+ attrs = self.attributes # Alias for readability.
1281
+ firstname = attrs.get('firstname','')
1282
+ middlename = attrs.get('middlename','')
1283
+ lastname = attrs.get('lastname','')
1284
+ author = attrs.get('author')
1285
+ initials = attrs.get('authorinitials')
1286
+ if author and not (firstname or middlename or lastname):
1287
+ if not self.parse_author(author):
1288
+ return
1289
+ attrs['author'] = author.replace('_',' ')
1290
+ self.process_author_names()
1291
+ return
1292
+ if not author:
1293
+ author = '%s %s %s' % (firstname, middlename, lastname)
1294
+ author = author.strip()
1295
+ author = re.sub(r'\s+',' ', author)
1296
+ if not initials:
1297
+ initials = firstname[:1] + middlename[:1] + lastname[:1]
1298
+ initials = initials.upper()
1299
+ names = [firstname,middlename,lastname,author,initials]
1300
+ for i,v in enumerate(names):
1301
+ v = config.subs_specialchars(v)
1302
+ v = subs_attrs(v)
1303
+ names[i] = v
1304
+ firstname,middlename,lastname,author,initials = names
1305
+ if firstname:
1306
+ attrs['firstname'] = firstname
1307
+ if middlename:
1308
+ attrs['middlename'] = middlename
1309
+ if lastname:
1310
+ attrs['lastname'] = lastname
1311
+ if author:
1312
+ attrs['author'] = author
1313
+ if initials:
1314
+ attrs['authorinitials'] = initials
1315
+ if author:
1316
+ attrs['authored'] = ''
1317
+
1318
+
1319
+ class Header:
1320
+ """Static methods and attributes only."""
1321
+ def __init__(self):
1322
+ raise AssertionError,'no class instances allowed'
1323
+ def translate():
1324
+ assert Lex.next() is Title and Title.level == 0
1325
+ Title.translate()
1326
+ attrs = document.attributes # Alias for readability.
1327
+ attrs['doctitle'] = Title.dict['title']
1328
+ if document.doctype == 'manpage':
1329
+ # manpage title formatted like mantitle(manvolnum).
1330
+ mo = re.match(r'^(?P<mantitle>.*)\((?P<manvolnum>.*)\)$',
1331
+ attrs['doctitle'])
1332
+ if not mo:
1333
+ error('malformed manpage title')
1334
+ else:
1335
+ mantitle = mo.group('mantitle').strip()
1336
+ # mantitle is lowered only if in ALL CAPS
1337
+ if mantitle == mantitle.upper():
1338
+ mantitle = mantitle.lower()
1339
+ attrs['mantitle'] = mantitle;
1340
+ attrs['manvolnum'] = mo.group('manvolnum').strip()
1341
+ AttributeEntry.translate_all()
1342
+ s = reader.read_next()
1343
+ if s:
1344
+ s = reader.read()
1345
+ document.parse_author(s)
1346
+ AttributeEntry.translate_all()
1347
+ if reader.read_next():
1348
+ # Parse revision line.
1349
+ s = reader.read()
1350
+ s = subs_attrs(s)
1351
+ if s:
1352
+ # Match RCS/CVS/SVN $Id$ marker format.
1353
+ mo = re.match(r'^\$Id: \S+ (?P<revision>\S+)'
1354
+ ' (?P<date>\S+) \S+ \S+ (\S+ )?\$$',s)
1355
+ if not mo:
1356
+ # Match AsciiDoc revision,date format.
1357
+ mo = re.match(r'^\D*(?P<revision>.*?),(?P<date>.+)$',s)
1358
+ if mo:
1359
+ revision = mo.group('revision').strip()
1360
+ date = mo.group('date').strip()
1361
+ else:
1362
+ revision = None
1363
+ date = s.strip()
1364
+ if revision:
1365
+ attrs['revision'] = config.subs_specialchars(revision)
1366
+ if date:
1367
+ attrs['date'] = config.subs_specialchars(date)
1368
+ AttributeEntry.translate_all()
1369
+ if document.doctype == 'manpage':
1370
+ # Translate mandatory NAME section.
1371
+ if Lex.next() is not Title:
1372
+ error('NAME section expected')
1373
+ else:
1374
+ Title.translate()
1375
+ if Title.dict['title'].upper() <> 'NAME':
1376
+ error('first section must be named NAME')
1377
+ if Title.level != 1:
1378
+ error('NAME section title must be at level 1')
1379
+ if not isinstance(Lex.next(),Paragraph):
1380
+ error('malformed NAME section body')
1381
+ lines = reader.read_until(r'^$')
1382
+ s = ' '.join(lines)
1383
+ mo = re.match(r'^(?P<manname>.*?)\s+-\s+(?P<manpurpose>.*)$',s)
1384
+ if not mo:
1385
+ error('malformed NAME section body')
1386
+ attrs['manname'] = mo.group('manname').strip()
1387
+ attrs['manpurpose'] = mo.group('manpurpose').strip()
1388
+ document.process_author_names()
1389
+ if document.backend == 'linuxdoc' and not attrs.has_key('author'):
1390
+ warning('linuxdoc requires author name')
1391
+ translate = staticmethod(translate)
1392
+
1393
+ class AttributeEntry:
1394
+ """Static methods and attributes only."""
1395
+ pattern = None
1396
+ subs = None
1397
+ name = None
1398
+ value = None
1399
+ def __init__(self):
1400
+ raise AssertionError,'no class instances allowed'
1401
+ def isnext():
1402
+ result = False # Assume not next.
1403
+ if not AttributeEntry.pattern:
1404
+ pat = document.attributes.get('attributeentry-pattern')
1405
+ if not pat:
1406
+ error("[attributes] missing 'attributeentry-pattern' entry")
1407
+ AttributeEntry.pattern = pat
1408
+ if not AttributeEntry.subs:
1409
+ subs = document.attributes.get('attributeentry-subs')
1410
+ if subs:
1411
+ subs = parse_options(subs,SUBS_OPTIONS,
1412
+ 'illegal [%s] %s: %s' % ('attributes','attributeentry-subs',subs))
1413
+ else:
1414
+ subs = ('specialcharacters','attributes')
1415
+ AttributeEntry.subs = subs
1416
+ line = reader.read_next()
1417
+ if line:
1418
+ mo = re.match(AttributeEntry.pattern,line)
1419
+ if mo:
1420
+ name = mo.group('attrname').strip()
1421
+ if name[-1] == '!': # Names like name! are None.
1422
+ name = name[:-1]
1423
+ value = None
1424
+ else:
1425
+ value = mo.group('attrvalue').strip()
1426
+ # Strip white space and illegal name chars.
1427
+ name = re.sub(r'(?u)[^\w\-_]', '', name).lower()
1428
+ AttributeEntry.name = name
1429
+ AttributeEntry.value = value
1430
+ result = True
1431
+ return result
1432
+ isnext = staticmethod(isnext)
1433
+ def translate():
1434
+ assert Lex.next() is AttributeEntry
1435
+ attr = AttributeEntry # Alias for brevity.
1436
+ reader.read() # Discard attribute from reader.
1437
+ # Don't override command-line attributes.
1438
+ if config.cmd_attrs.has_key(attr.name):
1439
+ return
1440
+ # Update document.attributes from previously parsed attribute.
1441
+ if attr.value:
1442
+ attr.value = Lex.subs((attr.value,), attr.subs)
1443
+ attr.value = writer.newline.join(attr.value)
1444
+ if attr.value is not None:
1445
+ document.attributes[attr.name] = attr.value
1446
+ elif document.attributes.has_key(attr.name):
1447
+ del document.attributes[attr.name]
1448
+ translate = staticmethod(translate)
1449
+ def translate_all():
1450
+ """ Process all contiguous attribute lines on reader."""
1451
+ while AttributeEntry.isnext():
1452
+ AttributeEntry.translate()
1453
+ translate_all = staticmethod(translate_all)
1454
+
1455
+ class AttributeList:
1456
+ """Static methods and attributes only."""
1457
+ pattern = None
1458
+ match = None
1459
+ attrs = {}
1460
+ def __init__(self):
1461
+ raise AssertionError,'no class instances allowed'
1462
+ def isnext():
1463
+ result = False # Assume not next.
1464
+ if not AttributeList.pattern:
1465
+ if not document.attributes.has_key('attributelist-pattern'):
1466
+ error("[attributes] missing 'attributelist-pattern' entry")
1467
+ AttributeList.pattern = document.attributes['attributelist-pattern']
1468
+ line = reader.read_next()
1469
+ if line:
1470
+ mo = re.match(AttributeList.pattern, line)
1471
+ if mo:
1472
+ AttributeList.match = mo
1473
+ result = True
1474
+ return result
1475
+ isnext = staticmethod(isnext)
1476
+ def translate():
1477
+ assert Lex.next() is AttributeList
1478
+ reader.read() # Discard attribute list from reader.
1479
+ d = AttributeList.match.groupdict()
1480
+ for k,v in d.items():
1481
+ if v is not None:
1482
+ if k == 'attrlist':
1483
+ v = subs_attrs(v)
1484
+ if v:
1485
+ parse_attributes(v, AttributeList.attrs)
1486
+ else:
1487
+ AttributeList.attrs[k] = v
1488
+ translate = staticmethod(translate)
1489
+ def consume(d):
1490
+ """Add attribute list to the dictionary 'd' and reset the
1491
+ list."""
1492
+ if AttributeList.attrs:
1493
+ d.update(AttributeList.attrs)
1494
+ AttributeList.attrs = {}
1495
+ consume = staticmethod(consume)
1496
+
1497
+ class BlockTitle:
1498
+ """Static methods and attributes only."""
1499
+ title = None
1500
+ pattern = None
1501
+ def __init__(self):
1502
+ raise AssertionError,'no class instances allowed'
1503
+ def isnext():
1504
+ result = False # Assume not next.
1505
+ line = reader.read_next()
1506
+ if line:
1507
+ mo = re.match(BlockTitle.pattern,line)
1508
+ if mo:
1509
+ BlockTitle.title = mo.group('title')
1510
+ result = True
1511
+ return result
1512
+ isnext = staticmethod(isnext)
1513
+ def translate():
1514
+ assert Lex.next() is BlockTitle
1515
+ reader.read() # Discard title from reader.
1516
+ # Perform title substitutions.
1517
+ if not Title.subs:
1518
+ Title.subs = config.subsnormal
1519
+ s = Lex.subs((BlockTitle.title,), Title.subs)
1520
+ s = writer.newline.join(s)
1521
+ if not s:
1522
+ warning('blank block title')
1523
+ BlockTitle.title = s
1524
+ translate = staticmethod(translate)
1525
+ def consume(d):
1526
+ """If there is a title add it to dictionary 'd' then reset title."""
1527
+ if BlockTitle.title:
1528
+ d['title'] = BlockTitle.title
1529
+ BlockTitle.title = None
1530
+ consume = staticmethod(consume)
1531
+
1532
+ class Title:
1533
+ """Processes Header and Section titles. Static methods and attributes
1534
+ only."""
1535
+ # Class variables
1536
+ underlines = ('==','--','~~','^^','++') # Levels 0,1,2,3,4.
1537
+ subs = ()
1538
+ pattern = None
1539
+ level = 0
1540
+ dict = {}
1541
+ sectname = None
1542
+ section_numbers = [0]*len(underlines)
1543
+ dump_dict = {}
1544
+ linecount = None # Number of lines in title (1 or 2).
1545
+ def __init__(self):
1546
+ raise AssertionError,'no class instances allowed'
1547
+ def translate():
1548
+ """Parse the Title.dict and Title.level from the reader. The
1549
+ real work has already been done by parse()."""
1550
+ assert Lex.next() is Title
1551
+ # Discard title from reader.
1552
+ for i in range(Title.linecount):
1553
+ reader.read()
1554
+ Title.setsectname()
1555
+ # Perform title substitutions.
1556
+ if not Title.subs:
1557
+ Title.subs = config.subsnormal
1558
+ s = Lex.subs((Title.dict['title'],), Title.subs)
1559
+ s = writer.newline.join(s)
1560
+ if not s:
1561
+ warning('blank section title')
1562
+ Title.dict['title'] = s
1563
+ translate = staticmethod(translate)
1564
+ def isnext():
1565
+ lines = reader.read_ahead(2)
1566
+ return Title.parse(lines)
1567
+ isnext = staticmethod(isnext)
1568
+ def parse(lines):
1569
+ """Parse title at start of lines tuple."""
1570
+ if len(lines) == 0: return False
1571
+ if len(lines[0]) == 0: return False # Title can't be blank.
1572
+ # Check for single-line titles.
1573
+ result = False
1574
+ for level in range(len(Title.underlines)):
1575
+ k = 'sect%s' % level
1576
+ if Title.dump_dict.has_key(k):
1577
+ mo = re.match(Title.dump_dict[k], lines[0])
1578
+ if mo:
1579
+ Title.dict = mo.groupdict()
1580
+ Title.level = level
1581
+ Title.linecount = 1
1582
+ result = True
1583
+ break
1584
+ if not result:
1585
+ # Check for double-line titles.
1586
+ if not Title.pattern: return False # Single-line titles only.
1587
+ if len(lines) < 2: return False
1588
+ title,ul = lines[:2]
1589
+ title_len = char_len(title)
1590
+ ul_len = char_len(ul)
1591
+ if ul_len < 2: return False
1592
+ # Fast elimination check.
1593
+ if ul[:2] not in Title.underlines: return False
1594
+ # Length of underline must be within +-3 of title.
1595
+ if not (ul_len-3 < title_len < ul_len+3): return False
1596
+ # Check for valid repetition of underline character pairs.
1597
+ s = ul[:2]*((ul_len+1)/2)
1598
+ if ul != s[:ul_len]: return False
1599
+ # Don't be fooled by back-to-back delimited blocks, require at
1600
+ # least one alphanumeric character in title.
1601
+ if not re.search(r'(?u)\w',title): return False
1602
+ mo = re.match(Title.pattern, title)
1603
+ if mo:
1604
+ Title.dict = mo.groupdict()
1605
+ Title.level = list(Title.underlines).index(ul[:2])
1606
+ Title.linecount = 2
1607
+ result = True
1608
+ # Check for expected pattern match groups.
1609
+ if result:
1610
+ if not Title.dict.has_key('title'):
1611
+ warning('[titles] entry has no <title> group')
1612
+ Title.dict['title'] = lines[0]
1613
+ for k,v in Title.dict.items():
1614
+ if v is None: del Title.dict[k]
1615
+ return result
1616
+ parse = staticmethod(parse)
1617
+ def load(dict):
1618
+ """Load and validate [titles] section entries from dict."""
1619
+ if dict.has_key('underlines'):
1620
+ errmsg = 'malformed [titles] underlines entry'
1621
+ try:
1622
+ underlines = parse_list(dict['underlines'])
1623
+ except:
1624
+ raise EAsciiDoc,errmsg
1625
+ if len(underlines) != len(Title.underlines):
1626
+ raise EAsciiDoc,errmsg
1627
+ for s in underlines:
1628
+ if len(s) !=2:
1629
+ raise EAsciiDoc,errmsg
1630
+ Title.underlines = tuple(underlines)
1631
+ Title.dump_dict['underlines'] = dict['underlines']
1632
+ if dict.has_key('subs'):
1633
+ Title.subs = parse_options(dict['subs'], SUBS_OPTIONS,
1634
+ 'illegal [titles] subs entry')
1635
+ Title.dump_dict['subs'] = dict['subs']
1636
+ if dict.has_key('sectiontitle'):
1637
+ pat = dict['sectiontitle']
1638
+ if not pat or not is_regexp(pat):
1639
+ raise EAsciiDoc,'malformed [titles] sectiontitle entry'
1640
+ Title.pattern = pat
1641
+ Title.dump_dict['sectiontitle'] = pat
1642
+ if dict.has_key('blocktitle'):
1643
+ pat = dict['blocktitle']
1644
+ if not pat or not is_regexp(pat):
1645
+ raise EAsciiDoc,'malformed [titles] blocktitle entry'
1646
+ BlockTitle.pattern = pat
1647
+ Title.dump_dict['blocktitle'] = pat
1648
+ # Load single-line title patterns.
1649
+ for k in ('sect0','sect1','sect2','sect3','sect4'):
1650
+ if dict.has_key(k):
1651
+ pat = dict[k]
1652
+ if not pat or not is_regexp(pat):
1653
+ raise EAsciiDoc,'malformed [titles] %s entry' % k
1654
+ Title.dump_dict[k] = pat
1655
+ # TODO: Check we have either a Title.pattern or at least one
1656
+ # single-line title pattern -- can this be done here or do we need
1657
+ # check routine like the other block checkers?
1658
+ load = staticmethod(load)
1659
+ def dump():
1660
+ dump_section('titles',Title.dump_dict)
1661
+ dump = staticmethod(dump)
1662
+ def setsectname():
1663
+ """Set Title section name. First search for section title in
1664
+ [specialsections], if not found use default 'sect<level>' name."""
1665
+ for pat,sect in config.specialsections.items():
1666
+ mo = re.match(pat,Title.dict['title'])
1667
+ if mo:
1668
+ title = mo.groupdict().get('title')
1669
+ if title is not None:
1670
+ Title.dict['title'] = title.strip()
1671
+ else:
1672
+ Title.dict['title'] = mo.group().strip()
1673
+ Title.sectname = sect
1674
+ break
1675
+ else:
1676
+ Title.sectname = 'sect%d' % Title.level
1677
+ setsectname = staticmethod(setsectname)
1678
+ def getnumber(level):
1679
+ """Return next section number at section 'level' formatted like
1680
+ 1.2.3.4."""
1681
+ number = ''
1682
+ for l in range(len(Title.section_numbers)):
1683
+ n = Title.section_numbers[l]
1684
+ if l == 0:
1685
+ continue
1686
+ elif l < level:
1687
+ number = '%s%d.' % (number, n)
1688
+ elif l == level:
1689
+ number = '%s%d.' % (number, n + 1)
1690
+ Title.section_numbers[l] = n + 1
1691
+ elif l > level:
1692
+ # Reset unprocessed section levels.
1693
+ Title.section_numbers[l] = 0
1694
+ return number
1695
+ getnumber = staticmethod(getnumber)
1696
+
1697
+
1698
+ class Section:
1699
+ """Static methods and attributes only."""
1700
+ endtags = [] # Stack of currently open section (level,endtag) tuples.
1701
+ ids = [] # List of already used ids.
1702
+ def __init__(self):
1703
+ raise AssertionError,'no class instances allowed'
1704
+ def savetag(level,etag):
1705
+ """Save section end."""
1706
+ Section.endtags.append((level,etag))
1707
+ savetag = staticmethod(savetag)
1708
+ def setlevel(level):
1709
+ """Set document level and write open section close tags up to level."""
1710
+ while Section.endtags and Section.endtags[-1][0] >= level:
1711
+ writer.write(Section.endtags.pop()[1])
1712
+ document.level = level
1713
+ setlevel = staticmethod(setlevel)
1714
+ def gen_id(title):
1715
+ """
1716
+ The normalized value of the id attribute is an NCName according to
1717
+ the 'Namespaces in XML' Recommendation:
1718
+ NCName ::= NCNameStartChar NCNameChar*
1719
+ NCNameChar ::= NameChar - ':'
1720
+ NCNameStartChar ::= Letter | '_'
1721
+ NameChar ::= Letter | Digit | '.' | '-' | '_' | ':'
1722
+ """
1723
+ base_ident = re.sub(r'[^a-zA-Z0-9]+', '_', title).strip('_').lower()
1724
+ # Prefix with underscore to ensure a valid id start character and to
1725
+ # ensure the id does not clash with existing document id's.
1726
+ base_ident = '_' + base_ident
1727
+ i = 1
1728
+ while True:
1729
+ if i == 1:
1730
+ ident = base_ident
1731
+ else:
1732
+ ident = '%s_%d' % (base_ident, i)
1733
+ if ident not in Section.ids:
1734
+ Section.ids.append(ident)
1735
+ return ident
1736
+ else:
1737
+ ident = base_ident
1738
+ i += 1
1739
+ gen_id = staticmethod(gen_id)
1740
+ def translate():
1741
+ assert Lex.next() is Title
1742
+ prev_sectname = Title.sectname
1743
+ Title.translate()
1744
+ if Title.level == 0 and document.doctype != 'book':
1745
+ error('only book doctypes can contain level 0 sections')
1746
+ if Title.level > document.level \
1747
+ and document.backend == 'docbook' \
1748
+ and prev_sectname in ('sect-colophon','sect-abstract', \
1749
+ 'sect-dedication','sect-glossary','sect-bibliography'):
1750
+ error('%s section cannot contain sub-sections' % prev_sectname)
1751
+ if Title.level > document.level+1:
1752
+ # Sub-sections of multi-part book level zero Preface and Appendices
1753
+ # are meant to be out of sequence.
1754
+ if document.doctype == 'book' \
1755
+ and document.level == 0 \
1756
+ and Title.level == 2 \
1757
+ and prev_sectname in ('sect-preface','sect-appendix'):
1758
+ pass
1759
+ else:
1760
+ warning('section title out of sequence: '
1761
+ 'expected level %d, got level %d'
1762
+ % (document.level+1, Title.level))
1763
+ if not document.attributes.get('sectids') is None \
1764
+ and 'id' not in AttributeList.attrs:
1765
+ # Generate ids for sections.
1766
+ AttributeList.attrs['id'] = Section.gen_id(Title.dict['title'])
1767
+ Section.setlevel(Title.level)
1768
+ Title.dict['sectnum'] = Title.getnumber(document.level)
1769
+ AttributeList.consume(Title.dict)
1770
+ stag,etag = config.section2tags(Title.sectname,Title.dict)
1771
+ Section.savetag(Title.level,etag)
1772
+ writer.write(stag)
1773
+ Section.translate_body()
1774
+ translate = staticmethod(translate)
1775
+ def translate_body(terminator=Title):
1776
+ isempty = True
1777
+ next = Lex.next()
1778
+ while next and next is not terminator:
1779
+ if next is Title and isinstance(terminator,DelimitedBlock):
1780
+ error('title not permitted in sidebar body')
1781
+ if document.backend == 'linuxdoc' \
1782
+ and document.level == 0 \
1783
+ and not isinstance(next,Paragraph):
1784
+ warning('only paragraphs are permitted in linuxdoc synopsis')
1785
+ next.translate()
1786
+ next = Lex.next()
1787
+ isempty = False
1788
+ # The section is not empty if contains a subsection.
1789
+ if next and isempty and Title.level > document.level:
1790
+ isempty = False
1791
+ # Report empty sections if invalid markup will result.
1792
+ if isempty:
1793
+ if document.backend == 'docbook' and Title.sectname != 'sect-index':
1794
+ error('empty section is not valid')
1795
+ translate_body = staticmethod(translate_body)
1796
+
1797
+ class AbstractBlock:
1798
+ def __init__(self):
1799
+ self.OPTIONS = () # The set of allowed options values
1800
+ # Configuration parameter names common to all blocks.
1801
+ self.CONF_ENTRIES = ('options','subs','presubs','postsubs',
1802
+ 'posattrs','style','.*-style')
1803
+ # Configuration parameters.
1804
+ self.name=None # Configuration file section name.
1805
+ self.delimiter=None # Regular expression matching block delimiter.
1806
+ self.template=None # template section entry.
1807
+ self.options=() # options entry list.
1808
+ self.presubs=None # presubs/subs entry list.
1809
+ self.postsubs=() # postsubs entry list.
1810
+ self.filter=None # filter entry.
1811
+ self.posattrs=() # posattrs entry list.
1812
+ self.style=None # Default style.
1813
+ self.styles=OrderedDict() # Styles dictionary.
1814
+ # Before a block is processed it's attributes (from it's
1815
+ # attributes list) are merged with the block configuration parameters
1816
+ # (by self.process_attributes()) resulting in the template substitution
1817
+ # dictionary (self.attributes) and the block's procssing parameters
1818
+ # (self.parameters).
1819
+ self.attributes={}
1820
+ # The names of block parameters.
1821
+ self.PARAM_NAMES=('template','options','presubs','postsubs','filter')
1822
+ self.parameters={}
1823
+ # Leading delimiter match object.
1824
+ self.mo=None
1825
+ def is_conf_entry(self,param):
1826
+ """Return True if param matches an allowed configuration file entry
1827
+ name."""
1828
+ for s in self.CONF_ENTRIES:
1829
+ if re.match('^'+s+'$',param):
1830
+ return True
1831
+ return False
1832
+ def load(self,name,entries):
1833
+ """Update block definition from section 'entries' dictionary."""
1834
+ for k in entries.keys():
1835
+ if not self.is_conf_entry(k):
1836
+ raise EAsciiDoc,'illegal [%s] entry name: %s' % (name,k)
1837
+ self.name = name
1838
+ for k,v in entries.items():
1839
+ if not is_name(k):
1840
+ raise EAsciiDoc, \
1841
+ 'malformed [%s] entry name: %s' % (name,k)
1842
+ if k == 'delimiter':
1843
+ if v and is_regexp(v):
1844
+ self.delimiter = v
1845
+ else:
1846
+ raise EAsciiDoc,'malformed [%s] regexp: %s' % (name,v)
1847
+ elif k == 'template':
1848
+ if not is_name(v):
1849
+ raise EAsciiDoc, \
1850
+ 'malformed [%s] template name: %s' % (name,v)
1851
+ self.template = v
1852
+ elif k == 'style':
1853
+ if not is_name(v):
1854
+ raise EAsciiDoc, \
1855
+ 'malformed [%s] style name: %s' % (name,v)
1856
+ self.style = v
1857
+ elif k == 'posattrs':
1858
+ self.posattrs = parse_options(v, (),
1859
+ 'illegal [%s] %s: %s' % (name,k,v))
1860
+ elif k == 'options':
1861
+ self.options = parse_options(v,self.OPTIONS,
1862
+ 'illegal [%s] %s: %s' % (name,k,v))
1863
+ elif k == 'presubs' or k == 'subs':
1864
+ self.presubs = parse_options(v,SUBS_OPTIONS,
1865
+ 'illegal [%s] %s: %s' % (name,k,v))
1866
+ elif k == 'postsubs':
1867
+ self.postsubs = parse_options(v,SUBS_OPTIONS,
1868
+ 'illegal [%s] %s: %s' % (name,k,v))
1869
+ elif k == 'filter':
1870
+ self.filter = v
1871
+ else:
1872
+ mo = re.match(r'^(?P<style>.*)-style$',k)
1873
+ if mo:
1874
+ if not v:
1875
+ raise EAsciiDoc, 'empty [%s] style: %s' % (name,k)
1876
+ style = mo.group('style')
1877
+ d = {}
1878
+ if not parse_named_attributes(v,d):
1879
+ raise EAsciiDoc,'malformed [%s] style: %s' % (name,v)
1880
+ self.styles[style] = d
1881
+ def dump(self):
1882
+ """Write block definition to stdout."""
1883
+ write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
1884
+ write('['+self.name+']')
1885
+ if self.is_conf_entry('delimiter'):
1886
+ write('delimiter='+self.delimiter)
1887
+ if self.template:
1888
+ write('template='+self.template)
1889
+ if self.options:
1890
+ write('options='+','.join(self.options))
1891
+ if self.presubs:
1892
+ if self.postsubs:
1893
+ write('presubs='+','.join(self.presubs))
1894
+ else:
1895
+ write('subs='+','.join(self.presubs))
1896
+ if self.postsubs:
1897
+ write('postsubs='+','.join(self.postsubs))
1898
+ if self.filter:
1899
+ write('filter='+self.filter)
1900
+ if self.posattrs:
1901
+ write('posattrs='+','.join(self.posattrs))
1902
+ if self.style:
1903
+ write('style='+self.style)
1904
+ if self.styles:
1905
+ for style,d in self.styles.items():
1906
+ s = ''
1907
+ for k,v in d.items(): s += '%s=%r,' % (k,v)
1908
+ write('%s-style=%s' % (style,s[:-1]))
1909
+ def validate(self):
1910
+ """Validate block after the complete configuration has been loaded."""
1911
+ if self.is_conf_entry('delimiter') and not self.delimiter:
1912
+ raise EAsciiDoc,'[%s] missing delimiter' % self.name
1913
+ if self.style:
1914
+ if not self.styles.has_key(self.style):
1915
+ warning(' missing [%s] %s-style entry' % (self.name,self.style))
1916
+ # Check all styles for missing templates.
1917
+ all_styles_have_template = True
1918
+ for k,v in self.styles.items():
1919
+ t = v.get('template')
1920
+ if t and not config.sections.has_key(t):
1921
+ warning('[%s] missing template section' % t)
1922
+ if not t:
1923
+ all_styles_have_template = False
1924
+ # Check we have a valid template entry or alternatively that all the
1925
+ # styles have templates.
1926
+ if self.is_conf_entry('template') and not 'skip' in self.options:
1927
+ if self.template:
1928
+ if not config.sections.has_key(self.template):
1929
+ warning('[%s] missing template section' % self.template)
1930
+ elif not all_styles_have_template:
1931
+ warning('[%s] styles missing templates' % self.name)
1932
+ def isnext(self):
1933
+ """Check if this block is next in document reader."""
1934
+ result = False
1935
+ reader.skip_blank_lines()
1936
+ if reader.read_next():
1937
+ mo = re.match(self.delimiter,reader.read_next())
1938
+ if mo:
1939
+ self.mo = mo
1940
+ result = True
1941
+ return result
1942
+ def translate(self):
1943
+ """Translate block from document reader."""
1944
+ if not self.presubs:
1945
+ self.presubs = config.subsnormal
1946
+ def update_params(self,src,dst):
1947
+ """Copy block processing parameters from src to dst dictionaries."""
1948
+ for k,v in src.items():
1949
+ if k in ('template','filter'):
1950
+ dst[k] = v
1951
+ elif k == 'options':
1952
+ dst[k] = parse_options(v,self.OPTIONS,
1953
+ 'illegal [%s] %s: %s' % (self.name,k,v))
1954
+ elif k in ('subs','presubs','postsubs'):
1955
+ subs = parse_options(v,SUBS_OPTIONS,
1956
+ 'illegal [%s] %s: %s' % (self.name,k,v))
1957
+ if k == 'subs':
1958
+ dst['presubs'] = subs
1959
+ else:
1960
+ dst[k] = subs
1961
+ def merge_attributes(self,attrs):
1962
+ """Merge block attributes 'attrs' dictionary with the block
1963
+ configuration parameters setting self.attributes (template substitution
1964
+ attributes) and self.parameters (block processing parameters)."""
1965
+ self.attributes = {}
1966
+ self.attributes.update(attrs)
1967
+ # Calculate dynamic block parameters.
1968
+ # Start with configuration file defaults.
1969
+ self.parameters['template'] = self.template
1970
+ self.parameters['options'] = self.options
1971
+ self.parameters['presubs'] = self.presubs
1972
+ self.parameters['postsubs'] = self.postsubs
1973
+ self.parameters['filter'] = self.filter
1974
+ # Load the selected style attributes.
1975
+ posattrs = self.posattrs
1976
+ if posattrs and posattrs[0] == 'style':
1977
+ style = self.attributes.get('1')
1978
+ else:
1979
+ style = None
1980
+ if not style:
1981
+ style = self.attributes.get('style',self.style)
1982
+ if style is not None:
1983
+ if not self.styles.has_key(style):
1984
+ warning('missing [%s] %s-style entry' % (self.name,style))
1985
+ else:
1986
+ self.attributes['style'] = style
1987
+ for k,v in self.styles[style].items():
1988
+ if k == 'posattrs':
1989
+ posattrs = v
1990
+ elif k in self.PARAM_NAMES:
1991
+ self.parameters[k] = v
1992
+ elif not self.attributes.has_key(k):
1993
+ # Style attributes don't take precedence over explicit.
1994
+ self.attributes[k] = v
1995
+ # Set named positional attributes.
1996
+ for i,v in enumerate(posattrs):
1997
+ if self.attributes.has_key(str(i+1)):
1998
+ self.attributes[v] = self.attributes[str(i+1)]
1999
+ # Override config and style attributes with document attributes.
2000
+ self.update_params(self.attributes,self.parameters)
2001
+ assert isinstance(self.parameters['options'],tuple)
2002
+ assert isinstance(self.parameters['presubs'],tuple)
2003
+ assert isinstance(self.parameters['postsubs'],tuple)
2004
+ def get_options(self):
2005
+ return self.parameters['options']
2006
+ def get_subs(self):
2007
+ return (self.parameters['presubs'], self.parameters['postsubs'])
2008
+ def get_template(self):
2009
+ return self.parameters['template']
2010
+ def get_filter(self):
2011
+ return self.parameters['filter']
2012
+
2013
+ class AbstractBlocks:
2014
+ """List of block definitions."""
2015
+ PREFIX = '' # Conf file section name prefix set in derived classes.
2016
+ BLOCK_TYPE = None # Block type set in derived classes.
2017
+ def __init__(self):
2018
+ self.current=None
2019
+ self.blocks = [] # List of Block objects.
2020
+ self.default = None # Default Block.
2021
+ self.delimiter = None # Combined tables delimiter regular expression.
2022
+ def load(self,sections):
2023
+ """Load block definition from 'sections' dictionary."""
2024
+ for k in sections.keys():
2025
+ if re.match(r'^'+ self.PREFIX + r'.+$',k):
2026
+ d = {}
2027
+ parse_entries(sections.get(k,()),d)
2028
+ for b in self.blocks:
2029
+ if b.name == k:
2030
+ break
2031
+ else:
2032
+ b = self.BLOCK_TYPE()
2033
+ self.blocks.append(b)
2034
+ try:
2035
+ b.load(k,d)
2036
+ except EAsciiDoc,e:
2037
+ raise EAsciiDoc,'[%s] %s' % (k,str(e))
2038
+ def dump(self):
2039
+ for b in self.blocks:
2040
+ b.dump()
2041
+ def isnext(self):
2042
+ for b in self.blocks:
2043
+ if b.isnext():
2044
+ self.current = b
2045
+ return True;
2046
+ return False
2047
+ def validate(self):
2048
+ """Validate the block definitions."""
2049
+ # Validate delimiters and build combined lists delimiter pattern.
2050
+ delimiters = []
2051
+ for b in self.blocks:
2052
+ assert b.__class__ is self.BLOCK_TYPE
2053
+ b.validate()
2054
+ if b.delimiter:
2055
+ delimiters.append(b.delimiter)
2056
+ self.delimiter = join_regexp(delimiters)
2057
+
2058
+ class Paragraph(AbstractBlock):
2059
+ def __init__(self):
2060
+ AbstractBlock.__init__(self)
2061
+ self.CONF_ENTRIES += ('delimiter','template','filter')
2062
+ self.OPTIONS = ('listelement',)
2063
+ self.text=None # Text in first line of paragraph.
2064
+ def load(self,name,entries):
2065
+ AbstractBlock.load(self,name,entries)
2066
+ def dump(self):
2067
+ AbstractBlock.dump(self)
2068
+ write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2069
+ write('')
2070
+ def isnext(self):
2071
+ result = AbstractBlock.isnext(self)
2072
+ if result:
2073
+ self.text = self.mo.groupdict().get('text')
2074
+ return result
2075
+ def translate(self):
2076
+ AbstractBlock.translate(self)
2077
+ attrs = {}
2078
+ attrs.update(self.mo.groupdict())
2079
+ BlockTitle.consume(attrs)
2080
+ AttributeList.consume(attrs)
2081
+ self.merge_attributes(attrs)
2082
+ reader.read() # Discard (already parsed item first line).
2083
+ body = reader.read_until(r'^\+$|^$|'+blocks.delimiter+r'|'+tables.delimiter)
2084
+ body = [self.text] + list(body)
2085
+ presubs,postsubs = self.get_subs()
2086
+ # Don't join verbatim paragraphs.
2087
+ if 'verbatim' not in (presubs + postsubs):
2088
+ body = join_lines(body)
2089
+ body = Lex.set_margin(body) # Move body to left margin.
2090
+ body = Lex.subs(body,presubs)
2091
+ if self.get_filter():
2092
+ body = filter_lines(self.get_filter(),body,self.attributes)
2093
+ body = Lex.subs(body,postsubs)
2094
+ template = self.get_template()
2095
+ stag,etag = config.section2tags(template, self.attributes)
2096
+ # Write start tag, content, end tag.
2097
+ writer.write(dovetail_tags(stag,body,etag))
2098
+
2099
+ class Paragraphs(AbstractBlocks):
2100
+ """List of paragraph definitions."""
2101
+ BLOCK_TYPE = Paragraph
2102
+ PREFIX = 'paradef-'
2103
+ def __init__(self):
2104
+ AbstractBlocks.__init__(self)
2105
+ def load(self,sections):
2106
+ AbstractBlocks.load(self,sections)
2107
+ def validate(self):
2108
+ AbstractBlocks.validate(self)
2109
+ # Check we have a default paragraph definition, put it last in list.
2110
+ for b in self.blocks:
2111
+ if b.name == 'paradef-default':
2112
+ self.blocks.append(b)
2113
+ self.default = b
2114
+ self.blocks.remove(b)
2115
+ break
2116
+ else:
2117
+ raise EAsciiDoc,'missing [paradef-default] section'
2118
+
2119
+ class List(AbstractBlock):
2120
+ TAGS = ('listtag','itemtag','texttag','entrytag','labeltag')
2121
+ TYPES = ('bulleted','numbered','labeled','callout')
2122
+ def __init__(self):
2123
+ AbstractBlock.__init__(self)
2124
+ self.CONF_ENTRIES += ('delimiter','type') + self.TAGS
2125
+ self.listtag=None
2126
+ self.itemtag=None
2127
+ self.texttag=None # Tag for list item text.
2128
+ self.labeltag=None # Variable lists only.
2129
+ self.entrytag=None # Variable lists only.
2130
+ self.label=None # List item label (labeled lists).
2131
+ self.text=None # Text in first line of list item.
2132
+ self.index=None # Matched delimiter 'index' group (numbered lists).
2133
+ self.type=None # List type.
2134
+ self.listindex=None # Current list index (1..)
2135
+ def load(self,name,entries):
2136
+ AbstractBlock.load(self,name,entries)
2137
+ for k,v in entries.items():
2138
+ if k == 'type':
2139
+ if v in self.TYPES:
2140
+ self.type = v
2141
+ else:
2142
+ raise EAsciiDoc,'illegal list type: %s' % v
2143
+ elif k in self.TAGS:
2144
+ if is_name(v):
2145
+ setattr(self,k,v)
2146
+ else:
2147
+ raise EAsciiDoc,'illegal list %s name: %s' % (k,v)
2148
+ def dump(self):
2149
+ AbstractBlock.dump(self)
2150
+ write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2151
+ write('type='+self.type)
2152
+ write('listtag='+self.listtag)
2153
+ write('itemtag='+self.itemtag)
2154
+ write('texttag='+self.texttag)
2155
+ if self.type == 'labeled':
2156
+ write('entrytag='+self.entrytag)
2157
+ write('labeltag='+self.labeltag)
2158
+ write('')
2159
+ def isnext(self):
2160
+ result = AbstractBlock.isnext(self)
2161
+ if result:
2162
+ self.label = self.mo.groupdict().get('label')
2163
+ self.text = self.mo.groupdict().get('text')
2164
+ self.index = self.mo.groupdict().get('index')
2165
+ return result
2166
+ def translate_entry(self):
2167
+ assert self.type == 'labeled'
2168
+ stag,etag = config.tag(self.entrytag, self.attributes)
2169
+ if stag:
2170
+ writer.write(stag)
2171
+ if self.listtag == 'hlist':
2172
+ # Horizontal label list.
2173
+ reader.read() # Discard (already parsed item first line).
2174
+ writer.write_tag(self.labeltag, [self.label],
2175
+ self.presubs, self.attributes)
2176
+ else:
2177
+ # Write multiple labels (vertical label list).
2178
+ while Lex.next() is self:
2179
+ reader.read() # Discard (already parsed item first line).
2180
+ writer.write_tag(self.labeltag, [self.label],
2181
+ self.presubs, self.attributes)
2182
+ # Write item text.
2183
+ self.translate_item()
2184
+ if etag:
2185
+ writer.write(etag)
2186
+ def iscontinued(self):
2187
+ if reader.read_next() == '+':
2188
+ reader.read() # Discard.
2189
+ # Allow attribute list to precede continued list item element.
2190
+ while Lex.next() is AttributeList:
2191
+ Lex.next().translate()
2192
+ return True
2193
+ else:
2194
+ return False
2195
+ def translate_item(self):
2196
+ if lists.listblock:
2197
+ self.translate_item_2()
2198
+ else:
2199
+ self.translate_item_1()
2200
+ def translate_item_1(self):
2201
+ """Translation for '+' style list continuation."""
2202
+ if self.type == 'callout':
2203
+ self.attributes['coids'] = calloutmap.calloutids(self.listindex)
2204
+ stag,etag = config.tag(self.itemtag, self.attributes)
2205
+ if stag:
2206
+ writer.write(stag)
2207
+ if self.text and self.text == '+':
2208
+ # Pathalogical case: continued Horizontal Labeled List with no
2209
+ # item text.
2210
+ continued = True
2211
+ elif not self.text and self.iscontinued():
2212
+ # Pathalogical case: continued Vertical Labeled List with no
2213
+ # item text.
2214
+ continued = True
2215
+ else:
2216
+ # Write ItemText.
2217
+ text = reader.read_until(lists.delimiter + r'|^\+$|^$|' +
2218
+ blocks.delimiter + r'|' + tables.delimiter)
2219
+ if self.text is not None:
2220
+ text = [self.text] + list(text)
2221
+ text = join_lines(text)
2222
+ if text:
2223
+ writer.write_tag(self.texttag, text, self.presubs, self.attributes)
2224
+ continued = self.iscontinued()
2225
+ while True:
2226
+ next = Lex.next()
2227
+ if next in lists.open:
2228
+ break
2229
+ elif isinstance(next,List):
2230
+ next.translate()
2231
+ elif isinstance(next,Paragraph) and 'listelement' in next.options:
2232
+ next.translate()
2233
+ elif continued:
2234
+ if next is Title or next is BlockTitle:
2235
+ error('title not allowed in list item continuation')
2236
+ next.translate()
2237
+ else:
2238
+ break
2239
+ continued = self.iscontinued()
2240
+ if etag:
2241
+ writer.write(etag)
2242
+ def translate_item_2(self):
2243
+ """Translation for List block style lists."""
2244
+ if self.type == 'callout':
2245
+ self.attributes['coids'] = calloutmap.calloutids(self.listindex)
2246
+ stag,etag = config.tag(self.itemtag, self.attributes)
2247
+ if stag:
2248
+ writer.write(stag)
2249
+ if self.text or reader.read_next():
2250
+ # Write ItemText.
2251
+ text = reader.read_until(lists.delimiter + r'|^$|' +
2252
+ blocks.delimiter + r'|' + tables.delimiter)
2253
+ if self.text is not None:
2254
+ text = [self.text] + list(text)
2255
+ text = join_lines(text)
2256
+ writer.write_tag(self.texttag, text, self.presubs, self.attributes)
2257
+ while True:
2258
+ next = Lex.next()
2259
+ if next in lists.open:
2260
+ break
2261
+ elif next is lists.listblock:
2262
+ break
2263
+ elif isinstance(next,List):
2264
+ next.translate()
2265
+ elif isinstance(next,Paragraph) and 'listelement' in next.options:
2266
+ next.translate()
2267
+ elif lists.listblock:
2268
+ if next is Title or next is BlockTitle:
2269
+ error('title not allowed in list item continuation')
2270
+ next.translate()
2271
+ else:
2272
+ break
2273
+ if etag:
2274
+ writer.write(etag)
2275
+ def check_index(self):
2276
+ """ Check calculated listindex (1,2,...) against the item index in the
2277
+ document (self.index)."""
2278
+ assert self.type in ('numbered','callout')
2279
+ if self.index:
2280
+ matched = False
2281
+ if re.match(r'\d+', self.index):
2282
+ i = int(self.index)
2283
+ matched = True
2284
+ elif re.match(r'[a-z]', self.index):
2285
+ i = ord(self.index) - ord('a') + 1
2286
+ matched = True
2287
+ if matched and i != self.listindex:
2288
+ print 'type: ',self.type,': expected ',self.listindex,' got ',i
2289
+ warning('list item %s out of sequence' % self.index)
2290
+ def translate(self):
2291
+ AbstractBlock.translate(self)
2292
+ lists.open.append(self)
2293
+ attrs = {}
2294
+ attrs.update(self.mo.groupdict())
2295
+ BlockTitle.consume(attrs)
2296
+ AttributeList.consume(attrs)
2297
+ self.merge_attributes(attrs)
2298
+ stag,etag = config.tag(self.listtag, self.attributes)
2299
+ if stag:
2300
+ writer.write(stag)
2301
+ self.listindex = 0
2302
+ while Lex.next() is self:
2303
+ self.listindex += 1
2304
+ document.attributes['listindex'] = str(self.listindex)
2305
+ if self.type in ('numbered','callout'):
2306
+ self.check_index()
2307
+ if self.type in ('bulleted','numbered','callout'):
2308
+ reader.read() # Discard (already parsed item first line).
2309
+ self.translate_item()
2310
+ elif self.type == 'labeled':
2311
+ self.translate_entry()
2312
+ else:
2313
+ raise AssertionError,'illegal [%s] list type' % self.name
2314
+ if etag:
2315
+ writer.write(etag)
2316
+ if self.type == 'callout':
2317
+ calloutmap.validate(self.listindex)
2318
+ calloutmap.listclose()
2319
+ lists.open.pop()
2320
+ if len(lists.open):
2321
+ document.attributes['listindex'] = str(lists.open[-1].listindex)
2322
+
2323
+ class Lists(AbstractBlocks):
2324
+ """List of List objects."""
2325
+ BLOCK_TYPE = List
2326
+ PREFIX = 'listdef-'
2327
+ def __init__(self):
2328
+ AbstractBlocks.__init__(self)
2329
+ self.open = [] # A stack of the current and parent lists.
2330
+ self.listblock = None # Current list is in list block.
2331
+ def load(self,sections):
2332
+ AbstractBlocks.load(self,sections)
2333
+ def validate(self):
2334
+ AbstractBlocks.validate(self)
2335
+ for b in self.blocks:
2336
+ # Check list has valid type.
2337
+ if not b.type in b.TYPES:
2338
+ raise EAsciiDoc,'[%s] illegal type' % b.name
2339
+ # Check all list tags.
2340
+ if not b.listtag or not config.tags.has_key(b.listtag):
2341
+ warning('[%s] missing listtag' % b.name)
2342
+ if not b.itemtag or not config.tags.has_key(b.itemtag):
2343
+ warning('[%s] missing tag itemtag' % b.name)
2344
+ if not b.texttag or not config.tags.has_key(b.texttag):
2345
+ warning('[%s] missing tag texttag' % b.name)
2346
+ if b.type == 'labeled':
2347
+ if not b.entrytag or not config.tags.has_key(b.entrytag):
2348
+ warning('[%s] missing entrytag' % b.name)
2349
+ if not b.labeltag or not config.tags.has_key(b.labeltag):
2350
+ warning('[%s] missing labeltag' % b.name)
2351
+
2352
+ class DelimitedBlock(AbstractBlock):
2353
+ def __init__(self):
2354
+ AbstractBlock.__init__(self)
2355
+ self.CONF_ENTRIES += ('delimiter','template','filter')
2356
+ self.OPTIONS = ('skip','sectionbody','list')
2357
+ self.start = None # File reader cursor at start delimiter.
2358
+ def load(self,name,entries):
2359
+ AbstractBlock.load(self,name,entries)
2360
+ def dump(self):
2361
+ AbstractBlock.dump(self)
2362
+ write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2363
+ write('')
2364
+ def isnext(self):
2365
+ return AbstractBlock.isnext(self)
2366
+ def translate(self):
2367
+ AbstractBlock.translate(self)
2368
+ if 'list' in self.options:
2369
+ lists.listblock = self
2370
+ reader.read() # Discard delimiter.
2371
+ self.start = reader.cursor[:]
2372
+ attrs = {}
2373
+ # Leave list block attributes for the list element.
2374
+ if lists.listblock is not self:
2375
+ BlockTitle.consume(attrs)
2376
+ AttributeList.consume(attrs)
2377
+ self.merge_attributes(attrs)
2378
+ options = self.get_options()
2379
+ if safe() and self.name == 'blockdef-backend':
2380
+ unsafe_error('Backend Block')
2381
+ # Discard block body.
2382
+ reader.read_until(self.delimiter,same_file=True)
2383
+ elif 'skip' in options:
2384
+ # Discard block body.
2385
+ reader.read_until(self.delimiter,same_file=True)
2386
+ else:
2387
+ template = self.get_template()
2388
+ stag,etag = config.section2tags(template,self.attributes)
2389
+ if 'sectionbody' in options or 'list' in options:
2390
+ # The body is treated like a SimpleSection.
2391
+ writer.write(stag)
2392
+ Section.translate_body(self)
2393
+ writer.write(etag)
2394
+ else:
2395
+ body = reader.read_until(self.delimiter,same_file=True)
2396
+ presubs,postsubs = self.get_subs()
2397
+ body = Lex.subs(body,presubs)
2398
+ if self.get_filter():
2399
+ body = filter_lines(self.get_filter(),body,self.attributes)
2400
+ body = Lex.subs(body,postsubs)
2401
+ # Write start tag, content, end tag.
2402
+ writer.write(dovetail_tags(stag,body,etag))
2403
+ if 'list' in options:
2404
+ lists.listblock = None
2405
+ if reader.eof():
2406
+ error('missing %s block closing delimiter' % self.name.split('-')[-1],
2407
+ cursor=self.start)
2408
+ else:
2409
+ delimiter = reader.read() # Discard delimiter line.
2410
+ assert re.match(self.delimiter,delimiter)
2411
+
2412
+ class DelimitedBlocks(AbstractBlocks):
2413
+ """List of delimited blocks."""
2414
+ BLOCK_TYPE = DelimitedBlock
2415
+ PREFIX = 'blockdef-'
2416
+ def __init__(self):
2417
+ AbstractBlocks.__init__(self)
2418
+ def load(self,sections):
2419
+ """Update blocks defined in 'sections' dictionary."""
2420
+ AbstractBlocks.load(self,sections)
2421
+ def validate(self):
2422
+ AbstractBlocks.validate(self)
2423
+
2424
+ class Column:
2425
+ """Table column."""
2426
+ def __init__(self):
2427
+ self.colalign = None # 'left','right','center'
2428
+ self.rulerwidth = None
2429
+ self.colwidth = None # Output width in page units.
2430
+
2431
+ class Table(AbstractBlock):
2432
+ COL_STOP = r"(`|'|\.)" # RE.
2433
+ ALIGNMENTS = {'`':'left', "'":'right', '.':'center'}
2434
+ FORMATS = ('fixed','csv','dsv')
2435
+ def __init__(self):
2436
+ AbstractBlock.__init__(self)
2437
+ self.CONF_ENTRIES += ('template','fillchar','format','colspec',
2438
+ 'headrow','footrow','bodyrow','headdata',
2439
+ 'footdata', 'bodydata')
2440
+ # Configuration parameters.
2441
+ self.fillchar=None
2442
+ self.format=None # 'fixed','csv','dsv'
2443
+ self.colspec=None
2444
+ self.headrow=None
2445
+ self.footrow=None
2446
+ self.bodyrow=None
2447
+ self.headdata=None
2448
+ self.footdata=None
2449
+ self.bodydata=None
2450
+ # Calculated parameters.
2451
+ self.underline=None # RE matching current table underline.
2452
+ self.isnumeric=False # True if numeric ruler.
2453
+ self.tablewidth=None # Optional table width scale factor.
2454
+ self.columns=[] # List of Columns.
2455
+ # Other.
2456
+ self.check_msg='' # Message set by previous self.validate() call.
2457
+ def load(self,name,entries):
2458
+ AbstractBlock.load(self,name,entries)
2459
+ """Update table definition from section entries in 'entries'."""
2460
+ for k,v in entries.items():
2461
+ if k == 'fillchar':
2462
+ if v and len(v) == 1:
2463
+ self.fillchar = v
2464
+ else:
2465
+ raise EAsciiDoc,'malformed table fillchar: %s' % v
2466
+ elif k == 'format':
2467
+ if v in Table.FORMATS:
2468
+ self.format = v
2469
+ else:
2470
+ raise EAsciiDoc,'illegal table format: %s' % v
2471
+ elif k == 'colspec':
2472
+ self.colspec = v
2473
+ elif k == 'headrow':
2474
+ self.headrow = v
2475
+ elif k == 'footrow':
2476
+ self.footrow = v
2477
+ elif k == 'bodyrow':
2478
+ self.bodyrow = v
2479
+ elif k == 'headdata':
2480
+ self.headdata = v
2481
+ elif k == 'footdata':
2482
+ self.footdata = v
2483
+ elif k == 'bodydata':
2484
+ self.bodydata = v
2485
+ def dump(self):
2486
+ AbstractBlock.dump(self)
2487
+ write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2488
+ write('fillchar='+self.fillchar)
2489
+ write('format='+self.format)
2490
+ if self.colspec:
2491
+ write('colspec='+self.colspec)
2492
+ if self.headrow:
2493
+ write('headrow='+self.headrow)
2494
+ if self.footrow:
2495
+ write('footrow='+self.footrow)
2496
+ write('bodyrow='+self.bodyrow)
2497
+ if self.headdata:
2498
+ write('headdata='+self.headdata)
2499
+ if self.footdata:
2500
+ write('footdata='+self.footdata)
2501
+ write('bodydata='+self.bodydata)
2502
+ write('')
2503
+ def validate(self):
2504
+ AbstractBlock.validate(self)
2505
+ """Check table definition and set self.check_msg if invalid else set
2506
+ self.check_msg to blank string."""
2507
+ # Check global table parameters.
2508
+ if config.textwidth is None:
2509
+ self.check_msg = 'missing [miscellaneous] textwidth entry'
2510
+ elif config.pagewidth is None:
2511
+ self.check_msg = 'missing [miscellaneous] pagewidth entry'
2512
+ elif config.pageunits is None:
2513
+ self.check_msg = 'missing [miscellaneous] pageunits entry'
2514
+ elif self.headrow is None:
2515
+ self.check_msg = 'missing headrow entry'
2516
+ elif self.footrow is None:
2517
+ self.check_msg = 'missing footrow entry'
2518
+ elif self.bodyrow is None:
2519
+ self.check_msg = 'missing bodyrow entry'
2520
+ elif self.headdata is None:
2521
+ self.check_msg = 'missing headdata entry'
2522
+ elif self.footdata is None:
2523
+ self.check_msg = 'missing footdata entry'
2524
+ elif self.bodydata is None:
2525
+ self.check_msg = 'missing bodydata entry'
2526
+ else:
2527
+ # No errors.
2528
+ self.check_msg = ''
2529
+ def isnext(self):
2530
+ return AbstractBlock.isnext(self)
2531
+ def parse_ruler(self,ruler):
2532
+ """Parse ruler calculating underline and ruler column widths."""
2533
+ fc = re.escape(self.fillchar)
2534
+ # Strip and save optional tablewidth from end of ruler.
2535
+ mo = re.match(r'^(.*'+fc+r'+)([\d\.]+)$',ruler)
2536
+ if mo:
2537
+ ruler = mo.group(1)
2538
+ self.tablewidth = float(mo.group(2))
2539
+ self.attributes['tablewidth'] = str(float(self.tablewidth))
2540
+ else:
2541
+ self.tablewidth = None
2542
+ self.attributes['tablewidth'] = '100.0'
2543
+ # Guess whether column widths are specified numerically or not.
2544
+ if ruler[1] != self.fillchar:
2545
+ # If the first column does not start with a fillchar then numeric.
2546
+ self.isnumeric = True
2547
+ elif ruler[1:] == self.fillchar*len(ruler[1:]):
2548
+ # The case of one column followed by fillchars is numeric.
2549
+ self.isnumeric = True
2550
+ else:
2551
+ self.isnumeric = False
2552
+ # Underlines must be 3 or more fillchars.
2553
+ self.underline = r'^' + fc + r'{3,}$'
2554
+ splits = re.split(self.COL_STOP,ruler)[1:]
2555
+ # Build self.columns.
2556
+ for i in range(0,len(splits),2):
2557
+ c = Column()
2558
+ c.colalign = self.ALIGNMENTS[splits[i]]
2559
+ s = splits[i+1]
2560
+ if self.isnumeric:
2561
+ # Strip trailing fillchars.
2562
+ s = re.sub(fc+r'+$','',s)
2563
+ if s == '':
2564
+ c.rulerwidth = None
2565
+ else:
2566
+ c.rulerwidth = int(validate(s,'int($)>0',
2567
+ 'malformed ruler: bad width'))
2568
+ else: # Calculate column width from inter-fillchar intervals.
2569
+ if not re.match(r'^'+fc+r'+$',s):
2570
+ raise EAsciiDoc,'malformed ruler: illegal fillchars'
2571
+ c.rulerwidth = len(s)+1
2572
+ self.columns.append(c)
2573
+ # Fill in unspecified ruler widths.
2574
+ if self.isnumeric:
2575
+ if self.columns[0].rulerwidth is None:
2576
+ prevwidth = 1
2577
+ for c in self.columns:
2578
+ if c.rulerwidth is None:
2579
+ c.rulerwidth = prevwidth
2580
+ prevwidth = c.rulerwidth
2581
+ def build_colspecs(self):
2582
+ """Generate colwidths and colspecs. This can only be done after the
2583
+ table arguments have been parsed since we use the table format."""
2584
+ self.attributes['cols'] = len(self.columns)
2585
+ # Calculate total ruler width.
2586
+ totalwidth = 0
2587
+ for c in self.columns:
2588
+ totalwidth = totalwidth + c.rulerwidth
2589
+ if totalwidth <= 0:
2590
+ raise EAsciiDoc,'zero width table'
2591
+ # Calculate marked up colwidths from rulerwidths.
2592
+ for c in self.columns:
2593
+ # Convert ruler width to output page width.
2594
+ width = float(c.rulerwidth)
2595
+ if self.format == 'fixed':
2596
+ if self.tablewidth is None:
2597
+ # Size proportional to ruler width.
2598
+ colfraction = width/config.textwidth
2599
+ else:
2600
+ # Size proportional to page width.
2601
+ colfraction = width/totalwidth
2602
+ else:
2603
+ # Size proportional to page width.
2604
+ colfraction = width/totalwidth
2605
+ c.colwidth = colfraction * config.pagewidth # To page units.
2606
+ if self.tablewidth is not None:
2607
+ c.colwidth = c.colwidth * self.tablewidth # Scale factor.
2608
+ if self.tablewidth > 1:
2609
+ c.colwidth = c.colwidth/100 # tablewidth is in percent.
2610
+ # Build colspecs.
2611
+ if self.colspec:
2612
+ cols = []
2613
+ i = 0
2614
+ for c in self.columns:
2615
+ i += 1
2616
+ self.attributes['colalign'] = c.colalign
2617
+ self.attributes['colwidth'] = str(int(c.colwidth))
2618
+ self.attributes['colnumber'] = str(i + 1)
2619
+ s = subs_attrs(self.colspec,self.attributes)
2620
+ if not s:
2621
+ warning('colspec dropped: contains undefined attribute')
2622
+ else:
2623
+ cols.append(s)
2624
+ self.attributes['colspecs'] = writer.newline.join(cols)
2625
+ def split_rows(self,rows):
2626
+ """Return a two item tuple containing a list of lines up to but not
2627
+ including the next underline (continued lines are joined ) and the
2628
+ tuple of all lines after the underline."""
2629
+ reo = re.compile(self.underline)
2630
+ i = 0
2631
+ while not reo.match(rows[i]):
2632
+ i = i+1
2633
+ if i == 0:
2634
+ raise EAsciiDoc,'missing table rows'
2635
+ if i >= len(rows):
2636
+ raise EAsciiDoc,'closing [%s] underline expected' % self.name
2637
+ return (join_lines(rows[:i]), rows[i+1:])
2638
+ def parse_rows(self, rows, rtag, dtag):
2639
+ """Parse rows list using the row and data tags. Returns a substituted
2640
+ list of output lines."""
2641
+ result = []
2642
+ # Source rows are parsed as single block, rather than line by line, to
2643
+ # allow the CSV reader to handle multi-line rows.
2644
+ if self.format == 'fixed':
2645
+ rows = self.parse_fixed(rows)
2646
+ elif self.format == 'csv':
2647
+ rows = self.parse_csv(rows)
2648
+ elif self.format == 'dsv':
2649
+ rows = self.parse_dsv(rows)
2650
+ else:
2651
+ assert True,'illegal table format'
2652
+ # Substitute and indent all data in all rows.
2653
+ stag,etag = subs_tag(rtag,self.attributes)
2654
+ for row in rows:
2655
+ result.append(' '+stag)
2656
+ for data in self.subs_row(row,dtag):
2657
+ result.append(' '+data)
2658
+ result.append(' '+etag)
2659
+ return result
2660
+ def subs_row(self, data, dtag):
2661
+ """Substitute the list of source row data elements using the data tag.
2662
+ Returns a substituted list of output table data items."""
2663
+ result = []
2664
+ if len(data) < len(self.columns):
2665
+ warning('fewer row data items then table columns')
2666
+ if len(data) > len(self.columns):
2667
+ warning('more row data items than table columns')
2668
+ for i in range(len(self.columns)):
2669
+ if i > len(data) - 1:
2670
+ d = '' # Fill missing column data with blanks.
2671
+ else:
2672
+ d = data[i]
2673
+ c = self.columns[i]
2674
+ self.attributes['colalign'] = c.colalign
2675
+ self.attributes['colwidth'] = str(int(c.colwidth))
2676
+ self.attributes['colnumber'] = str(i + 1)
2677
+ stag,etag = subs_tag(dtag,self.attributes)
2678
+ # Insert AsciiDoc line break (' +') where row data has newlines
2679
+ # ('\n'). This is really only useful when the table format is csv
2680
+ # and the output markup is HTML. It's also a bit dubious in that it
2681
+ # assumes the user has not modified the shipped line break pattern.
2682
+ subs = self.get_subs()[0]
2683
+ if 'replacements' in subs:
2684
+ # Insert line breaks in cell data.
2685
+ d = re.sub(r'(?m)\n',r' +\n',d)
2686
+ d = d.split('\n') # So writer.newline is written.
2687
+ else:
2688
+ d = [d]
2689
+ result = result + [stag] + Lex.subs(d,subs) + [etag]
2690
+ return result
2691
+ def parse_fixed(self,rows):
2692
+ """Parse the list of source table rows. Each row item in the returned
2693
+ list contains a list of cell data elements."""
2694
+ result = []
2695
+ for row in rows:
2696
+ data = []
2697
+ start = 0
2698
+ # build an encoded representation
2699
+ row = char_decode(row)
2700
+ for c in self.columns:
2701
+ end = start + c.rulerwidth
2702
+ if c is self.columns[-1]:
2703
+ # Text in last column can continue forever.
2704
+ # Use the encoded string to slice, but convert back
2705
+ # to plain string before further processing
2706
+ data.append(char_encode(row[start:]).strip())
2707
+ else:
2708
+ data.append(char_encode(row[start:end]).strip())
2709
+ start = end
2710
+ result.append(data)
2711
+ return result
2712
+ def parse_csv(self,rows):
2713
+ """Parse the list of source table rows. Each row item in the returned
2714
+ list contains a list of cell data elements."""
2715
+ import StringIO
2716
+ import csv
2717
+ result = []
2718
+ rdr = csv.reader(StringIO.StringIO('\r\n'.join(rows)),
2719
+ skipinitialspace=True)
2720
+ try:
2721
+ for row in rdr:
2722
+ result.append(row)
2723
+ except:
2724
+ raise EAsciiDoc,'csv parse error: %s' % row
2725
+ return result
2726
+ def parse_dsv(self,rows):
2727
+ """Parse the list of source table rows. Each row item in the returned
2728
+ list contains a list of cell data elements."""
2729
+ separator = self.attributes.get('separator',':')
2730
+ separator = eval('"'+separator+'"')
2731
+ if len(separator) != 1:
2732
+ raise EAsciiDoc,'malformed dsv separator: %s' % separator
2733
+ # TODO If separator is preceeded by an odd number of backslashes then
2734
+ # it is escaped and should not delimit.
2735
+ result = []
2736
+ for row in rows:
2737
+ # Skip blank lines
2738
+ if row == '': continue
2739
+ # Unescape escaped characters.
2740
+ row = eval('"'+row.replace('"','\\"')+'"')
2741
+ data = row.split(separator)
2742
+ data = [s.strip() for s in data]
2743
+ result.append(data)
2744
+ return result
2745
+ def translate(self):
2746
+ AbstractBlock.translate(self)
2747
+ # Reset instance specific properties.
2748
+ self.underline = None
2749
+ self.columns = []
2750
+ attrs = {}
2751
+ BlockTitle.consume(attrs)
2752
+ # Add relevant globals to table substitutions.
2753
+ attrs['pagewidth'] = str(config.pagewidth)
2754
+ attrs['pageunits'] = config.pageunits
2755
+ # Mix in document attribute list.
2756
+ AttributeList.consume(attrs)
2757
+ # Validate overridable attributes.
2758
+ for k,v in attrs.items():
2759
+ if k == 'format':
2760
+ if v not in self.FORMATS:
2761
+ raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.name,k,v)
2762
+ self.format = v
2763
+ elif k == 'tablewidth':
2764
+ try:
2765
+ self.tablewidth = float(attrs['tablewidth'])
2766
+ except:
2767
+ raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.name,k,v)
2768
+ self.merge_attributes(attrs)
2769
+ # Parse table ruler.
2770
+ ruler = reader.read()
2771
+ assert re.match(self.delimiter,ruler)
2772
+ self.parse_ruler(ruler)
2773
+ # Read the entire table.
2774
+ table = []
2775
+ while True:
2776
+ line = reader.read_next()
2777
+ # Table terminated by underline followed by a blank line or EOF.
2778
+ if len(table) > 0 and re.match(self.underline,table[-1]):
2779
+ if line in ('',None):
2780
+ break;
2781
+ if line is None:
2782
+ raise EAsciiDoc,'closing [%s] underline expected' % self.name
2783
+ table.append(reader.read())
2784
+ # EXPERIMENTAL: The number of lines in the table, requested by Benjamin Klum.
2785
+ self.attributes['rows'] = str(len(table))
2786
+ #TODO: Inherited validate() doesn't set check_msg, needs checking.
2787
+ if self.check_msg: # Skip if table definition was marked invalid.
2788
+ warning('skipping %s table: %s' % (self.name,self.check_msg))
2789
+ return
2790
+ # Generate colwidths and colspecs.
2791
+ self.build_colspecs()
2792
+ # Generate headrows, footrows, bodyrows.
2793
+ # Headrow, footrow and bodyrow data replaces same named attributes in
2794
+ # the table markup template. In order to ensure this data does not get
2795
+ # a second attribute substitution (which would interfere with any
2796
+ # already substituted inline passthroughs) unique placeholders are used
2797
+ # (the tab character does not appear elsewhere since it is expanded on
2798
+ # input) which are replaced after template attribute substitution.
2799
+ headrows = footrows = []
2800
+ bodyrows,table = self.split_rows(table)
2801
+ if table:
2802
+ headrows = bodyrows
2803
+ bodyrows,table = self.split_rows(table)
2804
+ if table:
2805
+ footrows,table = self.split_rows(table)
2806
+ if headrows:
2807
+ headrows = self.parse_rows(headrows, self.headrow, self.headdata)
2808
+ headrows = writer.newline.join(headrows)
2809
+ self.attributes['headrows'] = '\theadrows\t'
2810
+ if footrows:
2811
+ footrows = self.parse_rows(footrows, self.footrow, self.footdata)
2812
+ footrows = writer.newline.join(footrows)
2813
+ self.attributes['footrows'] = '\tfootrows\t'
2814
+ bodyrows = self.parse_rows(bodyrows, self.bodyrow, self.bodydata)
2815
+ bodyrows = writer.newline.join(bodyrows)
2816
+ self.attributes['bodyrows'] = '\tbodyrows\t'
2817
+ table = subs_attrs(config.sections[self.template],self.attributes)
2818
+ table = writer.newline.join(table)
2819
+ # Before we finish replace the table head, foot and body place holders
2820
+ # with the real data.
2821
+ if headrows:
2822
+ table = table.replace('\theadrows\t', headrows, 1)
2823
+ if footrows:
2824
+ table = table.replace('\tfootrows\t', footrows, 1)
2825
+ table = table.replace('\tbodyrows\t', bodyrows, 1)
2826
+ writer.write(table)
2827
+
2828
+ class Tables(AbstractBlocks):
2829
+ """List of tables."""
2830
+ BLOCK_TYPE = Table
2831
+ PREFIX = 'tabledef-'
2832
+ def __init__(self):
2833
+ AbstractBlocks.__init__(self)
2834
+ def load(self,sections):
2835
+ AbstractBlocks.load(self,sections)
2836
+ """Update tables defined in 'sections' dictionary."""
2837
+ def validate(self):
2838
+ # Does not call AbstractBlocks.validate().
2839
+ # Check we have a default table definition,
2840
+ for i in range(len(self.blocks)):
2841
+ if self.blocks[i].name == 'tabledef-default':
2842
+ default = self.blocks[i]
2843
+ break
2844
+ else:
2845
+ raise EAsciiDoc,'missing [table-default] section'
2846
+ # Set default table defaults.
2847
+ if default.format is None: default.subs = 'fixed'
2848
+ # Propagate defaults to unspecified table parameters.
2849
+ for b in self.blocks:
2850
+ if b is not default:
2851
+ if b.fillchar is None: b.fillchar = default.fillchar
2852
+ if b.format is None: b.format = default.format
2853
+ if b.template is None: b.template = default.template
2854
+ if b.colspec is None: b.colspec = default.colspec
2855
+ if b.headrow is None: b.headrow = default.headrow
2856
+ if b.footrow is None: b.footrow = default.footrow
2857
+ if b.bodyrow is None: b.bodyrow = default.bodyrow
2858
+ if b.headdata is None: b.headdata = default.headdata
2859
+ if b.footdata is None: b.footdata = default.footdata
2860
+ if b.bodydata is None: b.bodydata = default.bodydata
2861
+ # Check all tables have valid fill character.
2862
+ for b in self.blocks:
2863
+ if not b.fillchar or len(b.fillchar) != 1:
2864
+ raise EAsciiDoc,'[%s] missing or illegal fillchar' % b.name
2865
+ # Build combined tables delimiter patterns and assign defaults.
2866
+ delimiters = []
2867
+ for b in self.blocks:
2868
+ # Ruler is:
2869
+ # (ColStop,(ColWidth,FillChar+)?)+, FillChar+, TableWidth?
2870
+ b.delimiter = r'^(' + Table.COL_STOP \
2871
+ + r'(\d*|' + re.escape(b.fillchar) + r'*)' \
2872
+ + r')+' \
2873
+ + re.escape(b.fillchar) + r'+' \
2874
+ + '([\d\.]*)$'
2875
+ delimiters.append(b.delimiter)
2876
+ if not b.headrow:
2877
+ b.headrow = b.bodyrow
2878
+ if not b.footrow:
2879
+ b.footrow = b.bodyrow
2880
+ if not b.headdata:
2881
+ b.headdata = b.bodydata
2882
+ if not b.footdata:
2883
+ b.footdata = b.bodydata
2884
+ self.delimiter = join_regexp(delimiters)
2885
+ # Check table definitions are valid.
2886
+ for b in self.blocks:
2887
+ b.validate()
2888
+ if config.verbose:
2889
+ if b.check_msg:
2890
+ warning('[%s] table definition: %s' % (b.name,b.check_msg))
2891
+
2892
+ class Macros:
2893
+ # Default system macro syntax.
2894
+ SYS_DEFAULT = r'(?u)^(?P<name>\w(\w|-)*?)::(?P<target>\S*?)' + \
2895
+ r'(\[(?P<attrlist>.*?)\])$'
2896
+ def __init__(self):
2897
+ self.macros = [] # List of Macros.
2898
+ self.current = None # The last matched block macro.
2899
+ # Initialize default system macro.
2900
+ m = Macro()
2901
+ m.pattern = self.SYS_DEFAULT
2902
+ m.prefix = '+'
2903
+ m.reo = re.compile(m.pattern)
2904
+ self.macros.append(m)
2905
+ def load(self,entries):
2906
+ for entry in entries:
2907
+ m = Macro()
2908
+ m.load(entry)
2909
+ if m.name is None:
2910
+ # Delete undefined macro.
2911
+ for i in range(len(self.macros)-1,-1,-1):
2912
+ if self.macros[i].pattern == m.pattern:
2913
+ del self.macros[i]
2914
+ else:
2915
+ # Check for duplicates.
2916
+ for m2 in self.macros:
2917
+ if m.equals(m2):
2918
+ verbose('macro redefinition: %s%s' % (m.prefix,m.name))
2919
+ break
2920
+ else:
2921
+ self.macros.append(m)
2922
+ def dump(self):
2923
+ write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2924
+ write('[macros]')
2925
+ # Dump all macros except the first (built-in system) macro.
2926
+ for m in self.macros[1:]:
2927
+ write('%s=%s%s' % (m.pattern,m.prefix,m.name))
2928
+ write('')
2929
+ def validate(self):
2930
+ # Check all named sections exist.
2931
+ if config.verbose:
2932
+ for m in self.macros:
2933
+ if m.name and m.prefix != '+':
2934
+ m.section_name()
2935
+ def subs(self,text,prefix='',callouts=False):
2936
+ # If callouts is True then only callout macros are processed, if False
2937
+ # then all non-callout macros are processed.
2938
+ result = text
2939
+ for m in self.macros:
2940
+ if m.prefix == prefix:
2941
+ if callouts ^ (m.name != 'callout'):
2942
+ result = m.subs(result)
2943
+ return result
2944
+ def isnext(self):
2945
+ """Return matching macro if block macro is next on reader."""
2946
+ reader.skip_blank_lines()
2947
+ line = reader.read_next()
2948
+ if line:
2949
+ for m in self.macros:
2950
+ if m.prefix == '#':
2951
+ if m.reo.match(line):
2952
+ self.current = m
2953
+ return m
2954
+ return False
2955
+ def match(self,prefix,name,text):
2956
+ """Return re match object matching 'text' with macro type 'prefix',
2957
+ macro name 'name'."""
2958
+ for m in self.macros:
2959
+ if m.prefix == prefix:
2960
+ mo = m.reo.match(text)
2961
+ if mo:
2962
+ if m.name == name:
2963
+ return mo
2964
+ if re.match(name,mo.group('name')):
2965
+ return mo
2966
+ return None
2967
+
2968
+ # Macro set just prior to calling _subs_macro(). Ugly but there's no way
2969
+ # to pass optional arguments with _subs_macro().
2970
+ _macro = None
2971
+
2972
+ def _subs_macro(mo):
2973
+ """Function called to perform inline macro substitution. Uses matched macro
2974
+ regular expression object and returns string containing the substituted
2975
+ macro body. Called by Macros().subs()."""
2976
+ # Check if macro reference is escaped.
2977
+ if mo.group()[0] == '\\':
2978
+ return mo.group()[1:] # Strip leading backslash.
2979
+ d = mo.groupdict()
2980
+ # Delete groups that didn't participate in match.
2981
+ for k,v in d.items():
2982
+ if v is None: del d[k]
2983
+ if _macro.name:
2984
+ name = _macro.name
2985
+ else:
2986
+ if not d.has_key('name'):
2987
+ warning('missing macro name group: %s' % mo.re.pattern)
2988
+ return ''
2989
+ name = d['name']
2990
+ section_name = _macro.section_name(name)
2991
+ if not section_name:
2992
+ return ''
2993
+ # If we're dealing with a block macro get optional block ID and block title.
2994
+ if _macro.prefix == '#':
2995
+ AttributeList.consume(d)
2996
+ BlockTitle.consume(d)
2997
+ # Parse macro attributes.
2998
+ if d.has_key('attrlist'):
2999
+ if d['attrlist'] in (None,''):
3000
+ del d['attrlist']
3001
+ else:
3002
+ parse_attributes(d['attrlist'],d)
3003
+ if name == 'callout':
3004
+ listindex =int(d['index'])
3005
+ d['coid'] = calloutmap.add(listindex)
3006
+ # Unescape special characters in LaTeX target file names.
3007
+ if document.backend == 'latex' and d.has_key('target') and d['target']:
3008
+ if not d.has_key('0'):
3009
+ d['0'] = d['target']
3010
+ d['target']= config.subs_specialchars_reverse(d['target'])
3011
+ # BUG: We've already done attribute substitution on the macro which means
3012
+ # that any escaped attribute references are now unescaped and will be
3013
+ # substituted by config.subs_section() below. As a partial fix have withheld
3014
+ # {0} from substitution but this kludge doesn't fix it for other attributes
3015
+ # containing unescaped references.
3016
+ a0 = d.get('0')
3017
+ if a0:
3018
+ d['0'] = chr(0) # Replace temporarily with unused character.
3019
+ body = config.subs_section(section_name,d)
3020
+ if len(body) == 0:
3021
+ result = ''
3022
+ elif len(body) == 1:
3023
+ result = body[0]
3024
+ else:
3025
+ if _macro.prefix == '#':
3026
+ result = writer.newline.join(body)
3027
+ else:
3028
+ # Internally processed inline macros use UNIX line separator.
3029
+ result = '\n'.join(body)
3030
+ if a0:
3031
+ result = result.replace(chr(0), a0)
3032
+ return result
3033
+
3034
+ class Macro:
3035
+ def __init__(self):
3036
+ self.pattern = None # Matching regular expression.
3037
+ self.name = '' # Conf file macro name (None if implicit).
3038
+ self.prefix = '' # '' if inline, '+' if system, '#' if block.
3039
+ self.reo = None # Compiled pattern re object.
3040
+ def section_name(self,name=None):
3041
+ """Return macro markup template section name based on macro name and
3042
+ prefix. Return None section not found."""
3043
+ assert self.prefix != '+'
3044
+ if not name:
3045
+ assert self.name
3046
+ name = self.name
3047
+ if self.prefix == '#':
3048
+ suffix = '-blockmacro'
3049
+ else:
3050
+ suffix = '-inlinemacro'
3051
+ if config.sections.has_key(name+suffix):
3052
+ return name+suffix
3053
+ else:
3054
+ warning('missing macro section: [%s]' % name+suffix)
3055
+ return None
3056
+ def equals(self,m):
3057
+ if self.pattern != m.pattern:
3058
+ return False
3059
+ if self.name != m.name:
3060
+ return False
3061
+ if self.prefix != m.prefix:
3062
+ return False
3063
+ return True
3064
+ def load(self,entry):
3065
+ e = parse_entry(entry)
3066
+ if not e:
3067
+ raise EAsciiDoc,'malformed macro entry: %s' % entry
3068
+ if not is_regexp(e[0]):
3069
+ raise EAsciiDoc,'illegal %s macro regular expression: %s' \
3070
+ % (e[1],e[0])
3071
+ self.pattern, self.name = e
3072
+ self.reo = re.compile(self.pattern)
3073
+ if self.name:
3074
+ if self.name[0] in ('+','#'):
3075
+ self.prefix, self.name = self.name[0], self.name[1:]
3076
+ if self.name and not is_name(self.name):
3077
+ raise EAsciiDoc,'illegal section name in macro entry: %s' % entry
3078
+ def subs(self,text):
3079
+ global _macro
3080
+ _macro = self # Pass the macro to _subs_macro().
3081
+ return self.reo.sub(_subs_macro,text)
3082
+ def translate(self):
3083
+ """ Block macro translation."""
3084
+ assert self.prefix == '#'
3085
+ s = reader.read()
3086
+ s = subs_attrs(s) # Substitute global attributes.
3087
+ if s:
3088
+ s = self.subs(s)
3089
+ if s:
3090
+ writer.write(s)
3091
+
3092
+ class CalloutMap:
3093
+ def __init__(self):
3094
+ self.comap = {} # key = list index, value = callouts list.
3095
+ self.calloutindex = 0 # Current callout index number.
3096
+ self.listnumber = 1 # Current callout list number.
3097
+ def listclose(self):
3098
+ # Called when callout list is closed.
3099
+ self.listnumber += 1
3100
+ self.calloutindex = 0
3101
+ self.comap = {}
3102
+ def add(self,listindex):
3103
+ # Add next callout index to listindex map entry. Return the callout id.
3104
+ self.calloutindex += 1
3105
+ # Append the coindex to a list in the comap dictionary.
3106
+ if not self.comap.has_key(listindex):
3107
+ self.comap[listindex] = [self.calloutindex]
3108
+ else:
3109
+ self.comap[listindex].append(self.calloutindex)
3110
+ return self.calloutid(self.listnumber, self.calloutindex)
3111
+ def calloutid(listnumber,calloutindex):
3112
+ return 'CO%d-%d' % (listnumber,calloutindex)
3113
+ calloutid = staticmethod(calloutid)
3114
+ def calloutids(self,listindex):
3115
+ # Retieve list of callout indexes that refer to listindex.
3116
+ if self.comap.has_key(listindex):
3117
+ result = ''
3118
+ for coindex in self.comap[listindex]:
3119
+ result += ' ' + self.calloutid(self.listnumber,coindex)
3120
+ return result.strip()
3121
+ else:
3122
+ error('no callouts refer to list item '+str(listindex))
3123
+ return ''
3124
+ def validate(self,maxlistindex):
3125
+ # Check that all list indexes referenced by callouts exist.
3126
+ for listindex in self.comap.keys():
3127
+ if listindex > maxlistindex:
3128
+ warning('callout refers to non-existent list item '
3129
+ + str(listindex))
3130
+
3131
+ #---------------------------------------------------------------------------
3132
+ # Input stream Reader and output stream writer classes.
3133
+ #---------------------------------------------------------------------------
3134
+
3135
+ class Reader1:
3136
+ """Line oriented AsciiDoc input file reader. Processes include and
3137
+ conditional inclusion system macros. Tabs are expanded and lines are right
3138
+ trimmed."""
3139
+ # This class is not used directly, use Reader class instead.
3140
+ READ_BUFFER_MIN = 10 # Read buffer low level.
3141
+ def __init__(self):
3142
+ self.f = None # Input file object.
3143
+ self.fname = None # Input file name.
3144
+ self.next = [] # Read ahead buffer containing
3145
+ # [filename,linenumber,linetext] lists.
3146
+ self.cursor = None # Last read() [filename,linenumber,linetext].
3147
+ self.tabsize = 8 # Tab expansion number of spaces.
3148
+ self.parent = None # Included reader's parent reader.
3149
+ self._lineno = 0 # The last line read from file object f.
3150
+ self.include_depth = 0 # Current include depth.
3151
+ self.include_max = 5 # Maxiumum allowed include depth.
3152
+ def open(self,fname):
3153
+ self.fname = fname
3154
+ verbose('reading: '+fname)
3155
+ if fname == '<stdin>':
3156
+ self.f = sys.stdin
3157
+ else:
3158
+ self.f = open(fname,'rb')
3159
+ self._lineno = 0 # The last line read from file object f.
3160
+ self.next = []
3161
+ # Prefill buffer by reading the first line and then pushing it back.
3162
+ if Reader1.read(self):
3163
+ self.unread(self.cursor)
3164
+ self.cursor = None
3165
+ def closefile(self):
3166
+ """Used by class methods to close nested include files."""
3167
+ self.f.close()
3168
+ self.next = []
3169
+ def close(self):
3170
+ self.closefile()
3171
+ self.__init__()
3172
+ def read(self, skip=False):
3173
+ """Read next line. Return None if EOF. Expand tabs. Strip trailing
3174
+ white space. Maintain self.next read ahead buffer. If skip=True then
3175
+ conditional exclusion is active (ifdef and ifndef macros)."""
3176
+ # Top up buffer.
3177
+ if len(self.next) <= self.READ_BUFFER_MIN:
3178
+ s = self.f.readline()
3179
+ if s:
3180
+ self._lineno = self._lineno + 1
3181
+ while s:
3182
+ if self.tabsize != 0:
3183
+ s = s.expandtabs(self.tabsize)
3184
+ s = s.rstrip()
3185
+ self.next.append([self.fname,self._lineno,s])
3186
+ if len(self.next) > self.READ_BUFFER_MIN:
3187
+ break
3188
+ s = self.f.readline()
3189
+ if s:
3190
+ self._lineno = self._lineno + 1
3191
+ # Return first (oldest) buffer entry.
3192
+ if len(self.next) > 0:
3193
+ self.cursor = self.next[0]
3194
+ del self.next[0]
3195
+ result = self.cursor[2]
3196
+ # Check for include macro.
3197
+ mo = macros.match('+',r'include[1]?',result)
3198
+ if mo and not skip:
3199
+ # Perform attribute substitution on inlcude macro file name.
3200
+ fname = subs_attrs(mo.group('target'))
3201
+ if not fname:
3202
+ return Reader1.read(self) # Return next input line.
3203
+ if self.include_depth >= self.include_max:
3204
+ raise EAsciiDoc,'maxiumum inlcude depth exceeded'
3205
+ if self.fname != '<stdin>':
3206
+ fname = os.path.expandvars(os.path.expanduser(fname))
3207
+ fname = safe_filename(fname, os.path.dirname(self.fname))
3208
+ if not fname:
3209
+ return Reader1.read(self) # Return next input line.
3210
+ if mo.group('name') == 'include1':
3211
+ if not config.dumping:
3212
+ # Store the include file in memory for later
3213
+ # retrieval by the {include1:} system attribute.
3214
+ config.include1[fname] = \
3215
+ [s.rstrip() for s in open(fname)]
3216
+ return '{include1:%s}' % fname
3217
+ else:
3218
+ # This is a configuration dump, just pass the macro
3219
+ # call through.
3220
+ return result
3221
+ # Parse include macro attributes.
3222
+ attrs = {}
3223
+ parse_attributes(mo.group('attrlist'),attrs)
3224
+ # Clone self and set as parent (self assumes the role of child).
3225
+ parent = Reader1()
3226
+ assign(parent,self)
3227
+ self.parent = parent
3228
+ if attrs.has_key('tabsize'):
3229
+ self.tabsize = int(validate(attrs['tabsize'],'int($)>=0', \
3230
+ 'illegal include macro tabsize argument'))
3231
+ self.open(fname)
3232
+ self.include_depth = self.include_depth + 1
3233
+ result = Reader1.read(self)
3234
+ else:
3235
+ if not Reader1.eof(self):
3236
+ result = Reader1.read(self)
3237
+ else:
3238
+ result = None
3239
+ return result
3240
+ def eof(self):
3241
+ """Returns True if all lines have been read."""
3242
+ if len(self.next) == 0:
3243
+ # End of current file.
3244
+ if self.parent:
3245
+ self.closefile()
3246
+ assign(self,self.parent) # Restore parent reader.
3247
+ return Reader1.eof(self)
3248
+ else:
3249
+ return True
3250
+ else:
3251
+ return False
3252
+ def read_next(self):
3253
+ """Like read() but does not advance file pointer."""
3254
+ if Reader1.eof(self):
3255
+ return None
3256
+ else:
3257
+ return self.next[0][2]
3258
+ def unread(self,cursor):
3259
+ """Push the line (filename,linenumber,linetext) tuple back into the read
3260
+ buffer. Note that it's up to the caller to restore the previous
3261
+ cursor."""
3262
+ assert cursor
3263
+ self.next.insert(0,cursor)
3264
+
3265
+ class Reader(Reader1):
3266
+ """ Wraps (well, sought of) Reader1 class and implements conditional text
3267
+ inclusion."""
3268
+ def __init__(self):
3269
+ Reader1.__init__(self)
3270
+ self.depth = 0 # if nesting depth.
3271
+ self.skip = False # true if we're skipping ifdef...endif.
3272
+ self.skipname = '' # Name of current endif macro target.
3273
+ self.skipto = -1 # The depth at which skipping is reenabled.
3274
+ def read_super(self):
3275
+ result = Reader1.read(self,self.skip)
3276
+ if result is None and self.skip:
3277
+ raise EAsciiDoc,'missing endif::%s[]' % self.skipname
3278
+ return result
3279
+ def read(self):
3280
+ result = self.read_super()
3281
+ if result is None:
3282
+ return None
3283
+ while self.skip:
3284
+ mo = macros.match('+',r'ifdef|ifndef|endif',result)
3285
+ if mo:
3286
+ name = mo.group('name')
3287
+ target = mo.group('target')
3288
+ if name == 'endif':
3289
+ self.depth = self.depth-1
3290
+ if self.depth < 0:
3291
+ raise EAsciiDoc,'mismatched macro: %s' % result
3292
+ if self.depth == self.skipto:
3293
+ self.skip = False
3294
+ if target and self.skipname != target:
3295
+ raise EAsciiDoc,'mismatched macro: %s' % result
3296
+ else: # ifdef or ifndef.
3297
+ if not target:
3298
+ raise EAsciiDoc,'missing macro target: %s' % result
3299
+ self.depth = self.depth+1
3300
+ result = self.read_super()
3301
+ if result is None:
3302
+ return None
3303
+ mo = macros.match('+',r'ifdef|ifndef|endif',result)
3304
+ if mo:
3305
+ name = mo.group('name')
3306
+ target = mo.group('target')
3307
+ if name == 'endif':
3308
+ self.depth = self.depth-1
3309
+ else: # ifdef or ifndef.
3310
+ if not target:
3311
+ raise EAsciiDoc,'missing macro target: %s' % result
3312
+ defined = document.attributes.get(target) is not None
3313
+ if name == 'ifdef':
3314
+ self.skip = not defined
3315
+ else: # ifndef.
3316
+ self.skip = defined
3317
+ if self.skip:
3318
+ self.skipto = self.depth
3319
+ self.skipname = target
3320
+ self.depth = self.depth+1
3321
+ result = self.read()
3322
+ if result:
3323
+ # Expand executable block macros.
3324
+ mo = macros.match('+',r'eval|sys|sys2',result)
3325
+ if mo:
3326
+ action = mo.group('name')
3327
+ cmd = mo.group('attrlist')
3328
+ s = system(action, cmd, is_macro=True)
3329
+ if s is not None:
3330
+ self.cursor[2] = s # So we don't re-evaluate.
3331
+ result = s
3332
+ return result
3333
+ def eof(self):
3334
+ return self.read_next() is None
3335
+ def read_next(self):
3336
+ save_cursor = self.cursor
3337
+ result = self.read()
3338
+ if result is not None:
3339
+ self.unread(self.cursor)
3340
+ self.cursor = save_cursor
3341
+ return result
3342
+ def read_all(self,fname):
3343
+ """Read all lines from file fname and return as list. Use like class
3344
+ method: Reader().read_all(fname)"""
3345
+ result = []
3346
+ self.open(fname)
3347
+ try:
3348
+ while not self.eof():
3349
+ result.append(self.read())
3350
+ finally:
3351
+ self.close()
3352
+ return result
3353
+ def read_lines(self,count=1):
3354
+ """Return tuple containing count lines."""
3355
+ result = []
3356
+ i = 0
3357
+ while i < count and not self.eof():
3358
+ result.append(self.read())
3359
+ return tuple(result)
3360
+ def read_ahead(self,count=1):
3361
+ """Same as read_lines() but does not advance the file pointer."""
3362
+ result = []
3363
+ putback = []
3364
+ save_cursor = self.cursor
3365
+ try:
3366
+ i = 0
3367
+ while i < count and not self.eof():
3368
+ result.append(self.read())
3369
+ putback.append(self.cursor)
3370
+ i = i+1
3371
+ while putback:
3372
+ self.unread(putback.pop())
3373
+ finally:
3374
+ self.cursor = save_cursor
3375
+ return tuple(result)
3376
+ def skip_blank_lines(self):
3377
+ reader.read_until(r'\s*\S+')
3378
+ def read_until(self,pattern,same_file=False):
3379
+ """Like read() but reads lines up to (but not including) the first line
3380
+ that matches the pattern regular expression. If same_file is True
3381
+ then the terminating pattern must occur in the file the was being read
3382
+ when the routine was called."""
3383
+ if same_file:
3384
+ fname = self.cursor[0]
3385
+ result = []
3386
+ reo = re.compile(pattern)
3387
+ while not self.eof():
3388
+ save_cursor = self.cursor
3389
+ s = self.read()
3390
+ if (not same_file or fname == self.cursor[0]) and reo.match(s):
3391
+ self.unread(self.cursor)
3392
+ self.cursor = save_cursor
3393
+ break
3394
+ result.append(s)
3395
+ return tuple(result)
3396
+ # NOT USED -- part of unimplemented attempt a generalised line continuation.
3397
+ def read_continuation(self):
3398
+ """Like read() but treats trailing backslash as line continuation
3399
+ character."""
3400
+ s = self.read()
3401
+ if s is None:
3402
+ return None
3403
+ result = ''
3404
+ while s is not None and len(s) > 0 and s[-1] == '\\':
3405
+ result = result + s[:-1]
3406
+ s = self.read()
3407
+ if s is not None:
3408
+ result = result + s
3409
+ return result
3410
+ # NOT USED -- part of unimplemented attempt a generalised line continuation.
3411
+ def read_next_continuation(self):
3412
+ """Like read_next() but treats trailing backslash as line continuation
3413
+ character."""
3414
+ save_cursor = self.cursor
3415
+ result = self.read_continuation()
3416
+ if result is not None:
3417
+ self.unread(self.cursor)
3418
+ self.cursor = save_cursor
3419
+ return result
3420
+
3421
+ class Writer:
3422
+ """Writes lines to output file."""
3423
+ newline = '\r\n' # End of line terminator.
3424
+ f = None # Output file object.
3425
+ fname= None # Output file name.
3426
+ lines_out = 0 # Number of lines written.
3427
+ skip_blank_lines = False # If True don't output blank lines.
3428
+ def open(self,fname):
3429
+ self.fname = os.path.abspath(fname)
3430
+ verbose('writing: '+fname)
3431
+ if fname == '<stdout>':
3432
+ self.f = sys.stdout
3433
+ else:
3434
+ self.f = open(fname,'wb+')
3435
+ self.lines_out = 0
3436
+ def close(self):
3437
+ if self.fname != '<stdout>':
3438
+ self.f.close()
3439
+ def write_line(self, line=None):
3440
+ if not (self.skip_blank_lines and (not line or not line.strip())):
3441
+ if line is not None:
3442
+ self.f.write(line + self.newline)
3443
+ else:
3444
+ self.f.write(self.newline)
3445
+ self.lines_out = self.lines_out + 1
3446
+ def write(self,*args):
3447
+ """Iterates arguments, writes tuple and list arguments one line per
3448
+ element, else writes argument as single line. If no arguments writes
3449
+ blank line. If argument is None nothing is written. self.newline is
3450
+ appended to each line."""
3451
+ if len(args) == 0:
3452
+ self.write_line()
3453
+ self.lines_out = self.lines_out + 1
3454
+ else:
3455
+ for arg in args:
3456
+ if isinstance(arg,list) or isinstance(arg,tuple):
3457
+ for s in arg:
3458
+ self.write_line(s)
3459
+ elif arg is not None:
3460
+ self.write_line(arg)
3461
+ def write_tag(self,tagname,content,subs=None,d=None):
3462
+ """Write content enveloped by configuration file tag tagname.
3463
+ Substitutions specified in the 'subs' list are perform on the
3464
+ 'content'."""
3465
+ if subs is None:
3466
+ subs = config.subsnormal
3467
+ stag,etag = config.tag(tagname,d)
3468
+ if stag:
3469
+ self.write(stag)
3470
+ if content:
3471
+ self.write(Lex.subs(content,subs))
3472
+ if etag:
3473
+ self.write(etag)
3474
+
3475
+ #---------------------------------------------------------------------------
3476
+ # Configuration file processing.
3477
+ #---------------------------------------------------------------------------
3478
+ def _subs_specialwords(mo):
3479
+ """Special word substitution function called by
3480
+ Config.subs_specialwords()."""
3481
+ word = mo.re.pattern # The special word.
3482
+ template = config.specialwords[word] # The corresponding markup template.
3483
+ if not config.sections.has_key(template):
3484
+ raise EAsciiDoc,'missing special word template [%s]' % template
3485
+ if mo.group()[0] == '\\':
3486
+ return mo.group()[1:] # Return escaped word.
3487
+ args = {}
3488
+ args['words'] = mo.group() # The full match string is argument 'words'.
3489
+ args.update(mo.groupdict()) # Add other named match groups to the arguments.
3490
+ # Delete groups that didn't participate in match.
3491
+ for k,v in args.items():
3492
+ if v is None: del args[k]
3493
+ lines = subs_attrs(config.sections[template],args)
3494
+ if len(lines) == 0:
3495
+ result = ''
3496
+ elif len(lines) == 1:
3497
+ result = lines[0]
3498
+ else:
3499
+ result = writer.newline.join(lines)
3500
+ return result
3501
+
3502
+ class Config:
3503
+ """Methods to process configuration files."""
3504
+ # Predefined section name regexp's.
3505
+ SPECIAL_SECTIONS= ('tags','miscellaneous','attributes','specialcharacters',
3506
+ 'specialwords','macros','replacements','quotes','titles',
3507
+ r'paradef.+',r'listdef.+',r'blockdef.+',r'tabledef.*',
3508
+ 'replacements2')
3509
+ def __init__(self):
3510
+ self.sections = OrderedDict() # Keyed by section name containing
3511
+ # lists of section lines.
3512
+ # Command-line options.
3513
+ self.verbose = False
3514
+ self.header_footer = True # -s, --no-header-footer option.
3515
+ # [miscellaneous] section.
3516
+ self.tabsize = 8
3517
+ self.textwidth = 70
3518
+ self.newline = '\r\n'
3519
+ self.pagewidth = None
3520
+ self.pageunits = None
3521
+ self.outfilesuffix = ''
3522
+ self.subsnormal = SUBS_NORMAL
3523
+ self.subsverbatim = SUBS_VERBATIM
3524
+
3525
+ self.tags = {} # Values contain (stag,etag) tuples.
3526
+ self.specialchars = {} # Values of special character substitutions.
3527
+ self.specialwords = {} # Name is special word pattern, value is macro.
3528
+ self.replacements = OrderedDict() # Key is find pattern, value is
3529
+ #replace pattern.
3530
+ self.replacements2 = OrderedDict()
3531
+ self.specialsections = {} # Name is special section name pattern, value
3532
+ # is corresponding section name.
3533
+ self.quotes = {} # Values contain corresponding tag name.
3534
+ self.fname = '' # Most recently loaded configuration file name.
3535
+ self.conf_attrs = {} # Glossary entries from conf files.
3536
+ self.cmd_attrs = {} # Attributes from command-line -a options.
3537
+ self.loaded = [] # Loaded conf files.
3538
+ self.include1 = {} # Holds include1::[] files for {include1:}.
3539
+ self.dumping = False # True if asciidoc -c option specified.
3540
+
3541
+ def load(self,fname,dir=None):
3542
+ """Loads sections dictionary with sections from file fname.
3543
+ Existing sections are overlaid. Silently skips missing configuration
3544
+ files."""
3545
+ if dir:
3546
+ fname = os.path.join(dir, fname)
3547
+ # Sliently skip missing configuration file.
3548
+ if not os.path.isfile(fname):
3549
+ return
3550
+ # Don't load conf files twice (local and application conf files are the
3551
+ # same if the source file is in the application directory).
3552
+ if os.path.realpath(fname) in self.loaded:
3553
+ return
3554
+ rdr = Reader() # Reader processes system macros.
3555
+ rdr.open(fname)
3556
+ self.fname = fname
3557
+ reo = re.compile(r'(?u)^\[(?P<section>[^\W\d][\w-]*)\]\s*$')
3558
+ sections = OrderedDict()
3559
+ section,contents = '',[]
3560
+ while not rdr.eof():
3561
+ s = rdr.read()
3562
+ if s and s[0] == '#': # Skip comment lines.
3563
+ continue
3564
+ if s[:2] == '\\#': # Unescape lines starting with '#'.
3565
+ s = s[1:]
3566
+ s = s.rstrip()
3567
+ found = reo.findall(s)
3568
+ if found:
3569
+ if section: # Store previous section.
3570
+ if sections.has_key(section) \
3571
+ and self.is_special_section(section):
3572
+ if ''.join(contents):
3573
+ # Merge special sections.
3574
+ sections[section] = sections[section] + contents
3575
+ else:
3576
+ print 'blank section'
3577
+ del sections[section]
3578
+ else:
3579
+ sections[section] = contents
3580
+ section = found[0].lower()
3581
+ contents = []
3582
+ else:
3583
+ contents.append(s)
3584
+ if section and contents: # Store last section.
3585
+ if sections.has_key(section) \
3586
+ and self.is_special_section(section):
3587
+ if ''.join(contents):
3588
+ # Merge special sections.
3589
+ sections[section] = sections[section] + contents
3590
+ else:
3591
+ del sections[section]
3592
+ else:
3593
+ sections[section] = contents
3594
+ rdr.close()
3595
+ # Delete blank lines from sections.
3596
+ for k in sections.keys():
3597
+ for i in range(len(sections[k])-1,-1,-1):
3598
+ if not sections[k][i]:
3599
+ del sections[k][i]
3600
+ elif not self.is_special_section(k):
3601
+ break # Only trailing blanks from non-special sections.
3602
+ # Add/overwrite new sections.
3603
+ self.sections.update(sections)
3604
+ self.parse_tags()
3605
+ # Internally [miscellaneous] section entries are just attributes.
3606
+ d = {}
3607
+ parse_entries(sections.get('miscellaneous',()), d, unquote=True,
3608
+ allow_name_only=True)
3609
+ update_attrs(self.conf_attrs,d)
3610
+ d = {}
3611
+ parse_entries(sections.get('attributes',()), d, unquote=True,
3612
+ allow_name_only=True)
3613
+ update_attrs(self.conf_attrs,d)
3614
+ # Update document attributes so they are available immediately.
3615
+ document.init_attrs()
3616
+ d = {}
3617
+ parse_entries(sections.get('titles',()),d)
3618
+ Title.load(d)
3619
+ parse_entries(sections.get('specialcharacters',()),self.specialchars,escape_delimiter=False)
3620
+ parse_entries(sections.get('quotes',()),self.quotes)
3621
+ self.parse_specialwords()
3622
+ self.parse_replacements()
3623
+ self.parse_replacements('replacements2')
3624
+ self.parse_specialsections()
3625
+ paragraphs.load(sections)
3626
+ lists.load(sections)
3627
+ blocks.load(sections)
3628
+ tables.load(sections)
3629
+ macros.load(sections.get('macros',()))
3630
+ self.loaded.append(os.path.realpath(fname))
3631
+
3632
+ def load_all(self,dir):
3633
+ """Load the standard configuration files from directory 'dir'."""
3634
+ self.load('asciidoc.conf',dir)
3635
+ conf = document.backend + '.conf'
3636
+ self.load(conf,dir)
3637
+ conf = document.backend + '-' + document.doctype + '.conf'
3638
+ self.load(conf,dir)
3639
+ lang = document.attributes.get('lang')
3640
+ if lang:
3641
+ conf = 'lang-' + lang + '.conf'
3642
+ self.load(conf,dir)
3643
+ # Load ./filters/*.conf files if they exist.
3644
+ filters = os.path.join(dir,'filters')
3645
+ if os.path.isdir(filters):
3646
+ for f in os.listdir(filters):
3647
+ if re.match(r'^.+\.conf$',f):
3648
+ self.load(f,filters)
3649
+
3650
+ def load_miscellaneous(self,d):
3651
+ """Set miscellaneous configuration entries from dictionary 'd'."""
3652
+ def set_misc(name,rule='True',intval=False):
3653
+ if d.has_key(name):
3654
+ errmsg = 'illegal [miscellaneous] %s entry' % name
3655
+ if intval:
3656
+ setattr(self, name, int(validate(d[name],rule,errmsg)))
3657
+ else:
3658
+ setattr(self, name, validate(d[name],rule,errmsg))
3659
+ set_misc('tabsize','int($)>0',intval=True)
3660
+ set_misc('textwidth','int($)>0',intval=True)
3661
+ set_misc('pagewidth','int($)>0',intval=True)
3662
+ set_misc('pageunits')
3663
+ set_misc('outfilesuffix')
3664
+ if d.has_key('newline'):
3665
+ # Convert escape sequences to their character values.
3666
+ self.newline = eval('"'+d['newline']+'"')
3667
+ if d.has_key('subsnormal'):
3668
+ self.subsnormal = parse_options(d['subsnormal'],SUBS_OPTIONS,
3669
+ 'illegal [%s] %s: %s' %
3670
+ ('miscellaneous','subsnormal',d['subsnormal']))
3671
+ if d.has_key('subsverbatim'):
3672
+ self.subsverbatim = parse_options(d['subsverbatim'],SUBS_OPTIONS,
3673
+ 'illegal [%s] %s: %s' %
3674
+ ('miscellaneous','subsverbatim',d['subsverbatim']))
3675
+
3676
+ def validate(self):
3677
+ """Check the configuration for internal consistancy. Called after all
3678
+ configuration files have been loaded."""
3679
+ # Heuristic validate that at least one configuration file was loaded.
3680
+ if not self.specialchars or not self.tags or not lists:
3681
+ raise EAsciiDoc,'incomplete configuration files'
3682
+ # Check special characters are only one character long.
3683
+ for k in self.specialchars.keys():
3684
+ if len(k) != 1:
3685
+ raise EAsciiDoc,'[specialcharacters] ' \
3686
+ 'must be a single character: %s' % k
3687
+ # Check all special words have a corresponding inline macro body.
3688
+ for macro in self.specialwords.values():
3689
+ if not is_name(macro):
3690
+ raise EAsciiDoc,'illegal special word name: %s' % macro
3691
+ if not self.sections.has_key(macro):
3692
+ warning('missing special word macro: [%s]' % macro)
3693
+ # Check all text quotes have a corresponding tag.
3694
+ for q in self.quotes.keys():
3695
+ tag = self.quotes[q]
3696
+ if not tag:
3697
+ del self.quotes[q] # Undefine quote.
3698
+ else:
3699
+ if tag[0] == '#':
3700
+ tag = tag[1:]
3701
+ if not self.tags.has_key(tag):
3702
+ warning('[quotes] %s missing tag definition: %s' % (q,tag))
3703
+ # Check all specialsections section names exist.
3704
+ for k,v in self.specialsections.items():
3705
+ if not self.sections.has_key(v):
3706
+ warning('[%s] missing specialsections section' % v)
3707
+ paragraphs.validate()
3708
+ lists.validate()
3709
+ blocks.validate()
3710
+ tables.validate()
3711
+ macros.validate()
3712
+
3713
+ def is_special_section(self,section_name):
3714
+ for name in self.SPECIAL_SECTIONS:
3715
+ if re.match(name,section_name):
3716
+ return True
3717
+ return False
3718
+
3719
+ def dump(self):
3720
+ """Dump configuration to stdout."""
3721
+ # Header.
3722
+ hdr = ''
3723
+ hdr = hdr + '#' + writer.newline
3724
+ hdr = hdr + '# Generated by AsciiDoc %s for %s %s.%s' % \
3725
+ (VERSION,document.backend,document.doctype,writer.newline)
3726
+ t = time.asctime(time.localtime(time.time()))
3727
+ hdr = hdr + '# %s%s' % (t,writer.newline)
3728
+ hdr = hdr + '#' + writer.newline
3729
+ sys.stdout.write(hdr)
3730
+ # Dump special sections.
3731
+ # Dump only the configuration file and command-line attributes.
3732
+ # [miscellanous] entries are dumped as part of the [attributes].
3733
+ d = {}
3734
+ d.update(self.conf_attrs)
3735
+ d.update(self.cmd_attrs)
3736
+ dump_section('attributes',d)
3737
+ Title.dump()
3738
+ dump_section('quotes',self.quotes)
3739
+ dump_section('specialcharacters',self.specialchars)
3740
+ d = {}
3741
+ for k,v in self.specialwords.items():
3742
+ if d.has_key(v):
3743
+ d[v] = '%s "%s"' % (d[v],k) # Append word list.
3744
+ else:
3745
+ d[v] = '"%s"' % k
3746
+ dump_section('specialwords',d)
3747
+ dump_section('replacements',self.replacements)
3748
+ dump_section('replacements2',self.replacements2)
3749
+ dump_section('specialsections',self.specialsections)
3750
+ d = {}
3751
+ for k,v in self.tags.items():
3752
+ d[k] = '%s|%s' % v
3753
+ dump_section('tags',d)
3754
+ paragraphs.dump()
3755
+ lists.dump()
3756
+ blocks.dump()
3757
+ tables.dump()
3758
+ macros.dump()
3759
+ # Dump remaining sections.
3760
+ for k in self.sections.keys():
3761
+ if not self.is_special_section(k):
3762
+ sys.stdout.write('[%s]%s' % (k,writer.newline))
3763
+ for line in self.sections[k]:
3764
+ sys.stdout.write('%s%s' % (line,writer.newline))
3765
+ sys.stdout.write(writer.newline)
3766
+
3767
+ def subs_section(self,section,d):
3768
+ """Section attribute substitution using attributes from
3769
+ document.attributes and 'd'. Lines containing undefinded
3770
+ attributes are deleted."""
3771
+ if self.sections.has_key(section):
3772
+ return subs_attrs(self.sections[section],d)
3773
+ else:
3774
+ warning('missing [%s] section' % section)
3775
+ return ()
3776
+
3777
+ def parse_tags(self):
3778
+ """Parse [tags] section entries into self.tags dictionary."""
3779
+ d = {}
3780
+ parse_entries(self.sections.get('tags',()),d)
3781
+ for k,v in d.items():
3782
+ if v is None:
3783
+ if self.tags.has_key(k):
3784
+ del self.tags[k]
3785
+ elif v == 'none':
3786
+ self.tags[k] = (None,None)
3787
+ else:
3788
+ mo = re.match(r'(?P<stag>.*)\|(?P<etag>.*)',v)
3789
+ if mo:
3790
+ self.tags[k] = (mo.group('stag'), mo.group('etag'))
3791
+ else:
3792
+ raise EAsciiDoc,'[tag] %s value malformed' % k
3793
+
3794
+ def tag(self, name, d=None):
3795
+ """Returns (starttag,endtag) tuple named name from configuration file
3796
+ [tags] section. Raise error if not found. If a dictionary 'd' is
3797
+ passed then merge with document attributes and perform attribute
3798
+ substitution on tags."""
3799
+
3800
+ # TODO: Tags should be stored a single string, not split into start
3801
+ # and end tags since most are going to be substituted anyway (see
3802
+ # subs_tag() for how we should process them. parse_tags() (above)
3803
+ # should only validate i.e. parse_check(). This routine should be renamed
3804
+ # split_tag() and would call subs_tag(). self.tags dictionary values
3805
+ # would be strings not tuples.
3806
+
3807
+ if not self.tags.has_key(name):
3808
+ raise EAsciiDoc, 'missing tag: %s' % name
3809
+ stag,etag = self.tags[name]
3810
+ if d is not None:
3811
+ # TODO: Should we warn if substitution drops a tag?
3812
+ if stag:
3813
+ stag = subs_attrs(stag,d)
3814
+ if etag:
3815
+ etag = subs_attrs(etag,d)
3816
+ if stag is None: stag = ''
3817
+ if etag is None: etag = ''
3818
+ return (stag,etag)
3819
+
3820
+ def parse_specialsections(self):
3821
+ """Parse specialsections section to self.specialsections dictionary."""
3822
+ # TODO: This is virtually the same as parse_replacements() and should
3823
+ # be factored to single routine.
3824
+ d = {}
3825
+ parse_entries(self.sections.get('specialsections',()),d,unquote=True)
3826
+ for pat,sectname in d.items():
3827
+ pat = strip_quotes(pat)
3828
+ if not is_regexp(pat):
3829
+ raise EAsciiDoc,'[specialsections] entry ' \
3830
+ 'is not a valid regular expression: %s' % pat
3831
+ if sectname is None:
3832
+ if self.specialsections.has_key(pat):
3833
+ del self.specialsections[pat]
3834
+ else:
3835
+ self.specialsections[pat] = sectname
3836
+
3837
+ def parse_replacements(self,sect='replacements'):
3838
+ """Parse replacements section into self.replacements dictionary."""
3839
+ replacements = getattr(self,sect)
3840
+ d = OrderedDict()
3841
+ parse_entries(self.sections.get(sect,()), d, unquote=True)
3842
+ for pat,rep in d.items():
3843
+ pat = strip_quotes(pat)
3844
+ if not is_regexp(pat):
3845
+ raise EAsciiDoc,'[%s] entry in %s is not a valid' \
3846
+ ' regular expression: %s' % (sect,self.fname,pat)
3847
+ if rep is None:
3848
+ if replacements.has_key(pat):
3849
+ del replacements[pat]
3850
+ else:
3851
+ replacements[pat] = strip_quotes(rep)
3852
+
3853
+ def subs_replacements(self,s,sect='replacements'):
3854
+ """Substitute patterns from self.replacements in 's'."""
3855
+ result = s
3856
+ for pat,rep in getattr(self,sect).items():
3857
+ result = re.sub(pat, rep, result)
3858
+ return result
3859
+
3860
+ def parse_specialwords(self):
3861
+ """Parse special words section into self.specialwords dictionary."""
3862
+ reo = re.compile(r'(?:\s|^)(".+?"|[^"\s]+)(?=\s|$)')
3863
+ for line in self.sections.get('specialwords',()):
3864
+ e = parse_entry(line)
3865
+ if not e:
3866
+ raise EAsciiDoc,'[specialwords] entry in %s is malformed: %s' \
3867
+ % (self.fname,line)
3868
+ name,wordlist = e
3869
+ if not is_name(name):
3870
+ raise EAsciiDoc,'[specialwords] name in %s is illegal: %s' \
3871
+ % (self.fname,name)
3872
+ if wordlist is None:
3873
+ # Undefine all words associated with 'name'.
3874
+ for k,v in self.specialwords.items():
3875
+ if v == name:
3876
+ del self.specialwords[k]
3877
+ else:
3878
+ words = reo.findall(wordlist)
3879
+ for word in words:
3880
+ word = strip_quotes(word)
3881
+ if not is_regexp(word):
3882
+ raise EAsciiDoc,'[specialwords] entry in %s ' \
3883
+ 'is not a valid regular expression: %s' \
3884
+ % (self.fname,word)
3885
+ self.specialwords[word] = name
3886
+
3887
+ def subs_specialchars(self,s):
3888
+ """Perform special character substitution on string 's'."""
3889
+ """It may seem like a good idea to escape special characters with a '\'
3890
+ character, the reason we don't is because the escape character itself
3891
+ then has to be escaped and this makes including code listings
3892
+ problematic. Use the predefined {amp},{lt},{gt} attributes instead."""
3893
+ result = ''
3894
+ for ch in s:
3895
+ result = result + self.specialchars.get(ch,ch)
3896
+ return result
3897
+
3898
+ def subs_specialchars_reverse(self,s):
3899
+ """Perform reverse special character substitution on string 's'."""
3900
+ result = s
3901
+ for k,v in self.specialchars.items():
3902
+ result = result.replace(v, k)
3903
+ return result
3904
+
3905
+ def subs_specialwords(self,s):
3906
+ """Search for word patterns from self.specialwords in 's' and
3907
+ substitute using corresponding macro."""
3908
+ result = s
3909
+ for word in self.specialwords.keys():
3910
+ result = re.sub(word, _subs_specialwords, result)
3911
+ return result
3912
+
3913
+ def expand_templates(self,section):
3914
+ result = []
3915
+ for line in self.sections[section]:
3916
+ mo = macros.match('+',r'template',line)
3917
+ if mo:
3918
+ s = mo.group('attrlist')
3919
+ if self.sections.has_key(s):
3920
+ result += self.sections[s]
3921
+ else:
3922
+ warning('missing [%s] section' % s)
3923
+ else:
3924
+ result.append(line)
3925
+ return result
3926
+
3927
+ def expand_all_templates(self):
3928
+ for k in self.sections.keys():
3929
+ self.sections[k] = self.expand_templates(k)
3930
+
3931
+ def section2tags(self, section, d={}):
3932
+ """Perform attribute substitution on 'section' using document
3933
+ attributes plus 'd' attributes. Return tuple (stag,etag) containing
3934
+ pre and post | placeholder tags."""
3935
+ assert section is not None
3936
+ if self.sections.has_key(section):
3937
+ body = self.sections[section]
3938
+ else:
3939
+ warning('missing [%s] section' % section)
3940
+ body = ()
3941
+ # Split macro body into start and end tag lists.
3942
+ stag = []
3943
+ etag = []
3944
+ in_stag = True
3945
+ for s in body:
3946
+ if in_stag:
3947
+ mo = re.match(r'(?P<stag>.*)\|(?P<etag>.*)',s)
3948
+ if mo:
3949
+ if mo.group('stag'):
3950
+ stag.append(mo.group('stag'))
3951
+ if mo.group('etag'):
3952
+ etag.append(mo.group('etag'))
3953
+ in_stag = False
3954
+ else:
3955
+ stag.append(s)
3956
+ else:
3957
+ etag.append(s)
3958
+ # Do attribute substitution last so {brkbar} can be used to escape |.
3959
+ # But don't do attribute substitution on title -- we've already done it.
3960
+ title = d.get('title')
3961
+ if title:
3962
+ d['title'] = chr(0) # Replace with unused character.
3963
+ stag = subs_attrs(stag, d)
3964
+ etag = subs_attrs(etag, d)
3965
+ # Put the {title} back.
3966
+ if title:
3967
+ stag = map(lambda x: x.replace(chr(0), title), stag)
3968
+ etag = map(lambda x: x.replace(chr(0), title), etag)
3969
+ d['title'] = title
3970
+ return (stag,etag)
3971
+
3972
+
3973
+ #---------------------------------------------------------------------------
3974
+ # Application code.
3975
+ #---------------------------------------------------------------------------
3976
+ # Constants
3977
+ # ---------
3978
+ APP_DIR = None # This file's directory.
3979
+ USER_DIR = None # ~/.asciidoc
3980
+ CONF_DIR = '/etc/asciidoc' # Global configuration files directory.
3981
+ HELP_FILE = 'help.conf' # Default (English) help file.
3982
+
3983
+ # Globals
3984
+ # -------
3985
+ document = Document() # The document being processed.
3986
+ config = Config() # Configuration file reader.
3987
+ reader = Reader() # Input stream line reader.
3988
+ writer = Writer() # Output stream line writer.
3989
+ paragraphs = Paragraphs() # Paragraph definitions.
3990
+ lists = Lists() # List definitions.
3991
+ blocks = DelimitedBlocks() # DelimitedBlock definitions.
3992
+ tables = Tables() # Table definitions.
3993
+ macros = Macros() # Macro definitions.
3994
+ calloutmap = CalloutMap() # Coordinates callouts and callout list.
3995
+
3996
+ def asciidoc(backend, doctype, confiles, infile, outfile, options):
3997
+ """Convert AsciiDoc document to DocBook document of type doctype
3998
+ The AsciiDoc document is read from file object src the translated
3999
+ DocBook file written to file object dst."""
4000
+ try:
4001
+ if doctype not in ('article','manpage','book'):
4002
+ raise EAsciiDoc,'illegal document type'
4003
+ if backend == 'linuxdoc' and doctype != 'article':
4004
+ raise EAsciiDoc,'%s %s documents are not supported' \
4005
+ % (backend,doctype)
4006
+ document.backend = backend
4007
+ if not os.path.exists(os.path.join(APP_DIR, backend+'.conf')) and not \
4008
+ os.path.exists(os.path.join(CONF_DIR, backend+'.conf')):
4009
+ warning('non-standard %s backend' % backend, linenos=False)
4010
+ document.doctype = doctype
4011
+ document.infile = infile
4012
+ document.init_attrs()
4013
+ # Set processing options.
4014
+ for o in options:
4015
+ if o == '-c': config.dumping = True
4016
+ if o == '-s': config.header_footer = False
4017
+ if o == '-v': config.verbose = True
4018
+ # Check the infile exists.
4019
+ if infile != '<stdin>' and not os.path.isfile(infile):
4020
+ raise EAsciiDoc,'input file %s missing' % infile
4021
+ if '-e' not in options:
4022
+ # Load global configuration from system configuration directory.
4023
+ config.load_all(CONF_DIR)
4024
+ # Load global configuration files from asciidoc directory.
4025
+ config.load_all(APP_DIR)
4026
+ # Load configuration files from ~/.asciidoc if it exists.
4027
+ if USER_DIR is not None:
4028
+ config.load_all(USER_DIR)
4029
+ # Load configuration files from document directory.
4030
+ if infile != '<stdin>':
4031
+ config.load_all(os.path.dirname(infile))
4032
+ if infile != '<stdin>':
4033
+ # Load implicit document specific configuration files if they exist.
4034
+ config.load(os.path.splitext(infile)[0] + '.conf')
4035
+ config.load(os.path.splitext(infile)[0] + '-' + backend + '.conf')
4036
+ # If user specified configuration file(s) overlay the defaults.
4037
+ if confiles:
4038
+ for conf in confiles:
4039
+ if os.path.isfile(conf):
4040
+ config.load(conf)
4041
+ else:
4042
+ raise EAsciiDoc,'configuration file %s missing' % conf
4043
+ document.init_attrs() # Add conf files.
4044
+ # Check configuration for consistency.
4045
+ config.validate()
4046
+ # Build outfile name now all conf files have been read.
4047
+ if outfile is None:
4048
+ outfile = os.path.splitext(infile)[0] + '.' + backend
4049
+ if config.outfilesuffix:
4050
+ # Change file extension.
4051
+ outfile = os.path.splitext(outfile)[0] + config.outfilesuffix
4052
+ document.outfile = outfile
4053
+ if config.dumping:
4054
+ config.dump()
4055
+ else:
4056
+ reader.tabsize = config.tabsize
4057
+ reader.open(infile)
4058
+ try:
4059
+ writer.newline = config.newline
4060
+ writer.open(outfile)
4061
+ try:
4062
+ document.init_attrs() # Add file name related entries.
4063
+ document.translate()
4064
+ finally:
4065
+ writer.close()
4066
+ finally:
4067
+ reader.closefile() # Keep reader state for postmortem.
4068
+ except (KeyboardInterrupt, SystemExit):
4069
+ print
4070
+ except Exception,e:
4071
+ # Cleanup.
4072
+ if outfile and outfile != '<stdout>' and os.path.isfile(outfile):
4073
+ os.unlink(outfile)
4074
+ # Build and print error description.
4075
+ msg = 'FAILED: '
4076
+ if reader.cursor:
4077
+ msg = msg + '%s: line %d: ' % (reader.cursor[0],reader.cursor[1])
4078
+ if isinstance(e,EAsciiDoc):
4079
+ print_stderr(msg+str(e))
4080
+ else:
4081
+ print_stderr(msg+'unexpected error:')
4082
+ print_stderr('-'*60)
4083
+ traceback.print_exc(file=sys.stderr)
4084
+ print_stderr('-'*60)
4085
+ sys.exit(1)
4086
+
4087
+ def usage(msg=''):
4088
+ if msg:
4089
+ print_stderr(msg)
4090
+ show_help('default', sys.stderr)
4091
+
4092
+ def show_help(topic, stream=sys.stdout):
4093
+ """Print help topic to stdout."""
4094
+ # Select help file.
4095
+ lang = config.cmd_attrs.get('lang')
4096
+ if lang and lang != 'en':
4097
+ help_file = 'help-' + lang + '.conf'
4098
+ else:
4099
+ help_file = HELP_FILE
4100
+ # Print [topic] section from help file.
4101
+ topics = OrderedDict()
4102
+ load_sections(topics, help_file, CONF_DIR)
4103
+ load_sections(topics, help_file, APP_DIR)
4104
+ if USER_DIR is not None:
4105
+ load_sections(topics, help_file, USER_DIR)
4106
+ if len(topics) == 0:
4107
+ # Default to English if specified language help files not found.
4108
+ help_file = HELP_FILE
4109
+ load_sections(topics, help_file, CONF_DIR)
4110
+ load_sections(topics, help_file, APP_DIR)
4111
+ if len(topics) == 0:
4112
+ print_stderr('no help topics found')
4113
+ sys.exit(1)
4114
+ n = 0
4115
+ for k in topics.keys():
4116
+ if re.match(re.escape(topic), k):
4117
+ n += 1
4118
+ lines = topics[k]
4119
+ if n == 0:
4120
+ print_stderr('help topic not found: [%s] in %s' % (topic, help_file))
4121
+ print_stderr('available help topics: %s' % ', '.join(topics.keys()))
4122
+ sys.exit(1)
4123
+ elif n > 1:
4124
+ print_stderr('ambiguous help topic: %s' % topic)
4125
+ else:
4126
+ for line in lines:
4127
+ print >>stream, line
4128
+
4129
+ def main():
4130
+ if float(sys.version[:3]) < 2.3:
4131
+ print_stderr('FAILED: Python 2.3 or better required.')
4132
+ sys.exit(1)
4133
+ # Locate the executable and configuration files directory.
4134
+ global APP_DIR,USER_DIR
4135
+ APP_DIR = os.path.dirname(os.path.realpath(sys.argv[0]))
4136
+ USER_DIR = os.environ.get('HOME')
4137
+ if USER_DIR is not None:
4138
+ USER_DIR = os.path.join(USER_DIR,'.asciidoc')
4139
+ if not os.path.isdir(USER_DIR):
4140
+ USER_DIR = None
4141
+ # Process command line options.
4142
+ import getopt
4143
+ try:
4144
+ #DEPRECATED: --safe option.
4145
+ opts,args = getopt.getopt(sys.argv[1:],
4146
+ 'a:b:cd:ef:hno:svw:',
4147
+ ['attribute=','backend=','conf-file=','doctype=','dump-conf',
4148
+ 'help','no-conf','no-header-footer','out-file=','profile',
4149
+ 'section-numbers','verbose','version','safe','unsafe'])
4150
+ except getopt.GetoptError:
4151
+ usage()
4152
+ sys.exit(1)
4153
+ if len(args) > 1:
4154
+ usage()
4155
+ sys.exit(1)
4156
+ backend = DEFAULT_BACKEND
4157
+ doctype = DEFAULT_DOCTYPE
4158
+ confiles = []
4159
+ outfile = None
4160
+ options = []
4161
+ prof = False
4162
+ help_option = False
4163
+ for o,v in opts:
4164
+ if o in ('--help','-h'):
4165
+ help_option = True
4166
+ if o == '--profile':
4167
+ prof = True
4168
+ if o == '--unsafe':
4169
+ document.safe = False
4170
+ if o == '--version':
4171
+ print('asciidoc %s' % VERSION)
4172
+ sys.exit(0)
4173
+ if o in ('-b','--backend'):
4174
+ backend = v
4175
+ if o in ('-c','--dump-conf'):
4176
+ options.append('-c')
4177
+ if o in ('-d','--doctype'):
4178
+ doctype = v
4179
+ if o in ('-e','--no-conf'):
4180
+ options.append('-e')
4181
+ if o in ('-f','--conf-file'):
4182
+ confiles.append(v)
4183
+ if o in ('-n','--section-numbers'):
4184
+ o = '-a'
4185
+ v = 'numbered'
4186
+ if o in ('-a','--attribute'):
4187
+ e = parse_entry(v, allow_name_only=True)
4188
+ if not e:
4189
+ usage('Illegal -a option: %s' % v)
4190
+ sys.exit(1)
4191
+ k,v = e
4192
+ # A @ suffix denotes don't override existing document attributes.
4193
+ if v and v[-1] == '@':
4194
+ document.attributes[k] = v[:-1]
4195
+ else:
4196
+ config.cmd_attrs[k] = v
4197
+ if o in ('-o','--out-file'):
4198
+ if v == '-':
4199
+ outfile = '<stdout>'
4200
+ else:
4201
+ outfile = v
4202
+ if o in ('-s','--no-header-footer'):
4203
+ options.append('-s')
4204
+ if o in ('-v','--verbose'):
4205
+ options.append('-v')
4206
+ if help_option:
4207
+ if len(args) == 0:
4208
+ show_help('default')
4209
+ else:
4210
+ show_help(args[-1])
4211
+ sys.exit(0)
4212
+ if len(args) == 0 and len(opts) == 0:
4213
+ usage()
4214
+ sys.exit(1)
4215
+ if len(args) == 0:
4216
+ usage('No source file specified')
4217
+ sys.exit(1)
4218
+ if not backend:
4219
+ usage('No --backend option specified')
4220
+ sys.exit(1)
4221
+ if args[0] == '-':
4222
+ infile = '<stdin>'
4223
+ else:
4224
+ infile = args[0]
4225
+ if infile == '<stdin>' and not outfile:
4226
+ outfile = '<stdout>'
4227
+ # Convert in and out files to absolute paths.
4228
+ if infile != '<stdin>':
4229
+ infile = os.path.abspath(infile)
4230
+ if outfile and outfile != '<stdout>':
4231
+ outfile = os.path.abspath(outfile)
4232
+ # Do the work.
4233
+ if prof:
4234
+ import profile
4235
+ profile.run("asciidoc('%s','%s',(),'%s',None,())"
4236
+ % (backend,doctype,infile))
4237
+ else:
4238
+ asciidoc(backend, doctype, confiles, infile, outfile, options)
4239
+ if document.has_errors:
4240
+ sys.exit(1)
4241
+
4242
+ if __name__ == '__main__':
4243
+ try:
4244
+ main()
4245
+ except KeyboardInterrupt:
4246
+ pass
4247
+ except SystemExit:
4248
+ raise
4249
+ except:
4250
+ print_stderr('%s: unexpected error: %s' %
4251
+ (os.path.basename(sys.argv[0]), sys.exc_info()[1]))
4252
+ print_stderr('-'*60)
4253
+ traceback.print_exc(file=sys.stderr)
4254
+ print_stderr('-'*60)
4255
+ sys.exit(1)