FooBarWidget-mizuho 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. data/README.markdown +1 -1
  2. data/Rakefile +14 -2
  3. data/asciidoc/BUGS +11 -6
  4. data/asciidoc/BUGS.txt +7 -3
  5. data/asciidoc/CHANGELOG +313 -151
  6. data/asciidoc/CHANGELOG.txt +177 -12
  7. data/asciidoc/INSTALL +30 -36
  8. data/asciidoc/INSTALL.txt +20 -20
  9. data/asciidoc/Makefile.in +145 -0
  10. data/asciidoc/README +11 -11
  11. data/asciidoc/README.txt +9 -9
  12. data/asciidoc/a2x +40 -7
  13. data/asciidoc/asciidoc.conf +180 -126
  14. data/asciidoc/asciidoc.py +1667 -977
  15. data/asciidoc/common.aap +2 -2
  16. data/asciidoc/configure +2840 -0
  17. data/asciidoc/configure.ac +11 -0
  18. data/asciidoc/dblatex/asciidoc-dblatex.sty +2 -0
  19. data/asciidoc/dblatex/asciidoc-dblatex.xsl +15 -0
  20. data/asciidoc/dblatex/dblatex-readme.txt +19 -2
  21. data/asciidoc/doc/a2x.1 +77 -67
  22. data/asciidoc/doc/a2x.1.txt +11 -2
  23. data/asciidoc/doc/article.css-embedded.html +85 -63
  24. data/asciidoc/doc/article.html +644 -62
  25. data/asciidoc/doc/article.pdf +0 -0
  26. data/asciidoc/doc/article.txt +15 -17
  27. data/asciidoc/doc/asciidoc.1 +34 -40
  28. data/asciidoc/doc/asciidoc.1.css-embedded.html +67 -32
  29. data/asciidoc/doc/asciidoc.1.css.html +28 -28
  30. data/asciidoc/doc/asciidoc.1.html +33 -33
  31. data/asciidoc/doc/asciidoc.css-embedded.html +2234 -2348
  32. data/asciidoc/doc/asciidoc.css.html +2203 -2352
  33. data/asciidoc/doc/asciidoc.dict +52 -1
  34. data/asciidoc/doc/asciidoc.html +980 -1160
  35. data/asciidoc/doc/asciidoc.txt +941 -738
  36. data/asciidoc/doc/asciimathml.txt +63 -0
  37. data/asciidoc/doc/book-multi.html +26 -43
  38. data/asciidoc/doc/book-multi.txt +19 -23
  39. data/asciidoc/doc/book.css-embedded.html +92 -71
  40. data/asciidoc/doc/book.html +24 -41
  41. data/asciidoc/doc/book.txt +19 -21
  42. data/asciidoc/doc/docbook-xsl.css +1 -0
  43. data/asciidoc/doc/faq.txt +288 -60
  44. data/asciidoc/doc/images +1 -0
  45. data/asciidoc/doc/latex-backend.html +16 -123
  46. data/asciidoc/doc/latex-backend.txt +17 -19
  47. data/asciidoc/doc/latexmath.txt +233 -24
  48. data/asciidoc/doc/latexmathml.txt +41 -0
  49. data/asciidoc/doc/main.aap +9 -5
  50. data/asciidoc/doc/music-filter.pdf +0 -0
  51. data/asciidoc/doc/music-filter.txt +2 -2
  52. data/asciidoc/doc/source-highlight-filter.html +476 -105
  53. data/asciidoc/doc/source-highlight-filter.pdf +0 -0
  54. data/asciidoc/doc/source-highlight-filter.txt +39 -10
  55. data/asciidoc/docbook-xsl/asciidoc-docbook-xsl.txt +1 -29
  56. data/asciidoc/docbook-xsl/fo.xsl +35 -3
  57. data/asciidoc/docbook-xsl/manpage.xsl +3 -0
  58. data/asciidoc/docbook-xsl/text.xsl +50 -0
  59. data/asciidoc/docbook.conf +182 -73
  60. data/asciidoc/examples/website/ASCIIMathML.js +1 -0
  61. data/asciidoc/examples/website/CHANGELOG.html +618 -182
  62. data/asciidoc/examples/website/CHANGELOG.txt +1 -0
  63. data/asciidoc/examples/website/INSTALL.html +34 -36
  64. data/asciidoc/examples/website/INSTALL.txt +1 -0
  65. data/asciidoc/examples/website/LaTeXMathML.js +1 -0
  66. data/asciidoc/examples/website/README-website.html +26 -37
  67. data/asciidoc/examples/website/README-website.txt +6 -6
  68. data/asciidoc/examples/website/README.html +15 -15
  69. data/asciidoc/examples/website/README.txt +1 -0
  70. data/asciidoc/examples/website/a2x.1.html +74 -50
  71. data/asciidoc/examples/website/a2x.1.txt +1 -0
  72. data/asciidoc/examples/website/asciidoc-docbook-xsl.html +13 -48
  73. data/asciidoc/examples/website/asciidoc-docbook-xsl.txt +1 -0
  74. data/asciidoc/examples/website/asciimathml.txt +1 -0
  75. data/asciidoc/examples/website/customers.csv +1 -0
  76. data/asciidoc/examples/website/downloads.html +69 -31
  77. data/asciidoc/examples/website/downloads.txt +28 -5
  78. data/asciidoc/examples/website/faq.html +370 -124
  79. data/asciidoc/examples/website/faq.txt +1 -0
  80. data/asciidoc/examples/website/images +1 -0
  81. data/asciidoc/examples/website/index.html +64 -64
  82. data/asciidoc/examples/website/index.txt +22 -15
  83. data/asciidoc/examples/website/latex-backend.html +152 -257
  84. data/asciidoc/examples/website/latex-backend.txt +1 -0
  85. data/asciidoc/examples/website/latexmathml.txt +1 -0
  86. data/asciidoc/examples/website/manpage.html +27 -27
  87. data/asciidoc/examples/website/manpage.txt +1 -0
  88. data/asciidoc/examples/website/music-filter.html +18 -18
  89. data/asciidoc/examples/website/music-filter.txt +1 -0
  90. data/asciidoc/examples/website/music1.abc +1 -1
  91. data/asciidoc/examples/website/music1.png +0 -0
  92. data/asciidoc/examples/website/music2.ly +1 -1
  93. data/asciidoc/examples/website/music2.png +0 -0
  94. data/asciidoc/examples/website/newlists.txt +40 -0
  95. data/asciidoc/examples/website/newtables.txt +397 -0
  96. data/asciidoc/examples/website/source-highlight-filter.html +67 -32
  97. data/asciidoc/examples/website/source-highlight-filter.txt +1 -0
  98. data/asciidoc/examples/website/support.html +4 -4
  99. data/asciidoc/examples/website/toc.js +1 -0
  100. data/asciidoc/examples/website/userguide.html +2190 -2339
  101. data/asciidoc/examples/website/userguide.txt +1 -0
  102. data/asciidoc/examples/website/version83.txt +37 -0
  103. data/asciidoc/examples/website/version9.html +13 -13
  104. data/asciidoc/examples/website/xhtml11-manpage.css +1 -0
  105. data/asciidoc/examples/website/xhtml11-quirks.css +1 -0
  106. data/asciidoc/examples/website/xhtml11.css +1 -0
  107. data/asciidoc/filters/code-filter-readme.txt +3 -3
  108. data/asciidoc/filters/code-filter-test.txt +6 -6
  109. data/asciidoc/filters/source-highlight-filter.conf +12 -5
  110. data/asciidoc/html4.conf +152 -58
  111. data/asciidoc/install-sh +201 -0
  112. data/asciidoc/latex.conf +41 -41
  113. data/asciidoc/stylesheets/docbook-xsl.css +1 -0
  114. data/asciidoc/stylesheets/xhtml11.css +39 -4
  115. data/asciidoc/text.conf +4 -4
  116. data/asciidoc/vim/syntax/asciidoc.vim +58 -32
  117. data/asciidoc/wordpress.conf +48 -0
  118. data/asciidoc/xhtml11-quirks.conf +1 -1
  119. data/asciidoc/xhtml11.conf +198 -70
  120. data/bin/mizuho +5 -2
  121. data/lib/mizuho/generator.rb +48 -19
  122. data/mizuho.gemspec +16 -6
  123. metadata +58 -15
  124. data/asciidoc/doc/asciimath.txt +0 -47
  125. data/asciidoc/docbook-xsl/shaded-literallayout.patch +0 -32
  126. data/asciidoc/examples/website/asciimath.html +0 -157
  127. data/asciidoc/examples/website/latexmath.html +0 -119
  128. data/asciidoc/filters/code-filter-test-c++.txt +0 -7
  129. data/asciidoc/install.sh +0 -55
  130. data/asciidoc/linuxdoc.conf +0 -285
  131. data/asciidoc/math.conf +0 -50
  132. data/asciidoc/stylesheets/xhtml-deprecated-manpage.css +0 -21
  133. data/asciidoc/stylesheets/xhtml-deprecated.css +0 -247
  134. data/asciidoc/t.conf +0 -20
  135. data/asciidoc/xhtml-deprecated-css.conf +0 -235
  136. data/asciidoc/xhtml-deprecated.conf +0 -351
data/asciidoc/asciidoc.py CHANGED
@@ -6,10 +6,10 @@ Copyright (C) 2002-2008 Stuart Rackham. Free use of this software is granted
6
6
  under the terms of the GNU General Public License (GPL).
7
7
  """
8
8
 
9
- import sys, os, re, time, traceback, tempfile, popen2, codecs, locale
9
+ import sys, os, re, time, traceback, tempfile, subprocess, codecs, locale
10
10
  from types import *
11
11
 
12
- VERSION = '8.2.7' # See CHANGLOG file for version history.
12
+ VERSION = '8.3.1' # See CHANGLOG file for version history.
13
13
 
14
14
  #---------------------------------------------------------------------------
15
15
  # Program onstants.
@@ -20,10 +20,10 @@ DEFAULT_DOCTYPE = 'article'
20
20
  # definition subs entry.
21
21
  SUBS_OPTIONS = ('specialcharacters','quotes','specialwords',
22
22
  'replacements', 'attributes','macros','callouts','normal','verbatim',
23
- 'none','passthroughs','replacements2')
23
+ 'none','replacements2')
24
24
  # Default value for unspecified subs and presubs configuration file entries.
25
25
  SUBS_NORMAL = ('specialcharacters','quotes','attributes',
26
- 'specialwords','replacements','macros','passthroughs')
26
+ 'specialwords','replacements','macros')
27
27
  SUBS_VERBATIM = ('specialcharacters','callouts')
28
28
 
29
29
  NAME_RE = r'(?u)[^\W\d][-\w]*' # Valid section or attrbibute name.
@@ -33,32 +33,29 @@ NAME_RE = r'(?u)[^\W\d][-\w]*' # Valid section or attrbibute name.
33
33
  # Utility functions and classes.
34
34
  #---------------------------------------------------------------------------
35
35
 
36
- class EAsciiDoc(Exception):
37
- pass
36
+ class EAsciiDoc(Exception): pass
38
37
 
39
-
40
- from UserDict import UserDict
41
-
42
- class OrderedDict(UserDict):
38
+ class OrderedDict(dict):
43
39
  """
44
40
  Dictionary ordered by insertion order.
45
41
  Python Cookbook: Ordered Dictionary, Submitter: David Benjamin.
46
42
  http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747
47
43
  """
48
- def __init__(self, d=None):
44
+ def __init__(self, d=None, **kwargs):
49
45
  self._keys = []
50
- UserDict.__init__(self, d)
46
+ if d is None: d = kwargs
47
+ dict.__init__(self, d)
51
48
  def __delitem__(self, key):
52
- UserDict.__delitem__(self, key)
49
+ dict.__delitem__(self, key)
53
50
  self._keys.remove(key)
54
51
  def __setitem__(self, key, item):
55
- UserDict.__setitem__(self, key, item)
52
+ dict.__setitem__(self, key, item)
56
53
  if key not in self._keys: self._keys.append(key)
57
54
  def clear(self):
58
- UserDict.clear(self)
55
+ dict.clear(self)
59
56
  self._keys = []
60
57
  def copy(self):
61
- d = UserDict.copy(self)
58
+ d = dict.copy(self)
62
59
  d._keys = self._keys[:]
63
60
  return d
64
61
  def items(self):
@@ -74,17 +71,38 @@ class OrderedDict(UserDict):
74
71
  del self[key]
75
72
  return (key, val)
76
73
  def setdefault(self, key, failobj = None):
77
- UserDict.setdefault(self, key, failobj)
74
+ dict.setdefault(self, key, failobj)
78
75
  if key not in self._keys: self._keys.append(key)
79
76
  def update(self, d=None, **kwargs):
80
77
  if d is None:
81
78
  d = kwargs
82
- UserDict.update(self, d)
79
+ dict.update(self, d)
83
80
  for key in d.keys():
84
81
  if key not in self._keys: self._keys.append(key)
85
82
  def values(self):
86
83
  return map(self.get, self._keys)
87
84
 
85
+ class AttrDict(dict):
86
+ """
87
+ Like a dictionary except values can be accessed as attributes i.e. obj.foo
88
+ can be used in addition to obj['foo'].
89
+ If an item is not present None is returned.
90
+ """
91
+ def __getattr__(self, key):
92
+ try: return self[key]
93
+ except KeyError, k: return None
94
+ def __setattr__(self, key, value):
95
+ self[key] = value
96
+ def __delattr__(self, key):
97
+ try: del self[key]
98
+ except KeyError, k: raise AttributeError, k
99
+ def __repr__(self):
100
+ return '<AttrDict ' + dict.__repr__(self) + '>'
101
+ def __getstate__(self):
102
+ return dict(self)
103
+ def __setstate__(self,value):
104
+ for k,v in value.items(): self[k]=v
105
+
88
106
  def print_stderr(line):
89
107
  sys.stderr.write(line+os.linesep)
90
108
 
@@ -96,25 +114,35 @@ def warning(msg,linenos=True):
96
114
  console(msg,'WARNING: ',linenos)
97
115
  document.has_warnings = True
98
116
 
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
117
+ def deprecated(msg, linenos=True):
118
+ console(msg, 'DEPRECATED: ', linenos)
107
119
 
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
120
+ def message(msg, prefix='', linenos=True, cursor=None):
121
+ """
122
+ Return formatted message string. 'offset' is added to reported line number
123
+ for warnings emitted when reading ahead.
124
+ """
112
125
  if linenos and reader.cursor:
113
126
  if not cursor:
114
127
  cursor = reader.cursor
115
- s = s + '%s: line %d: ' % (os.path.basename(cursor[0]),cursor[1])
116
- s = s + msg
117
- print_stderr(s)
128
+ prefix += '%s: line %d: ' % (os.path.basename(cursor[0]),cursor[1])
129
+ return prefix + msg
130
+
131
+ def error(msg, cursor=None, halt=False):
132
+ """
133
+ Report fatal error.
134
+ If halt=True raise EAsciiDoc exception.
135
+ If halt=False don't exit application, continue in the hope of reporting all
136
+ fatal errors finishing with a non-zero exit code.
137
+ """
138
+ if halt:
139
+ raise EAsciiDoc, message(msg,linenos=False,cursor=cursor)
140
+ else:
141
+ console(msg,'ERROR: ',cursor=cursor)
142
+ document.has_errors = True
143
+
144
+ def console(msg, prefix='', linenos=True, cursor=None):
145
+ print_stderr(message(msg,prefix,linenos,cursor))
118
146
 
119
147
  def file_in(fname, directory):
120
148
  """Return True if file fname resides inside directory."""
@@ -124,7 +152,7 @@ def file_in(fname, directory):
124
152
  directory = os.getcwd()
125
153
  else:
126
154
  assert os.path.isdir(directory)
127
- directory = os.path.abspath(directory)
155
+ directory = os.path.realpath(directory)
128
156
  fname = os.path.realpath(fname)
129
157
  return os.path.commonprefix((directory, fname)) == directory
130
158
 
@@ -142,9 +170,11 @@ def is_safe_file(fname, directory=None):
142
170
  directory = '.'
143
171
  return not safe() or file_in(fname, directory)
144
172
 
145
- # Return file name which must reside in the parent file directory.
146
- # Return None if file is not found or not safe.
147
173
  def safe_filename(fname, parentdir):
174
+ """
175
+ Return file name which must reside in the parent file directory.
176
+ Return None if file is not found or not safe.
177
+ """
148
178
  if not os.path.isabs(fname):
149
179
  # Include files are relative to parent document
150
180
  # directory.
@@ -160,16 +190,6 @@ def safe_filename(fname, parentdir):
160
190
  def unsafe_error(msg):
161
191
  error('unsafe: '+msg)
162
192
 
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
193
  def assign(dst,src):
174
194
  """Assign all attributes from 'src' object to 'dst' object."""
175
195
  for a,v in src.__dict__.items():
@@ -212,33 +232,50 @@ def validate(value,rule,errmsg):
212
232
  raise EAsciiDoc,errmsg
213
233
  return value
214
234
 
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
+ def lstrip_list(s):
236
+ """
237
+ Return list with empty items from start of list removed.
238
+ """
239
+ for i in range(len(s)):
240
+ if s[i]: break
241
+ else:
242
+ return []
243
+ return s[i:]
244
+
245
+ def rstrip_list(s):
246
+ """
247
+ Return list with empty items from end of list removed.
248
+ """
249
+ for i in range(len(s)-1,-1,-1):
250
+ if s[i]: break
251
+ else:
252
+ return []
253
+ return s[:i+1]
254
+
255
+ def strip_list(s):
256
+ """
257
+ Return list with empty items from start and end of list removed.
258
+ """
259
+ s = lstrip_list(s)
260
+ s = rstrip_list(s)
261
+ return s
262
+
263
+ def is_array(obj):
264
+ """
265
+ Return True if object is list or tuple type.
266
+ """
267
+ return isinstance(obj,list) or isinstance(obj,tuple)
235
268
 
236
269
  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)
270
+ """
271
+ Append list or tuple of strings 'lines2' to list 'lines1'. Join the last
272
+ non-blank item in 'lines1' with the first non-blank item in 'lines2' into a
273
+ single string.
274
+ """
275
+ assert is_array(lines1)
276
+ assert is_array(lines2)
277
+ lines1 = strip_list(lines1)
278
+ lines2 = strip_list(lines2)
242
279
  if not lines1 or not lines2:
243
280
  return list(lines1) + list(lines2)
244
281
  result = list(lines1[:-1])
@@ -286,10 +323,8 @@ def parse_attributes(attrs,dict):
286
323
  for v in d.values():
287
324
  if not (isinstance(v,str) or isinstance(v,int) or isinstance(v,float) or v is None):
288
325
  raise
289
- dict.update(d)
290
326
  except:
291
- # Try quoting the attrs.
292
- s = s.replace('"',r'\"') # Escape double-quotes.
327
+ s = s.replace('"','\\"')
293
328
  s = s.split(',')
294
329
  s = map(lambda x: '"' + x.strip() + '"', s)
295
330
  s = ','.join(s)
@@ -299,7 +334,7 @@ def parse_attributes(attrs,dict):
299
334
  return # If there's a syntax error leave with {0}=attrs.
300
335
  for k in d.keys(): # Drop any empty positional arguments.
301
336
  if d[k] == '': del d[k]
302
- dict.update(d)
337
+ dict.update(d)
303
338
  assert len(d) > 0
304
339
 
305
340
  def parse_named_attributes(s,attrs):
@@ -335,7 +370,7 @@ def parse_options(options,allowed,errmsg):
335
370
  result = []
336
371
  if options:
337
372
  for s in re.split(r'\s*,\s*',options):
338
- if (allowed and s not in allowed) or (s == '' or not is_name(s)):
373
+ if (allowed and s not in allowed) or not is_name(s):
339
374
  raise EAsciiDoc,'%s: %s' % (errmsg,s)
340
375
  result.append(s)
341
376
  return tuple(result)
@@ -347,16 +382,12 @@ def symbolize(s):
347
382
  def is_name(s):
348
383
  """Return True if s is valid attribute, macro or tag name
349
384
  (starts with alpha containing alphanumeric and dashes only)."""
350
- return re.match(NAME_RE,s) is not None
385
+ return re.match(r'^'+NAME_RE+r'$',s) is not None
351
386
 
352
387
  def subs_quotes(text):
353
388
  """Quoted text is marked up and the resulting text is
354
389
  returned."""
355
- # The quote patterns are iterated in reverse sort order to avoid ambiguity.
356
- # So, for example, __ is processed before _.
357
390
  keys = config.quotes.keys()
358
- keys.sort()
359
- keys.reverse()
360
391
  for q in keys:
361
392
  i = q.find('|')
362
393
  if i != -1 and q != '|' and q != '||':
@@ -369,41 +400,42 @@ def subs_quotes(text):
369
400
  if tag[0] == '#':
370
401
  tag = tag[1:]
371
402
  # Unconstrained quotes can appear anywhere.
372
- reo = re.compile(r'(?msu)(^|.)(\[(?P<attrs>[^[]+?)\])?' \
403
+ reo = re.compile(r'(?msu)(^|.)(\[(?P<attrlist>[^[]+?)\])?' \
373
404
  + r'(?:' + re.escape(lq) + r')' \
374
405
  + r'(?P<content>.+?)(?:'+re.escape(rq)+r')')
375
406
  else:
376
407
  # The text within constrained quotes must be bounded by white space.
377
408
  # Non-word (\W) characters are allowed at boundaries to accomodate
378
409
  # enveloping quotes.
379
- reo = re.compile(r'(?msu)(^|\W)(\[(?P<attrs>[^[]+?)\])?' \
410
+ reo = re.compile(r'(?msu)(^|\W)(\[(?P<attrlist>[^[]+?)\])?' \
380
411
  + r'(?:' + re.escape(lq) + r')' \
381
- + r'(?P<content>.+?)(?:'+re.escape(rq)+r')(?=\W|$)')
412
+ + r'(?P<content>.*?\S)(?:'+re.escape(rq)+r')(?=\W|$)')
382
413
  pos = 0
383
414
  while True:
384
415
  mo = reo.search(text,pos)
385
416
  if not mo: break
386
417
  if text[mo.start()] == '\\':
387
- pos = mo.end()
418
+ # Delete leading backslash.
419
+ text = text[:mo.start()] + text[mo.start()+1:]
420
+ # Skip past start of match.
421
+ pos = mo.start() + 1
388
422
  else:
389
- attrs = {}
390
- parse_attributes(mo.group('attrs'), attrs)
391
- stag,etag = config.tag(tag, attrs)
423
+ attrlist = {}
424
+ parse_attributes(mo.group('attrlist'), attrlist)
425
+ stag,etag = config.tag(tag, attrlist)
392
426
  s = mo.group(1) + stag + mo.group('content') + etag
393
427
  text = text[:mo.start()] + s + text[mo.end():]
394
428
  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
429
  return text
400
430
 
401
431
  def subs_tag(tag,dict={}):
402
432
  """Perform attribute substitution and split tag string returning start, end
403
433
  tag tuple (c.f. Config.tag())."""
434
+ if not tag:
435
+ return [None,None]
404
436
  s = subs_attrs(tag,dict)
405
437
  if not s:
406
- warning('tag "%s" dropped: contains undefined attribute' % tag)
438
+ warning('tag \'%s\' dropped: contains undefined attribute' % tag)
407
439
  return [None,None]
408
440
  result = s.split('|')
409
441
  if len(result) == 1:
@@ -422,7 +454,7 @@ def parse_entry(entry, dict=None, unquote=False, unique_values=False,
422
454
  If name! and allow_name_only=True then value is set to None.
423
455
  Leading and trailing white space is striped from 'name' and 'value'.
424
456
  'name' can contain any printable characters.
425
- If the '=' delimiter character is allowed in the 'name' then
457
+ If the '=' delimiter character is allowed in the 'name' then
426
458
  it must be escaped with a backslash and escape_delimiter must be True.
427
459
  If 'unquote' is True leading and trailing double-quotes are stripped from
428
460
  'name' and 'value'.
@@ -533,18 +565,26 @@ def filter_lines(filter_cmd, lines, dict={}):
533
565
  Run 'lines' through the 'filter_cmd' shell command and return the result.
534
566
  The 'dict' dictionary contains additional filter attributes.
535
567
  """
536
- # BUG: Has problems finding filters with spaces in command name.
537
- if not filter_cmd:
568
+ # Return input lines if there's not filter.
569
+ if not filter_cmd or not filter_cmd.strip():
538
570
  return lines
539
571
  # Perform attributes substitution on the filter command.
540
572
  s = subs_attrs(filter_cmd, dict)
541
573
  if not s:
542
- raise EAsciiDoc,'missing filter attribute: %s' % filter_cmd
543
- filter_cmd = s
574
+ raise EAsciiDoc,'undefined filter attribute in command: %s' % filter_cmd
575
+ filter_cmd = s.strip()
576
+ # Parse for quoted and unquoted command and command tail.
577
+ # Double quoted.
578
+ mo = re.match(r'^"(?P<cmd>[^"]+)"(?P<tail>.*)$', filter_cmd)
579
+ if not mo:
580
+ # Single quoted.
581
+ mo = re.match(r"^'(?P<cmd>[^']+)'(?P<tail>.*)$", filter_cmd)
582
+ if not mo:
583
+ # Unquoted catch all.
584
+ mo = re.match(r'^(?P<cmd>\S+)(?P<tail>.*)$', filter_cmd)
585
+ cmd = mo.group('cmd').strip()
544
586
  # Search for the filter command in both user and application 'filters'
545
587
  # sub-directories.
546
- mo = re.match(r'^(?P<cmd>\S+)(?P<tail>.*)$', filter_cmd)
547
- cmd = mo.group('cmd')
548
588
  found = False
549
589
  if not os.path.dirname(cmd):
550
590
  # Check in asciidoc user and application directories for unqualified
@@ -564,73 +604,36 @@ def filter_lines(filter_cmd, lines, dict={}):
564
604
  if found:
565
605
  cmd = cmd2
566
606
  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
607
  if os.path.isfile(cmd):
574
608
  found = True
575
609
  else:
576
610
  warning('filter not found: %s' % cmd)
577
611
  if found:
578
612
  filter_cmd = '"' + cmd + '"' + mo.group('tail')
579
- verbose('filtering: ' + filter_cmd)
580
613
  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)
614
+ # Windows doesn't like running scripts directly so explicitly
615
+ # specify interpreter.
616
+ if found:
617
+ if cmd.endswith('.py'):
618
+ filter_cmd = 'python ' + filter_cmd
619
+ elif cmd.endswith('.rb'):
620
+ filter_cmd = 'ruby ' + filter_cmd
621
+ verbose('filtering: ' + filter_cmd)
622
+ input = os.linesep.join(lines)
623
+ try:
624
+ p = subprocess.Popen(filter_cmd, shell=True,
625
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE)
626
+ output = p.communicate(input)[0]
627
+ except:
628
+ raise EAsciiDoc,'filter error: %s: %s' % (filter_cmd, sys.exc_info()[1])
629
+ if output:
630
+ result = [s.rstrip() for s in output.split(os.linesep)]
609
631
  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.
632
+ result = []
633
+ filter_status = p.wait()
634
+ if filter_status:
635
+ warning('filter non-zero exit code: %s: returned %d' %
636
+ (filter_cmd, filter_status))
634
637
  if lines and not result:
635
638
  warning('no output from filter: %s' % filter_cmd)
636
639
  return result
@@ -715,7 +718,7 @@ def system(name, args, is_macro=False):
715
718
 
716
719
  def subs_attrs(lines, dictionary=None):
717
720
  """Substitute 'lines' of text with attributes from the global
718
- document.attributes dictionary and from t'dictionary' ('dictionary'
721
+ document.attributes dictionary and from 'dictionary' ('dictionary'
719
722
  entries take precedence). Return a tuple of the substituted lines. 'lines'
720
723
  containing undefined attributes are deleted. If 'lines' is a string then
721
724
  return a string.
@@ -938,6 +941,12 @@ class Lex:
938
941
  if Lex.prev_element and Lex.prev_cursor == reader.cursor:
939
942
  return Lex.prev_element
940
943
  result = None
944
+ # Check for AttributeEntry.
945
+ if not result and AttributeEntry.isnext():
946
+ result = AttributeEntry
947
+ # Check for AttributeList.
948
+ if not result and AttributeList.isnext():
949
+ result = AttributeList
941
950
  # Check for Title.
942
951
  if not result and Title.isnext():
943
952
  result = Title
@@ -956,14 +965,10 @@ class Lex:
956
965
  else:
957
966
  result = blocks.current
958
967
  # Check for Table.
968
+ if not result and tables_OLD.isnext():
969
+ result = tables_OLD.current
959
970
  if not result and tables.isnext():
960
971
  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
972
  # Check for BlockTitle.
968
973
  if not result and BlockTitle.isnext():
969
974
  result = BlockTitle
@@ -978,86 +983,33 @@ class Lex:
978
983
  return result
979
984
  next = staticmethod(next)
980
985
 
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
986
  def subs_1(s,options):
1036
987
  """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."""
988
+ Does not process 'attributes' substitutions."""
1039
989
  if not s:
1040
990
  return s
1041
991
  result = s
1042
992
  for o in options:
1043
- if o == 'specialcharacters':
993
+ if o == 'none':
994
+ return s
995
+ elif o == 'specialcharacters':
1044
996
  result = config.subs_specialchars(result)
1045
- # Quoted text.
997
+ elif o == 'attributes':
998
+ result = subs_attrs(result)
1046
999
  elif o == 'quotes':
1047
1000
  result = subs_quotes(result)
1048
- # Special words.
1049
1001
  elif o == 'specialwords':
1050
1002
  result = config.subs_specialwords(result)
1051
- # Replacements.
1052
1003
  elif o in ('replacements','replacements2'):
1053
1004
  result = config.subs_replacements(result,o)
1054
- # Inline macros.
1055
1005
  elif o == 'macros':
1056
1006
  result = macros.subs(result)
1057
1007
  elif o == 'callouts':
1058
1008
  result = macros.subs(result,callouts=True)
1059
1009
  else:
1060
1010
  raise EAsciiDoc,'illegal substitution option: %s' % o
1011
+ if not result:
1012
+ break
1061
1013
  return result
1062
1014
  subs_1 = staticmethod(subs_1)
1063
1015
 
@@ -1075,19 +1027,18 @@ class Lex:
1075
1027
  return lines
1076
1028
  # Join lines so quoting can span multiple lines.
1077
1029
  para = '\n'.join(lines)
1078
- if 'passthroughs' in options:
1079
- passthroughs = []
1080
- para = Lex.extract_passthroughs(para,passthroughs)
1030
+ if 'macros' in options:
1031
+ para = macros.extract_passthroughs(para)
1081
1032
  for o in options:
1082
1033
  if o == 'attributes':
1083
1034
  # If we don't substitute attributes line-by-line then a single
1084
1035
  # undefined attribute will drop the entire paragraph.
1085
1036
  lines = subs_attrs(para.split('\n'))
1086
1037
  para = '\n'.join(lines)
1087
- elif o != 'passthroughs':
1038
+ else:
1088
1039
  para = Lex.subs_1(para,(o,))
1089
- if 'passthroughs' in options:
1090
- para = Lex.restore_passthroughs(para,passthroughs)
1040
+ if 'macros' in options:
1041
+ para = macros.restore_passthroughs(para)
1091
1042
  return para.splitlines()
1092
1043
  subs = staticmethod(subs)
1093
1044
 
@@ -1145,6 +1096,7 @@ class Document:
1145
1096
  self.attributes['backend-'+document.backend] = ''
1146
1097
  self.attributes['doctype-'+document.doctype] = ''
1147
1098
  self.attributes[document.backend+'-'+document.doctype] = ''
1099
+ self.attributes['asciidoc-file'] = APP_FILE
1148
1100
  self.attributes['asciidoc-dir'] = APP_DIR
1149
1101
  self.attributes['user-dir'] = USER_DIR
1150
1102
  if self.infile != '<stdin>':
@@ -1215,12 +1167,12 @@ class Document:
1215
1167
  error('SYNOPSIS section expected')
1216
1168
  else:
1217
1169
  Title.translate()
1218
- if Title.dict['title'].upper() <> 'SYNOPSIS':
1170
+ if Title.attributes['title'].upper() <> 'SYNOPSIS':
1219
1171
  error('second section must be named SYNOPSIS')
1220
1172
  if Title.level != 1:
1221
1173
  error('SYNOPSIS section title must be at level 1')
1222
1174
  d = {}
1223
- d.update(Title.dict)
1175
+ d.update(Title.attributes)
1224
1176
  AttributeList.consume(d)
1225
1177
  stag,etag = config.section2tags('sect-synopsis',d)
1226
1178
  writer.write(stag)
@@ -1324,7 +1276,7 @@ class Header:
1324
1276
  assert Lex.next() is Title and Title.level == 0
1325
1277
  Title.translate()
1326
1278
  attrs = document.attributes # Alias for readability.
1327
- attrs['doctitle'] = Title.dict['title']
1279
+ attrs['doctitle'] = Title.attributes['title']
1328
1280
  if document.doctype == 'manpage':
1329
1281
  # manpage title formatted like mantitle(manvolnum).
1330
1282
  mo = re.match(r'^(?P<mantitle>.*)\((?P<manvolnum>.*)\)$',
@@ -1372,7 +1324,7 @@ class Header:
1372
1324
  error('NAME section expected')
1373
1325
  else:
1374
1326
  Title.translate()
1375
- if Title.dict['title'].upper() <> 'NAME':
1327
+ if Title.attributes['title'].upper() <> 'NAME':
1376
1328
  error('first section must be named NAME')
1377
1329
  if Title.level != 1:
1378
1330
  error('NAME section title must be at level 1')
@@ -1395,6 +1347,7 @@ class AttributeEntry:
1395
1347
  pattern = None
1396
1348
  subs = None
1397
1349
  name = None
1350
+ name2 = None
1398
1351
  value = None
1399
1352
  def __init__(self):
1400
1353
  raise AssertionError,'no class instances allowed'
@@ -1415,36 +1368,49 @@ class AttributeEntry:
1415
1368
  AttributeEntry.subs = subs
1416
1369
  line = reader.read_next()
1417
1370
  if line:
1371
+ # Attribute entry formatted like :<name>[.<name2>]:[ <value>]
1418
1372
  mo = re.match(AttributeEntry.pattern,line)
1419
1373
  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
1374
+ AttributeEntry.name = mo.group('attrname')
1375
+ AttributeEntry.name2 = mo.group('attrname2')
1376
+ AttributeEntry.value = mo.group('attrvalue') or ''
1377
+ AttributeEntry.value = AttributeEntry.value.strip()
1430
1378
  result = True
1431
1379
  return result
1432
1380
  isnext = staticmethod(isnext)
1433
1381
  def translate():
1434
1382
  assert Lex.next() is AttributeEntry
1435
1383
  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]
1384
+ reader.read() # Discard attribute entry from reader.
1385
+ if AttributeEntry.name2: # The entry is a conf file entry.
1386
+ section = {}
1387
+ # [attributes] and [miscellaneous] entries can have name! syntax.
1388
+ if attr.name in ('attributes','miscellaneous') and attr.name2[-1] == '!':
1389
+ section[attr.name] = [attr.name2]
1390
+ else:
1391
+ section[attr.name] = ['%s=%s' % (attr.name2,attr.value)]
1392
+ config.load_sections(section)
1393
+ config.validate()
1394
+ else: # The entry is an attribute.
1395
+ if attr.name[-1] == '!':
1396
+ # Names like name! undefine the attribute.
1397
+ attr.name = attr.name[:-1]
1398
+ attr.value = None
1399
+ # Strip white space and illegal name chars.
1400
+ attr.name = re.sub(r'(?u)[^\w\-_]', '', attr.name).lower()
1401
+ # Don't override command-line attributes.
1402
+ if attr.name in config.cmd_attrs:
1403
+ return
1404
+ # Update document.attributes from previously parsed attribute.
1405
+ if attr.name == 'attributeentry-subs':
1406
+ AttributeEntry.subs = None # Force update in isnext().
1407
+ elif attr.value:
1408
+ attr.value = Lex.subs((attr.value,), attr.subs)
1409
+ attr.value = writer.newline.join(attr.value)
1410
+ if attr.value is not None:
1411
+ document.attributes[attr.name] = attr.value
1412
+ elif attr.name in document.attributes:
1413
+ del document.attributes[attr.name]
1448
1414
  translate = staticmethod(translate)
1449
1415
  def translate_all():
1450
1416
  """ Process all contiguous attribute lines on reader."""
@@ -1492,6 +1458,11 @@ class AttributeList:
1492
1458
  if AttributeList.attrs:
1493
1459
  d.update(AttributeList.attrs)
1494
1460
  AttributeList.attrs = {}
1461
+ # Generate option attributes.
1462
+ if 'options' in d:
1463
+ options = parse_options(d['options'], (), 'illegal option name')
1464
+ for option in options:
1465
+ d[option+'-option'] = ''
1495
1466
  consume = staticmethod(consume)
1496
1467
 
1497
1468
  class BlockTitle:
@@ -1537,7 +1508,7 @@ class Title:
1537
1508
  subs = ()
1538
1509
  pattern = None
1539
1510
  level = 0
1540
- dict = {}
1511
+ attributes = {}
1541
1512
  sectname = None
1542
1513
  section_numbers = [0]*len(underlines)
1543
1514
  dump_dict = {}
@@ -1545,7 +1516,7 @@ class Title:
1545
1516
  def __init__(self):
1546
1517
  raise AssertionError,'no class instances allowed'
1547
1518
  def translate():
1548
- """Parse the Title.dict and Title.level from the reader. The
1519
+ """Parse the Title.attributes and Title.level from the reader. The
1549
1520
  real work has already been done by parse()."""
1550
1521
  assert Lex.next() is Title
1551
1522
  # Discard title from reader.
@@ -1555,11 +1526,11 @@ class Title:
1555
1526
  # Perform title substitutions.
1556
1527
  if not Title.subs:
1557
1528
  Title.subs = config.subsnormal
1558
- s = Lex.subs((Title.dict['title'],), Title.subs)
1529
+ s = Lex.subs((Title.attributes['title'],), Title.subs)
1559
1530
  s = writer.newline.join(s)
1560
1531
  if not s:
1561
1532
  warning('blank section title')
1562
- Title.dict['title'] = s
1533
+ Title.attributes['title'] = s
1563
1534
  translate = staticmethod(translate)
1564
1535
  def isnext():
1565
1536
  lines = reader.read_ahead(2)
@@ -1576,7 +1547,7 @@ class Title:
1576
1547
  if Title.dump_dict.has_key(k):
1577
1548
  mo = re.match(Title.dump_dict[k], lines[0])
1578
1549
  if mo:
1579
- Title.dict = mo.groupdict()
1550
+ Title.attributes = mo.groupdict()
1580
1551
  Title.level = level
1581
1552
  Title.linecount = 1
1582
1553
  result = True
@@ -1601,25 +1572,25 @@ class Title:
1601
1572
  if not re.search(r'(?u)\w',title): return False
1602
1573
  mo = re.match(Title.pattern, title)
1603
1574
  if mo:
1604
- Title.dict = mo.groupdict()
1575
+ Title.attributes = mo.groupdict()
1605
1576
  Title.level = list(Title.underlines).index(ul[:2])
1606
1577
  Title.linecount = 2
1607
1578
  result = True
1608
1579
  # Check for expected pattern match groups.
1609
1580
  if result:
1610
- if not Title.dict.has_key('title'):
1581
+ if not Title.attributes.has_key('title'):
1611
1582
  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]
1583
+ Title.attributes['title'] = lines[0]
1584
+ for k,v in Title.attributes.items():
1585
+ if v is None: del Title.attributes[k]
1615
1586
  return result
1616
1587
  parse = staticmethod(parse)
1617
- def load(dict):
1618
- """Load and validate [titles] section entries from dict."""
1619
- if dict.has_key('underlines'):
1588
+ def load(entries):
1589
+ """Load and validate [titles] section entries dictionary."""
1590
+ if entries.has_key('underlines'):
1620
1591
  errmsg = 'malformed [titles] underlines entry'
1621
1592
  try:
1622
- underlines = parse_list(dict['underlines'])
1593
+ underlines = parse_list(entries['underlines'])
1623
1594
  except:
1624
1595
  raise EAsciiDoc,errmsg
1625
1596
  if len(underlines) != len(Title.underlines):
@@ -1628,27 +1599,27 @@ class Title:
1628
1599
  if len(s) !=2:
1629
1600
  raise EAsciiDoc,errmsg
1630
1601
  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,
1602
+ Title.dump_dict['underlines'] = entries['underlines']
1603
+ if entries.has_key('subs'):
1604
+ Title.subs = parse_options(entries['subs'], SUBS_OPTIONS,
1634
1605
  'illegal [titles] subs entry')
1635
- Title.dump_dict['subs'] = dict['subs']
1636
- if dict.has_key('sectiontitle'):
1637
- pat = dict['sectiontitle']
1606
+ Title.dump_dict['subs'] = entries['subs']
1607
+ if entries.has_key('sectiontitle'):
1608
+ pat = entries['sectiontitle']
1638
1609
  if not pat or not is_regexp(pat):
1639
1610
  raise EAsciiDoc,'malformed [titles] sectiontitle entry'
1640
1611
  Title.pattern = pat
1641
1612
  Title.dump_dict['sectiontitle'] = pat
1642
- if dict.has_key('blocktitle'):
1643
- pat = dict['blocktitle']
1613
+ if entries.has_key('blocktitle'):
1614
+ pat = entries['blocktitle']
1644
1615
  if not pat or not is_regexp(pat):
1645
1616
  raise EAsciiDoc,'malformed [titles] blocktitle entry'
1646
1617
  BlockTitle.pattern = pat
1647
1618
  Title.dump_dict['blocktitle'] = pat
1648
1619
  # Load single-line title patterns.
1649
1620
  for k in ('sect0','sect1','sect2','sect3','sect4'):
1650
- if dict.has_key(k):
1651
- pat = dict[k]
1621
+ if entries.has_key(k):
1622
+ pat = entries[k]
1652
1623
  if not pat or not is_regexp(pat):
1653
1624
  raise EAsciiDoc,'malformed [titles] %s entry' % k
1654
1625
  Title.dump_dict[k] = pat
@@ -1663,13 +1634,13 @@ class Title:
1663
1634
  """Set Title section name. First search for section title in
1664
1635
  [specialsections], if not found use default 'sect<level>' name."""
1665
1636
  for pat,sect in config.specialsections.items():
1666
- mo = re.match(pat,Title.dict['title'])
1637
+ mo = re.match(pat,Title.attributes['title'])
1667
1638
  if mo:
1668
1639
  title = mo.groupdict().get('title')
1669
1640
  if title is not None:
1670
- Title.dict['title'] = title.strip()
1641
+ Title.attributes['title'] = title.strip()
1671
1642
  else:
1672
- Title.dict['title'] = mo.group().strip()
1643
+ Title.attributes['title'] = mo.group().strip()
1673
1644
  Title.sectname = sect
1674
1645
  break
1675
1646
  else:
@@ -1721,9 +1692,10 @@ class Section:
1721
1692
  NameChar ::= Letter | Digit | '.' | '-' | '_' | ':'
1722
1693
  """
1723
1694
  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
1695
+ # Prefix the ID name with idprefix attribute or underscore if not
1696
+ # defined. Prefix ensures the ID does not clash with existing IDs.
1697
+ idprefix = document.attributes.get('idprefix','_')
1698
+ base_ident = idprefix + base_ident
1727
1699
  i = 1
1728
1700
  while True:
1729
1701
  if i == 1:
@@ -1763,11 +1735,11 @@ class Section:
1763
1735
  if not document.attributes.get('sectids') is None \
1764
1736
  and 'id' not in AttributeList.attrs:
1765
1737
  # Generate ids for sections.
1766
- AttributeList.attrs['id'] = Section.gen_id(Title.dict['title'])
1738
+ AttributeList.attrs['id'] = Section.gen_id(Title.attributes['title'])
1767
1739
  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)
1740
+ Title.attributes['sectnum'] = Title.getnumber(document.level)
1741
+ AttributeList.consume(Title.attributes)
1742
+ stag,etag = config.section2tags(Title.sectname,Title.attributes)
1771
1743
  Section.savetag(Title.level,etag)
1772
1744
  writer.write(stag)
1773
1745
  Section.translate_body()
@@ -1796,12 +1768,12 @@ class Section:
1796
1768
 
1797
1769
  class AbstractBlock:
1798
1770
  def __init__(self):
1799
- self.OPTIONS = () # The set of allowed options values
1800
1771
  # Configuration parameter names common to all blocks.
1801
- self.CONF_ENTRIES = ('options','subs','presubs','postsubs',
1802
- 'posattrs','style','.*-style')
1803
- # Configuration parameters.
1772
+ self.CONF_ENTRIES = ('delimiter','options','subs','presubs','postsubs',
1773
+ 'posattrs','style','.*-style','template','filter')
1774
+ self.start = None # File reader cursor at start delimiter.
1804
1775
  self.name=None # Configuration file section name.
1776
+ # Configuration parameters.
1805
1777
  self.delimiter=None # Regular expression matching block delimiter.
1806
1778
  self.template=None # template section entry.
1807
1779
  self.options=() # options entry list.
@@ -1810,18 +1782,27 @@ class AbstractBlock:
1810
1782
  self.filter=None # filter entry.
1811
1783
  self.posattrs=() # posattrs entry list.
1812
1784
  self.style=None # Default style.
1813
- self.styles=OrderedDict() # Styles dictionary.
1785
+ self.styles=OrderedDict() # Each entry is a styles dictionary.
1814
1786
  # Before a block is processed it's attributes (from it's
1815
1787
  # attributes list) are merged with the block configuration parameters
1816
- # (by self.process_attributes()) resulting in the template substitution
1788
+ # (by self.merge_attributes()) resulting in the template substitution
1817
1789
  # dictionary (self.attributes) and the block's procssing parameters
1818
1790
  # (self.parameters).
1819
1791
  self.attributes={}
1820
1792
  # The names of block parameters.
1821
1793
  self.PARAM_NAMES=('template','options','presubs','postsubs','filter')
1822
- self.parameters={}
1794
+ self.parameters=None
1823
1795
  # Leading delimiter match object.
1824
1796
  self.mo=None
1797
+ def short_name(self):
1798
+ """ Return the text following the last dash in the section namem """
1799
+ i = self.name.rfind('-')
1800
+ if i == -1:
1801
+ return self.name
1802
+ else:
1803
+ return self.name[i+1:]
1804
+ def error(self, msg, cursor=None, halt=False):
1805
+ error('[%s] %s' % (self.name,msg), cursor, halt)
1825
1806
  def is_conf_entry(self,param):
1826
1807
  """Return True if param matches an allowed configuration file entry
1827
1808
  name."""
@@ -1831,53 +1812,89 @@ class AbstractBlock:
1831
1812
  return False
1832
1813
  def load(self,name,entries):
1833
1814
  """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
1815
  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':
1816
+ self.update_parameters(entries, self, all=True)
1817
+ def update_parameters(self, src, dst=None, all=False):
1818
+ """
1819
+ Parse processing parameters from src dictionary to dst object.
1820
+ dst defaults to self.parameters.
1821
+ If all is True then copy src entries that aren't parameter names.
1822
+ """
1823
+ dst = dst or self.parameters
1824
+ msg = '[%s] malformed entry %%s: %%s' % self.name
1825
+ def copy(obj,k,v):
1826
+ if isinstance(obj,dict):
1827
+ obj[k] = v
1828
+ else:
1829
+ setattr(obj,k,v)
1830
+ for k,v in src.items():
1831
+ if not re.match(r'\d+',k) and not is_name(k):
1832
+ raise EAsciiDoc, msg % (k,v)
1833
+ if k == 'template':
1834
+ if not is_name(v):
1835
+ raise EAsciiDoc, msg % (k,v)
1836
+ copy(dst,k,v)
1837
+ elif k == 'filter':
1838
+ copy(dst,k,v)
1839
+ elif k == 'options':
1840
+ if isinstance(v,str):
1841
+ v = parse_options(v, (), msg % (k,v))
1842
+ copy(dst,k,v)
1843
+ elif k in ('subs','presubs','postsubs'):
1844
+ # Subs is an alias for presubs.
1845
+ if k == 'subs': k = 'presubs'
1846
+ if isinstance(v,str):
1847
+ v = parse_options(v, SUBS_OPTIONS, msg % (k,v))
1848
+ copy(dst,k,v)
1849
+ elif k == 'delimiter':
1843
1850
  if v and is_regexp(v):
1844
- self.delimiter = v
1851
+ copy(dst,k,v)
1845
1852
  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
1853
+ raise EAsciiDoc, msg % (k,v)
1852
1854
  elif k == 'style':
1853
- if not is_name(v):
1854
- raise EAsciiDoc, \
1855
- 'malformed [%s] style name: %s' % (name,v)
1856
- self.style = v
1855
+ if is_name(v):
1856
+ copy(dst,k,v)
1857
+ else:
1858
+ raise EAsciiDoc, msg % (k,v)
1857
1859
  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
1860
+ v = parse_options(v, (), msg % (k,v))
1861
+ copy(dst,k,v)
1871
1862
  else:
1872
1863
  mo = re.match(r'^(?P<style>.*)-style$',k)
1873
1864
  if mo:
1874
1865
  if not v:
1875
- raise EAsciiDoc, 'empty [%s] style: %s' % (name,k)
1866
+ raise EAsciiDoc, msg % (k,v)
1876
1867
  style = mo.group('style')
1868
+ if not is_name(style):
1869
+ raise EAsciiDoc, msg % (k,v)
1877
1870
  d = {}
1878
1871
  if not parse_named_attributes(v,d):
1879
- raise EAsciiDoc,'malformed [%s] style: %s' % (name,v)
1872
+ raise EAsciiDoc, msg % (k,v)
1873
+ if 'subs' in d:
1874
+ # Subs is an alias for presubs.
1875
+ d['presubs'] = d['subs']
1876
+ del d['subs']
1880
1877
  self.styles[style] = d
1878
+ elif all or k in self.PARAM_NAMES:
1879
+ copy(dst,k,v) # Derived class specific entries.
1880
+ def get_param(self,name,params=None):
1881
+ """
1882
+ Return named processing parameter from params dictionary.
1883
+ If the parameter is not in params look in self.parameters.
1884
+ """
1885
+ if params and name in params:
1886
+ return params[name]
1887
+ elif name in self.parameters:
1888
+ return self.parameters[name]
1889
+ else:
1890
+ return None
1891
+ def get_subs(self,params=None):
1892
+ """
1893
+ Return (presubs,postsubs) tuple.
1894
+ """
1895
+ presubs = self.get_param('presubs',params)
1896
+ postsubs = self.get_param('postsubs',params)
1897
+ return (presubs,postsubs)
1881
1898
  def dump(self):
1882
1899
  """Write block definition to stdout."""
1883
1900
  write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
@@ -1911,8 +1928,12 @@ class AbstractBlock:
1911
1928
  if self.is_conf_entry('delimiter') and not self.delimiter:
1912
1929
  raise EAsciiDoc,'[%s] missing delimiter' % self.name
1913
1930
  if self.style:
1931
+ if not is_name(self.style):
1932
+ raise EAsciiDoc, 'illegal style name: %s' % self.style
1914
1933
  if not self.styles.has_key(self.style):
1915
- warning(' missing [%s] %s-style entry' % (self.name,self.style))
1934
+ if not isinstance(self,List): # Lists don't have templates.
1935
+ warning('[%s] \'%s\' style not in %s' % (
1936
+ self.name,self.style,self.styles.keys()))
1916
1937
  # Check all styles for missing templates.
1917
1938
  all_styles_have_template = True
1918
1939
  for k,v in self.styles.items():
@@ -1928,7 +1949,8 @@ class AbstractBlock:
1928
1949
  if not config.sections.has_key(self.template):
1929
1950
  warning('[%s] missing template section' % self.template)
1930
1951
  elif not all_styles_have_template:
1931
- warning('[%s] styles missing templates' % self.name)
1952
+ if not isinstance(self,List): # Lists don't have templates.
1953
+ warning('[%s] styles missing templates' % self.name)
1932
1954
  def isnext(self):
1933
1955
  """Check if this block is next in document reader."""
1934
1956
  result = False
@@ -1943,34 +1965,50 @@ class AbstractBlock:
1943
1965
  """Translate block from document reader."""
1944
1966
  if not self.presubs:
1945
1967
  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)."""
1968
+ if reader.cursor:
1969
+ self.start = reader.cursor[:]
1970
+ def merge_attributes(self,attrs,params=[]):
1971
+ """
1972
+ Use the current blocks attribute list (attrs dictionary) to build a
1973
+ dictionary of block processing parameters (self.parameters) and tag
1974
+ substitution attributes (self.attributes).
1975
+
1976
+ 1. Copy the default parameters (self.*) to self.parameters.
1977
+ self.parameters are used internally to render the current block.
1978
+ Optional params array of addtional parameters.
1979
+
1980
+ 2. Copy attrs to self.attributes. self.attributes are used for template
1981
+ and tag substitution in the current block.
1982
+
1983
+ 3. If a style attribute was specified update self.parameters with the
1984
+ corresponding style parameters; if there are any style parameters
1985
+ remaining add them to self.attributes (existing attribute list entries
1986
+ take precedence).
1987
+
1988
+ 4. Set named positional attributes in self.attributes if self.posattrs
1989
+ was specified.
1990
+
1991
+ 5. Finally self.parameters is updated with any corresponding parameters
1992
+ specified in attrs.
1993
+
1994
+ """
1995
+
1996
+ def check_array_parameter(param):
1997
+ # Check the parameter is a sequence type.
1998
+ if not is_array(self.parameters[param]):
1999
+ error('malformed presubs attribute: %s' %
2000
+ self.parameters[param])
2001
+ # Revert to default value.
2002
+ self.parameters[param] = getattr(self,param)
2003
+
2004
+ params = list(self.PARAM_NAMES) + params
1965
2005
  self.attributes = {}
1966
2006
  self.attributes.update(attrs)
1967
2007
  # Calculate dynamic block parameters.
1968
2008
  # 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
2009
+ self.parameters = AttrDict()
2010
+ for name in params:
2011
+ self.parameters[name] = getattr(self,name)
1974
2012
  # Load the selected style attributes.
1975
2013
  posattrs = self.posattrs
1976
2014
  if posattrs and posattrs[0] == 'style':
@@ -1979,15 +2017,15 @@ class AbstractBlock:
1979
2017
  style = None
1980
2018
  if not style:
1981
2019
  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:
2020
+ if style:
2021
+ if not is_name(style):
2022
+ raise EAsciiDoc, 'illegal style name: %s' % style
2023
+ if self.styles.has_key(style):
1986
2024
  self.attributes['style'] = style
1987
2025
  for k,v in self.styles[style].items():
1988
2026
  if k == 'posattrs':
1989
2027
  posattrs = v
1990
- elif k in self.PARAM_NAMES:
2028
+ elif k in params:
1991
2029
  self.parameters[k] = v
1992
2030
  elif not self.attributes.has_key(k):
1993
2031
  # Style attributes don't take precedence over explicit.
@@ -1996,19 +2034,11 @@ class AbstractBlock:
1996
2034
  for i,v in enumerate(posattrs):
1997
2035
  if self.attributes.has_key(str(i+1)):
1998
2036
  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']
2037
+ # Override config and style attributes with attribute list attributes.
2038
+ self.update_parameters(attrs)
2039
+ check_array_parameter('options')
2040
+ check_array_parameter('presubs')
2041
+ check_array_parameter('postsubs')
2012
2042
 
2013
2043
  class AbstractBlocks:
2014
2044
  """List of block definitions."""
@@ -2058,8 +2088,6 @@ class AbstractBlocks:
2058
2088
  class Paragraph(AbstractBlock):
2059
2089
  def __init__(self):
2060
2090
  AbstractBlock.__init__(self)
2061
- self.CONF_ENTRIES += ('delimiter','template','filter')
2062
- self.OPTIONS = ('listelement',)
2063
2091
  self.text=None # Text in first line of paragraph.
2064
2092
  def load(self,name,entries):
2065
2093
  AbstractBlock.load(self,name,entries)
@@ -2080,18 +2108,20 @@ class Paragraph(AbstractBlock):
2080
2108
  AttributeList.consume(attrs)
2081
2109
  self.merge_attributes(attrs)
2082
2110
  reader.read() # Discard (already parsed item first line).
2083
- body = reader.read_until(r'^\+$|^$|'+blocks.delimiter+r'|'+tables.delimiter)
2111
+ body = reader.read_until(r'^\+$|^$|' + blocks.delimiter
2112
+ + r'|' + tables.delimiter
2113
+ + r'|' + tables_OLD.delimiter
2114
+ + r'|' + AttributeList.pattern
2115
+ )
2084
2116
  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)
2117
+ presubs = self.parameters.presubs
2118
+ postsubs = self.parameters.postsubs
2089
2119
  body = Lex.set_margin(body) # Move body to left margin.
2090
2120
  body = Lex.subs(body,presubs)
2091
- if self.get_filter():
2092
- body = filter_lines(self.get_filter(),body,self.attributes)
2121
+ if self.parameters.filter:
2122
+ body = filter_lines(self.parameters.filter,body,self.attributes)
2093
2123
  body = Lex.subs(body,postsubs)
2094
- template = self.get_template()
2124
+ template = self.parameters.template
2095
2125
  stag,etag = config.section2tags(template, self.attributes)
2096
2126
  # Write start tag, content, end tag.
2097
2127
  writer.write(dovetail_tags(stag,body,etag))
@@ -2117,16 +2147,15 @@ class Paragraphs(AbstractBlocks):
2117
2147
  raise EAsciiDoc,'missing [paradef-default] section'
2118
2148
 
2119
2149
  class List(AbstractBlock):
2120
- TAGS = ('listtag','itemtag','texttag','entrytag','labeltag')
2121
- TYPES = ('bulleted','numbered','labeled','callout')
2122
2150
  def __init__(self):
2123
2151
  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.
2152
+ self.CONF_ENTRIES += ('type','tags')
2153
+ self.PARAM_NAMES += ('tags',)
2154
+ # tabledef conf file parameters.
2155
+ self.type=None
2156
+ self.tags=None # Name of listtags-<tags> conf section.
2157
+ # Calculated parameters.
2158
+ self.tag=None # Current tags AttrDict.
2130
2159
  self.label=None # List item label (labeled lists).
2131
2160
  self.text=None # Text in first line of list item.
2132
2161
  self.index=None # Matched delimiter 'index' group (numbered lists).
@@ -2134,28 +2163,19 @@ class List(AbstractBlock):
2134
2163
  self.listindex=None # Current list index (1..)
2135
2164
  def load(self,name,entries):
2136
2165
  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
2166
  def dump(self):
2149
2167
  AbstractBlock.dump(self)
2150
2168
  write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2151
2169
  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)
2170
+ write('tags='+self.tags)
2158
2171
  write('')
2172
+ def validate(self):
2173
+ AbstractBlock.validate(self)
2174
+ tags = [self.tags]
2175
+ tags += [s['tags'] for s in self.styles.values() if 'tags' in s]
2176
+ for t in tags:
2177
+ if t not in lists.tags:
2178
+ self.error('missing section: [listtags-%s]' % t,halt=True)
2159
2179
  def isnext(self):
2160
2180
  result = AbstractBlock.isnext(self)
2161
2181
  if result:
@@ -2165,24 +2185,20 @@ class List(AbstractBlock):
2165
2185
  return result
2166
2186
  def translate_entry(self):
2167
2187
  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.
2188
+ entrytag = subs_tag(self.tag.entry, self.attributes)
2189
+ labeltag = subs_tag(self.tag.label, self.attributes)
2190
+ writer.write(entrytag[0])
2191
+ writer.write(labeltag[0])
2192
+ # Write labels.
2193
+ while Lex.next() is self:
2173
2194
  reader.read() # Discard (already parsed item first line).
2174
- writer.write_tag(self.labeltag, [self.label],
2195
+ writer.write_tag(self.tag.term, [self.label],
2175
2196
  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)
2197
+ if self.text: break
2198
+ writer.write(labeltag[1])
2182
2199
  # Write item text.
2183
2200
  self.translate_item()
2184
- if etag:
2185
- writer.write(etag)
2201
+ writer.write(entrytag[1])
2186
2202
  def iscontinued(self):
2187
2203
  if reader.read_next() == '+':
2188
2204
  reader.read() # Discard.
@@ -2201,26 +2217,27 @@ class List(AbstractBlock):
2201
2217
  """Translation for '+' style list continuation."""
2202
2218
  if self.type == 'callout':
2203
2219
  self.attributes['coids'] = calloutmap.calloutids(self.listindex)
2204
- stag,etag = config.tag(self.itemtag, self.attributes)
2205
- if stag:
2206
- writer.write(stag)
2220
+ itemtag = subs_tag(self.tag.item, self.attributes)
2221
+ writer.write(itemtag[0])
2207
2222
  if self.text and self.text == '+':
2208
- # Pathalogical case: continued Horizontal Labeled List with no
2223
+ # Pathological case: continued Horizontal Labeled List with no
2209
2224
  # item text.
2210
2225
  continued = True
2211
2226
  elif not self.text and self.iscontinued():
2212
- # Pathalogical case: continued Vertical Labeled List with no
2227
+ # Pathological case: continued Vertical Labeled List with no
2213
2228
  # item text.
2214
2229
  continued = True
2215
2230
  else:
2216
2231
  # Write ItemText.
2217
- text = reader.read_until(lists.delimiter + r'|^\+$|^$|' +
2218
- blocks.delimiter + r'|' + tables.delimiter)
2232
+ text = reader.read_until(
2233
+ lists.delimiter + r'|^\+$|^$|' + blocks.delimiter
2234
+ + r'|' + tables.delimiter
2235
+ + r'|' + tables_OLD.delimiter
2236
+ )
2219
2237
  if self.text is not None:
2220
2238
  text = [self.text] + list(text)
2221
- text = join_lines(text)
2222
2239
  if text:
2223
- writer.write_tag(self.texttag, text, self.presubs, self.attributes)
2240
+ writer.write_tag(self.tag.text, text, self.presubs, self.attributes)
2224
2241
  continued = self.iscontinued()
2225
2242
  while True:
2226
2243
  next = Lex.next()
@@ -2237,23 +2254,23 @@ class List(AbstractBlock):
2237
2254
  else:
2238
2255
  break
2239
2256
  continued = self.iscontinued()
2240
- if etag:
2241
- writer.write(etag)
2257
+ writer.write(itemtag[1])
2242
2258
  def translate_item_2(self):
2243
2259
  """Translation for List block style lists."""
2244
2260
  if self.type == 'callout':
2245
2261
  self.attributes['coids'] = calloutmap.calloutids(self.listindex)
2246
- stag,etag = config.tag(self.itemtag, self.attributes)
2247
- if stag:
2248
- writer.write(stag)
2262
+ itemtag = subs_tag(self.tag.item, self.attributes)
2263
+ writer.write(itemtag[0])
2249
2264
  if self.text or reader.read_next():
2250
2265
  # Write ItemText.
2251
- text = reader.read_until(lists.delimiter + r'|^$|' +
2252
- blocks.delimiter + r'|' + tables.delimiter)
2266
+ text = reader.read_until(
2267
+ lists.delimiter + r'|^$|' + blocks.delimiter
2268
+ + r'|' + tables.delimiter
2269
+ + r'|' + tables_OLD.delimiter
2270
+ )
2253
2271
  if self.text is not None:
2254
2272
  text = [self.text] + list(text)
2255
- text = join_lines(text)
2256
- writer.write_tag(self.texttag, text, self.presubs, self.attributes)
2273
+ writer.write_tag(self.tag.text, text, self.presubs, self.attributes)
2257
2274
  while True:
2258
2275
  next = Lex.next()
2259
2276
  if next in lists.open:
@@ -2270,8 +2287,7 @@ class List(AbstractBlock):
2270
2287
  next.translate()
2271
2288
  else:
2272
2289
  break
2273
- if etag:
2274
- writer.write(etag)
2290
+ writer.write(itemtag[1])
2275
2291
  def check_index(self):
2276
2292
  """ Check calculated listindex (1,2,...) against the item index in the
2277
2293
  document (self.index)."""
@@ -2287,15 +2303,37 @@ class List(AbstractBlock):
2287
2303
  if matched and i != self.listindex:
2288
2304
  print 'type: ',self.type,': expected ',self.listindex,' got ',i
2289
2305
  warning('list item %s out of sequence' % self.index)
2306
+ def check_tags(self):
2307
+ """ Check that all necessary tags are present. """
2308
+ tags = set(Lists.TAGS)
2309
+ if self.type != 'labeled':
2310
+ tags = tags.difference(['entry','label','term'])
2311
+ missing = tags.difference(self.tag.keys())
2312
+ if missing:
2313
+ self.error('missing tag(s): %s' % ','.join(missing), halt=True)
2290
2314
  def translate(self):
2291
2315
  AbstractBlock.translate(self)
2316
+ if self.short_name() in ('bibliography','glossary','qanda'):
2317
+ deprecated('old %s list syntax' % self.short_name())
2292
2318
  lists.open.append(self)
2293
2319
  attrs = {}
2294
2320
  attrs.update(self.mo.groupdict())
2295
2321
  BlockTitle.consume(attrs)
2296
2322
  AttributeList.consume(attrs)
2297
- self.merge_attributes(attrs)
2298
- stag,etag = config.tag(self.listtag, self.attributes)
2323
+ self.merge_attributes(attrs,['tags'])
2324
+ self.tag = lists.tags[self.parameters.tags]
2325
+ self.check_tags()
2326
+ if 'width' in self.attributes:
2327
+ # Set horizontal list 'labelwidth' and 'itemwidth' attributes.
2328
+ v = str(self.attributes['width'])
2329
+ mo = re.match(r'^(\d{1,2})%?$',v)
2330
+ if mo:
2331
+ labelwidth = int(mo.group(1))
2332
+ self.attributes['labelwidth'] = str(labelwidth)
2333
+ self.attributes['itemwidth'] = str(100-labelwidth)
2334
+ else:
2335
+ self.error('illegal attribute value: label="%s"' % v)
2336
+ stag,etag = subs_tag(self.tag.list, self.attributes)
2299
2337
  if stag:
2300
2338
  writer.write(stag)
2301
2339
  self.listindex = 0
@@ -2324,37 +2362,50 @@ class Lists(AbstractBlocks):
2324
2362
  """List of List objects."""
2325
2363
  BLOCK_TYPE = List
2326
2364
  PREFIX = 'listdef-'
2365
+ TYPES = ('bulleted','numbered','labeled','callout')
2366
+ TAGS = ('list', 'entry','item','text', 'label','term')
2327
2367
  def __init__(self):
2328
2368
  AbstractBlocks.__init__(self)
2329
2369
  self.open = [] # A stack of the current and parent lists.
2330
2370
  self.listblock = None # Current list is in list block.
2371
+ self.tags={} # List tags dictionary. Each entry is a tags AttrDict.
2331
2372
  def load(self,sections):
2332
2373
  AbstractBlocks.load(self,sections)
2374
+ self.load_tags(sections)
2375
+ def load_tags(self,sections):
2376
+ """
2377
+ Load listtags-* conf file sections to self.tags.
2378
+ """
2379
+ for section in sections.keys():
2380
+ mo = re.match(r'^listtags-(?P<name>\w+)$',section)
2381
+ if mo:
2382
+ name = mo.group('name')
2383
+ if self.tags.has_key(name):
2384
+ d = self.tags[name]
2385
+ else:
2386
+ d = AttrDict()
2387
+ parse_entries(sections.get(section,()),d)
2388
+ for k in d.keys():
2389
+ if k not in self.TAGS:
2390
+ warning('[%s] contains illegal list tag: %s' %
2391
+ (section,k))
2392
+ self.tags[name] = d
2333
2393
  def validate(self):
2334
2394
  AbstractBlocks.validate(self)
2335
2395
  for b in self.blocks:
2336
2396
  # Check list has valid type.
2337
- if not b.type in b.TYPES:
2397
+ if not b.type in Lists.TYPES:
2338
2398
  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)
2399
+ b.validate()
2400
+ def dump(self):
2401
+ AbstractBlocks.dump(self)
2402
+ for k,v in self.tags.items():
2403
+ dump_section('listtags-'+k, v)
2404
+
2351
2405
 
2352
2406
  class DelimitedBlock(AbstractBlock):
2353
2407
  def __init__(self):
2354
2408
  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
2409
  def load(self,name,entries):
2359
2410
  AbstractBlock.load(self,name,entries)
2360
2411
  def dump(self):
@@ -2368,14 +2419,13 @@ class DelimitedBlock(AbstractBlock):
2368
2419
  if 'list' in self.options:
2369
2420
  lists.listblock = self
2370
2421
  reader.read() # Discard delimiter.
2371
- self.start = reader.cursor[:]
2372
2422
  attrs = {}
2373
2423
  # Leave list block attributes for the list element.
2374
2424
  if lists.listblock is not self:
2375
2425
  BlockTitle.consume(attrs)
2376
2426
  AttributeList.consume(attrs)
2377
2427
  self.merge_attributes(attrs)
2378
- options = self.get_options()
2428
+ options = self.parameters.options
2379
2429
  if safe() and self.name == 'blockdef-backend':
2380
2430
  unsafe_error('Backend Block')
2381
2431
  # Discard block body.
@@ -2384,7 +2434,7 @@ class DelimitedBlock(AbstractBlock):
2384
2434
  # Discard block body.
2385
2435
  reader.read_until(self.delimiter,same_file=True)
2386
2436
  else:
2387
- template = self.get_template()
2437
+ template = self.parameters.template
2388
2438
  stag,etag = config.section2tags(template,self.attributes)
2389
2439
  if 'sectionbody' in options or 'list' in options:
2390
2440
  # The body is treated like a SimpleSection.
@@ -2393,18 +2443,18 @@ class DelimitedBlock(AbstractBlock):
2393
2443
  writer.write(etag)
2394
2444
  else:
2395
2445
  body = reader.read_until(self.delimiter,same_file=True)
2396
- presubs,postsubs = self.get_subs()
2446
+ presubs = self.parameters.presubs
2447
+ postsubs = self.parameters.postsubs
2397
2448
  body = Lex.subs(body,presubs)
2398
- if self.get_filter():
2399
- body = filter_lines(self.get_filter(),body,self.attributes)
2449
+ if self.parameters.filter:
2450
+ body = filter_lines(self.parameters.filter,body,self.attributes)
2400
2451
  body = Lex.subs(body,postsubs)
2401
2452
  # Write start tag, content, end tag.
2402
2453
  writer.write(dovetail_tags(stag,body,etag))
2403
2454
  if 'list' in options:
2404
2455
  lists.listblock = None
2405
2456
  if reader.eof():
2406
- error('missing %s block closing delimiter' % self.name.split('-')[-1],
2407
- cursor=self.start)
2457
+ self.error('missing closing delimiter',self.start)
2408
2458
  else:
2409
2459
  delimiter = reader.read() # Discard delimiter line.
2410
2460
  assert re.match(self.delimiter,delimiter)
@@ -2423,372 +2473,384 @@ class DelimitedBlocks(AbstractBlocks):
2423
2473
 
2424
2474
  class Column:
2425
2475
  """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.
2476
+ def __init__(self, width=None, align=None, style=None):
2477
+ self.width=width or '1'
2478
+ self.align=align or '<'
2479
+ self.style=style # Style name or None.
2480
+ # Calculated attribute values.
2481
+ self.colalign=None # 'left','center','right'.
2482
+ self.abswidth=None # 1.. (page units).
2483
+ self.pcwidth=None # 1..99 (percentage).
2430
2484
 
2431
2485
  class Table(AbstractBlock):
2432
- COL_STOP = r"(`|'|\.)" # RE.
2433
- ALIGNMENTS = {'`':'left', "'":'right', '.':'center'}
2434
- FORMATS = ('fixed','csv','dsv')
2486
+ ALIGNMENTS = {'<':'left', '>':'right', '^':'center'}
2487
+ FORMATS = ('psv','csv','dsv')
2488
+ SEPARATORS = dict(
2489
+ csv=',',
2490
+ dsv=r':|\n',
2491
+ psv=r'((?P<cellcount>\d+)\*)?\|',
2492
+ )
2435
2493
  def __init__(self):
2436
2494
  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
2495
+ self.CONF_ENTRIES += ('format','tags','separator')
2496
+ # tabledef conf file parameters.
2497
+ self.format='psv'
2498
+ self.separator=None
2499
+ self.tags=None # Name of tabletags-<tags> conf section.
2450
2500
  # 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.
2501
+ self.abswidth=None # 1.. (page units).
2502
+ self.pcwidth = None # 1..99 (percentage).
2503
+ self.rows=[] # Parsed rows, each row is a list of cell data.
2454
2504
  self.columns=[] # List of Columns.
2455
- # Other.
2456
- self.check_msg='' # Message set by previous self.validate() call.
2457
2505
  def load(self,name,entries):
2458
2506
  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
2507
  def dump(self):
2486
2508
  AbstractBlock.dump(self)
2487
2509
  write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2488
- write('fillchar='+self.fillchar)
2489
2510
  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
2511
  write('')
2503
2512
  def validate(self):
2504
2513
  AbstractBlock.validate(self)
2505
- """Check table definition and set self.check_msg if invalid else set
2506
- self.check_msg to blank string."""
2514
+ if self.format not in Table.FORMATS:
2515
+ self.error('illegal format=%s' % self.format,halt=True)
2516
+ self.tags = self.tags or 'default'
2517
+ tags = [self.tags]
2518
+ tags += [s['tags'] for s in self.styles.values() if 'tags' in s]
2519
+ for t in tags:
2520
+ if t not in tables.tags:
2521
+ self.error('missing section: [tabletags-%s]' % t,halt=True)
2522
+ if self.separator:
2523
+ # Evaluate escape characters.
2524
+ self.separator = eval('"'+self.separator+'"')
2525
+ #TODO: Move to class Tables
2507
2526
  # Check global table parameters.
2508
- if config.textwidth is None:
2509
- self.check_msg = 'missing [miscellaneous] textwidth entry'
2510
2527
  elif config.pagewidth is None:
2511
- self.check_msg = 'missing [miscellaneous] pagewidth entry'
2528
+ self.error('missing [miscellaneous] entry: pagewidth')
2512
2529
  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'
2530
+ self.error('missing [miscellaneous] entry: pageunits')
2531
+ def validate_attributes(self):
2532
+ """Validate and parse table attributes."""
2533
+ # Set defaults.
2534
+ format = self.format
2535
+ tags = self.tags
2536
+ separator = self.separator
2537
+ abswidth = float(config.pagewidth)
2538
+ pcwidth = 100.0
2539
+ for k,v in self.attributes.items():
2540
+ if k == 'format':
2541
+ if v not in self.FORMATS:
2542
+ self.error('illegal %s=%s' % (k,v))
2543
+ else:
2544
+ format = v
2545
+ elif k == 'tags':
2546
+ if v not in tables.tags:
2547
+ self.error('illegal %s=%s' % (k,v))
2548
+ else:
2549
+ tags = v
2550
+ elif k == 'separator':
2551
+ separator = v
2552
+ elif k == 'width':
2553
+ if not re.match(r'^\d{1,3}%$',v) or int(v[:-1]) > 100:
2554
+ self.error('illegal %s=%s' % (k,v))
2555
+ else:
2556
+ abswidth = float(v[:-1])/100 * config.pagewidth
2557
+ pcwidth = float(v[:-1])
2558
+ # Calculate separator if it has not been specified.
2559
+ if not separator:
2560
+ separator = Table.SEPARATORS[format]
2561
+ if format == 'csv':
2562
+ if len(separator) > 1:
2563
+ self.error('illegal csv separator=%s' % separator)
2564
+ separator = ','
2526
2565
  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))
2566
+ if not is_regexp(separator):
2567
+ self.error('illegal regular expression: separator=%s' %
2568
+ separator)
2569
+ self.parameters.format = format
2570
+ self.parameters.tags = tags
2571
+ self.parameters.separator = separator
2572
+ self.abswidth = abswidth
2573
+ self.pcwidth = pcwidth
2574
+ def get_tags(self,params):
2575
+ tags = self.get_param('tags',params)
2576
+ assert(tags and tags in tables.tags)
2577
+ return tables.tags[tags]
2578
+ def get_style(self,prefix):
2579
+ """
2580
+ Return the style dictionary whose name starts with 'prefix'.
2581
+ """
2582
+ if prefix is None:
2583
+ return None
2584
+ names = self.styles.keys()
2585
+ names.sort()
2586
+ for name in names:
2587
+ if name.startswith(prefix):
2588
+ return self.styles[name]
2540
2589
  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
2590
+ self.error('missing style: %s*' % prefix)
2591
+ return None
2592
+ def parse_cols(self,cols):
2593
+ """
2594
+ Build list of column objects from table 'cols' attribute.
2595
+ """
2596
+ # [<multiplier>*][<align>][<width>][<style>]
2597
+ COLS_RE1 = r'^((?P<count>\d+)\*)?(?P<align>[<\^>])?(?P<width>\d+%?)?(?P<style>[a-zA-Z]\w*)?$'
2598
+ # [<multiplier>*][<width>][<align>][<style>]
2599
+ COLS_RE2 = r'^((?P<count>\d+)\*)?(?P<width>\d+%?)?(?P<align>[<\^>])?(?P<style>[a-zA-Z]\w*)?$'
2600
+ reo1 = re.compile(COLS_RE1)
2601
+ reo2 = re.compile(COLS_RE2)
2602
+ cols = str(cols)
2603
+ if re.match(r'^\d+$',cols):
2604
+ for i in range(int(cols)):
2605
+ self.columns.append(Column())
2550
2606
  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
2607
+ for col in re.split(r'\s*,\s*',cols):
2608
+ mo = reo1.match(col)
2609
+ if not mo:
2610
+ mo = reo2.match(col)
2611
+ if mo:
2612
+ count = int(mo.groupdict().get('count') or 1)
2613
+ for i in range(count):
2614
+ self.columns.append(
2615
+ Column(mo.group('width'), mo.group('align'),
2616
+ self.get_style(mo.group('style')))
2617
+ )
2599
2618
  else:
2600
- # Size proportional to page width.
2601
- colfraction = width/totalwidth
2619
+ self.error('illegal column spec: %s' % col,self.start)
2620
+ # Validate widths and calculate missing widths.
2621
+ n = 0; percents = 0; props = 0
2622
+ for col in self.columns:
2623
+ if col.width:
2624
+ if col.width[-1] == '%': percents += int(col.width[:-1])
2625
+ else: props += int(col.width)
2626
+ n += 1
2627
+ if percents > 0 and props > 0:
2628
+ self.error('mixed percent and proportional widths: %s'
2629
+ % cols,self.start)
2630
+ pcunits = percents > 0
2631
+ # Fill in missing widths.
2632
+ if n < len(self.columns) and percents < 100:
2633
+ if pcunits:
2634
+ width = float(100 - percents)/float(len(self.columns) - n)
2602
2635
  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)
2636
+ width = 1
2637
+ for col in self.columns:
2638
+ if not col.width:
2639
+ if pcunits:
2640
+ col.width = str(int(width))+'%'
2641
+ percents += width
2642
+ else:
2643
+ col.width = str(width)
2644
+ props += width
2645
+ # Calculate column alignment and absolute and percent width values.
2646
+ percents = 0
2647
+ for col in self.columns:
2648
+ col.colalign = Table.ALIGNMENTS[col.align]
2649
+ if pcunits:
2650
+ col.pcwidth = float(col.width[:-1])
2651
+ else:
2652
+ col.pcwidth = (float(col.width)/props)*100
2653
+ col.abswidth = int(self.abswidth * (col.pcwidth/100))
2654
+ percents += col.pcwidth
2655
+ col.pcwidth = int(col.pcwidth)
2656
+ if round(percents) > 100:
2657
+ self.error('total width exceeds 100%%: %s' % cols,self.start)
2658
+ elif round(percents) < 100:
2659
+ self.error('total width less than 100%%: %s' % cols,self.start)
2660
+ def build_colspecs(self):
2661
+ """
2662
+ Generate column related substitution attributes.
2663
+ """
2664
+ cols = []
2665
+ i = 0
2666
+ for col in self.columns:
2667
+ i += 1
2668
+ colspec = self.get_tags(col.style).colspec
2669
+ if colspec:
2670
+ self.attributes['colalign'] = col.colalign
2671
+ self.attributes['colabswidth'] = col.abswidth
2672
+ self.attributes['colpcwidth'] = col.pcwidth
2673
+ self.attributes['colnumber'] = str(i+1)
2674
+ s = subs_attrs(colspec, self.attributes)
2620
2675
  if not s:
2621
2676
  warning('colspec dropped: contains undefined attribute')
2622
2677
  else:
2623
2678
  cols.append(s)
2679
+ if cols:
2624
2680
  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)
2681
+ def parse_rows(self, text):
2682
+ """
2683
+ Parse the table source text into self.rows (a list of rows, each row
2684
+ is a list of raw cell text.
2685
+ """
2686
+ if self.parameters.format in ('psv','dsv'):
2687
+ cells = self.parse_psv_dsv(text)
2688
+ colcount = len(self.columns)
2689
+ for i in range(0, len(cells), colcount):
2690
+ self.rows.append(cells[i:i+colcount])
2691
+ elif self.parameters.format == 'csv':
2692
+ self.parse_csv(text)
2650
2693
  else:
2651
2694
  assert True,'illegal table format'
2652
- # Substitute and indent all data in all rows.
2695
+ def subs_rows(self, rows, rowtype='body'):
2696
+ """
2697
+ Return a string of output markup from a list of rows, each row
2698
+ is a list of raw cell text.
2699
+ """
2700
+ tags = tables.tags[self.parameters.tags]
2701
+ if rowtype == 'header':
2702
+ rtag = tags.headrow
2703
+ elif rowtype == 'footer':
2704
+ rtag = tags.footrow
2705
+ else:
2706
+ rtag = tags.bodyrow
2707
+ result = []
2653
2708
  stag,etag = subs_tag(rtag,self.attributes)
2654
2709
  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):
2710
+ result.append(stag)
2711
+ result += self.subs_row(row,rowtype)
2712
+ result.append(etag)
2713
+ return writer.newline.join(result)
2714
+ def subs_row(self, row, rowtype):
2715
+ """
2716
+ Substitute the list of cells using the cell data tag.
2717
+ Returns a list of marked up table cell elements.
2718
+ """
2719
+ if len(row) < len(self.columns):
2720
+ warning('fewer row data items than table columns')
2721
+ if len(row) > len(self.columns):
2667
2722
  warning('more row data items than table columns')
2723
+ result = []
2668
2724
  for i in range(len(self.columns)):
2669
- if i > len(data) - 1:
2670
- d = '' # Fill missing column data with blanks.
2725
+ col = self.columns[i]
2726
+ tags = self.get_tags(col.style)
2727
+ self.attributes['colalign'] = col.colalign
2728
+ self.attributes['colabswidth'] = col.abswidth
2729
+ self.attributes['colpcwidth'] = col.pcwidth
2730
+ self.attributes['colnumber'] = str(i+1)
2731
+ if rowtype == 'header':
2732
+ dtag = tags.headdata
2733
+ elif rowtype == 'footer':
2734
+ dtag = tags.footdata
2671
2735
  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.
2736
+ dtag = tags.bodydata
2737
+ # Fill missing column data with blanks.
2738
+ if i > len(row) - 1:
2739
+ data = ''
2687
2740
  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)
2741
+ data = row[i]
2742
+ # Format header cells with the table style not column style.
2743
+ if rowtype == 'header':
2744
+ colstyle = None
2745
+ else:
2746
+ colstyle = col.style
2747
+ presubs,postsubs = self.get_subs(colstyle)
2748
+ data = [data]
2749
+ data = Lex.subs(data, presubs)
2750
+ data = filter_lines(self.get_param('filter',colstyle),
2751
+ data, self.attributes)
2752
+ data = Lex.subs(data, postsubs)
2753
+ if rowtype != 'header':
2754
+ ptag = tags.paragraph
2755
+ if ptag:
2756
+ stag,etag = subs_tag(ptag,self.attributes)
2757
+ text = '\n'.join(data).strip()
2758
+ data = []
2759
+ for para in re.split(r'\n{2,}',text):
2760
+ data += dovetail_tags([stag],para.split('\n'),[etag])
2761
+ stag,etag = subs_tag(dtag,self.attributes)
2762
+ result = result + dovetail_tags([stag],data,[etag])
2711
2763
  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."""
2764
+ def parse_csv(self,text):
2765
+ """
2766
+ Parse the table source text and return a list of rows, each row
2767
+ is a list of raw cell text.
2768
+ """
2715
2769
  import StringIO
2716
2770
  import csv
2717
- result = []
2718
- rdr = csv.reader(StringIO.StringIO('\r\n'.join(rows)),
2719
- skipinitialspace=True)
2771
+ self.rows = []
2772
+ rdr = csv.reader(StringIO.StringIO('\r\n'.join(text)),
2773
+ delimiter=self.parameters.separator, skipinitialspace=True)
2720
2774
  try:
2721
2775
  for row in rdr:
2722
- result.append(row)
2776
+ self.rows.append(row)
2723
2777
  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
2778
+ self.error('csv parse error: %s' % row)
2779
+ def parse_psv_dsv(self,text):
2780
+ """
2781
+ Parse list of PSV or DSV table source text lines and return a list of
2782
+ cells.
2783
+ """
2784
+ text = '\n'.join(text)
2785
+ separator = '(?msu)'+self.parameters.separator
2786
+ format = self.parameters.format
2787
+ start = 0
2788
+ cellcount = 1
2789
+ cells = []
2790
+ cell = ''
2791
+ for mo in re.finditer(separator,text):
2792
+ cell += text[start:mo.start()]
2793
+ if cell.endswith('\\'):
2794
+ cell = cell[:-1]+mo.group() # Reinstate escaped separators.
2795
+ else:
2796
+ for i in range(cellcount):
2797
+ cells.append(cell)
2798
+ cellcount = int(mo.groupdict().get('cellcount') or '1')
2799
+ cell = ''
2800
+ start = mo.end()
2801
+ # Last cell follows final separator.
2802
+ cell += text[start:]
2803
+ for i in range(cellcount):
2804
+ cells.append(cell)
2805
+ # We expect a dummy blank item preceeding first PSV cell.
2806
+ if format == 'psv':
2807
+ if cells[0] != '':
2808
+ self.error('missing leading separator: %s' % separator,
2809
+ self.start)
2810
+ else:
2811
+ cells.pop(0)
2812
+ return cells
2745
2813
  def translate(self):
2746
2814
  AbstractBlock.translate(self)
2815
+ reader.read() # Discard delimiter.
2747
2816
  # Reset instance specific properties.
2748
- self.underline = None
2749
2817
  self.columns = []
2818
+ self.rows = []
2750
2819
  attrs = {}
2751
2820
  BlockTitle.consume(attrs)
2752
- # Add relevant globals to table substitutions.
2753
- attrs['pagewidth'] = str(config.pagewidth)
2754
- attrs['pageunits'] = config.pageunits
2755
2821
  # Mix in document attribute list.
2756
2822
  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
2823
  self.merge_attributes(attrs)
2769
- # Parse table ruler.
2770
- ruler = reader.read()
2771
- assert re.match(self.delimiter,ruler)
2772
- self.parse_ruler(ruler)
2824
+ self.validate_attributes()
2825
+ # Add global and calculated configuration parameters.
2826
+ self.attributes['pagewidth'] = config.pagewidth
2827
+ self.attributes['pageunits'] = config.pageunits
2828
+ self.attributes['tableabswidth'] = int(self.abswidth)
2829
+ self.attributes['tablepcwidth'] = int(self.pcwidth)
2773
2830
  # 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))
2831
+ text = reader.read_until(self.delimiter)
2832
+ if reader.eof():
2833
+ self.error('missing closing delimiter',self.start)
2834
+ else:
2835
+ delimiter = reader.read() # Discard closing delimiter.
2836
+ assert re.match(self.delimiter,delimiter)
2837
+ if len(text) == 0:
2838
+ warning('[%s] table is empty' % self.name)
2789
2839
  return
2790
- # Generate colwidths and colspecs.
2840
+ cols = attrs.get('cols')
2841
+ if not cols:
2842
+ # Calculate column count from number of items in first line.
2843
+ if self.parameters.format == 'csv':
2844
+ cols = text[0].count(self.parameters.separator)
2845
+ else:
2846
+ cols = len(self.parse_psv_dsv(text[:1]))
2847
+ self.parse_cols(cols)
2848
+ # Set calculated attributes.
2849
+ self.attributes['colcount'] = len(self.columns)
2791
2850
  self.build_colspecs()
2851
+ self.parse_rows(text)
2852
+ # The 'rowcount' attribute is used by the experimental LaTeX backend.
2853
+ self.attributes['rowcount'] = str(len(self.rows))
2792
2854
  # Generate headrows, footrows, bodyrows.
2793
2855
  # Headrow, footrow and bodyrow data replaces same named attributes in
2794
2856
  # the table markup template. In order to ensure this data does not get
@@ -2796,25 +2858,20 @@ class Table(AbstractBlock):
2796
2858
  # already substituted inline passthroughs) unique placeholders are used
2797
2859
  # (the tab character does not appear elsewhere since it is expanded on
2798
2860
  # 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)
2861
+ headrows = footrows = bodyrows = None
2862
+ if self.rows and 'header' in self.parameters.options:
2863
+ headrows = self.subs_rows(self.rows[0:1],'header')
2809
2864
  self.attributes['headrows'] = '\theadrows\t'
2810
- if footrows:
2811
- footrows = self.parse_rows(footrows, self.footrow, self.footdata)
2812
- footrows = writer.newline.join(footrows)
2865
+ self.rows = self.rows[1:]
2866
+ if self.rows and 'footer' in self.parameters.options:
2867
+ footrows = self.subs_rows( self.rows[-1:], 'footer')
2813
2868
  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)
2869
+ self.rows = self.rows[:-1]
2870
+ if self.rows:
2871
+ bodyrows = self.subs_rows(self.rows)
2872
+ self.attributes['bodyrows'] = '\tbodyrows\t'
2873
+ table = subs_attrs(config.sections[self.parameters.template],
2874
+ self.attributes)
2818
2875
  table = writer.newline.join(table)
2819
2876
  # Before we finish replace the table head, foot and body place holders
2820
2877
  # with the real data.
@@ -2822,72 +2879,85 @@ class Table(AbstractBlock):
2822
2879
  table = table.replace('\theadrows\t', headrows, 1)
2823
2880
  if footrows:
2824
2881
  table = table.replace('\tfootrows\t', footrows, 1)
2825
- table = table.replace('\tbodyrows\t', bodyrows, 1)
2882
+ if bodyrows:
2883
+ table = table.replace('\tbodyrows\t', bodyrows, 1)
2826
2884
  writer.write(table)
2827
2885
 
2828
2886
  class Tables(AbstractBlocks):
2829
2887
  """List of tables."""
2830
2888
  BLOCK_TYPE = Table
2831
2889
  PREFIX = 'tabledef-'
2890
+ TAGS = ('colspec', 'headrow','footrow','bodyrow',
2891
+ 'headdata','footdata', 'bodydata','paragraph')
2832
2892
  def __init__(self):
2833
2893
  AbstractBlocks.__init__(self)
2894
+ # Table tags dictionary. Each entry is a tags dictionary.
2895
+ self.tags={}
2834
2896
  def load(self,sections):
2835
2897
  AbstractBlocks.load(self,sections)
2836
- """Update tables defined in 'sections' dictionary."""
2898
+ self.load_tags(sections)
2899
+ def load_tags(self,sections):
2900
+ """
2901
+ Load tabletags-* conf file sections to self.tags.
2902
+ """
2903
+ for section in sections.keys():
2904
+ mo = re.match(r'^tabletags-(?P<name>\w+)$',section)
2905
+ if mo:
2906
+ name = mo.group('name')
2907
+ if self.tags.has_key(name):
2908
+ d = self.tags[name]
2909
+ else:
2910
+ d = AttrDict()
2911
+ parse_entries(sections.get(section,()),d)
2912
+ for k in d.keys():
2913
+ if k not in self.TAGS:
2914
+ warning('[%s] contains illegal table tag: %s' %
2915
+ (section,k))
2916
+ self.tags[name] = d
2837
2917
  def validate(self):
2838
- # Does not call AbstractBlocks.validate().
2918
+ AbstractBlocks.validate(self)
2839
2919
  # Check we have a default table definition,
2840
2920
  for i in range(len(self.blocks)):
2841
2921
  if self.blocks[i].name == 'tabledef-default':
2842
2922
  default = self.blocks[i]
2843
2923
  break
2844
2924
  else:
2845
- raise EAsciiDoc,'missing [table-default] section'
2846
- # Set default table defaults.
2847
- if default.format is None: default.subs = 'fixed'
2925
+ raise EAsciiDoc,'missing [tabledef-default] section'
2848
2926
  # Propagate defaults to unspecified table parameters.
2849
2927
  for b in self.blocks:
2850
2928
  if b is not default:
2851
- if b.fillchar is None: b.fillchar = default.fillchar
2852
2929
  if b.format is None: b.format = default.format
2853
2930
  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)
2931
+ # Check tags and propagate default tags.
2932
+ if not 'default' in self.tags:
2933
+ raise EAsciiDoc,'missing [tabletags-default] section'
2934
+ default = self.tags['default']
2935
+ for tag in ('bodyrow','bodydata','paragraph'): # Mandatory default tags.
2936
+ if tag not in default:
2937
+ raise EAsciiDoc,'missing [tabletags-default] entry: %s' % tag
2938
+ for t in self.tags.values():
2939
+ if t is not default:
2940
+ if t.colspec is None: t.colspec = default.colspec
2941
+ if t.headrow is None: t.headrow = default.headrow
2942
+ if t.footrow is None: t.footrow = default.footrow
2943
+ if t.bodyrow is None: t.bodyrow = default.bodyrow
2944
+ if t.headdata is None: t.headdata = default.headdata
2945
+ if t.footdata is None: t.footdata = default.footdata
2946
+ if t.bodydata is None: t.bodydata = default.bodydata
2947
+ if t.paragraph is None: t.paragraph = default.paragraph
2948
+ # Use body tags if header and footer tags are not specified.
2949
+ for t in self.tags.values():
2950
+ if not t.headrow: t.headrow = t.bodyrow
2951
+ if not t.footrow: t.footrow = t.bodyrow
2952
+ if not t.headdata: t.headdata = t.bodydata
2953
+ if not t.footdata: t.footdata = t.bodydata
2885
2954
  # Check table definitions are valid.
2886
2955
  for b in self.blocks:
2887
2956
  b.validate()
2888
- if config.verbose:
2889
- if b.check_msg:
2890
- warning('[%s] table definition: %s' % (b.name,b.check_msg))
2957
+ def dump(self):
2958
+ AbstractBlocks.dump(self)
2959
+ for k,v in self.tags.items():
2960
+ dump_section('tabletags-'+k, v)
2891
2961
 
2892
2962
  class Macros:
2893
2963
  # Default system macro syntax.
@@ -2896,6 +2966,7 @@ class Macros:
2896
2966
  def __init__(self):
2897
2967
  self.macros = [] # List of Macros.
2898
2968
  self.current = None # The last matched block macro.
2969
+ self.passthroughs = []
2899
2970
  # Initialize default system macro.
2900
2971
  m = Macro()
2901
2972
  m.pattern = self.SYS_DEFAULT
@@ -2924,7 +2995,11 @@ class Macros:
2924
2995
  write('[macros]')
2925
2996
  # Dump all macros except the first (built-in system) macro.
2926
2997
  for m in self.macros[1:]:
2927
- write('%s=%s%s' % (m.pattern,m.prefix,m.name))
2998
+ # Escape = in pattern.
2999
+ macro = '%s=%s%s' % (m.pattern.replace('=',r'\='), m.prefix, m.name)
3000
+ if m.subslist is not None:
3001
+ macro += '[' + ','.join(m.subslist) + ']'
3002
+ write(macro)
2928
3003
  write('')
2929
3004
  def validate(self):
2930
3005
  # Check all named sections exist.
@@ -2964,72 +3039,20 @@ class Macros:
2964
3039
  if re.match(name,mo.group('name')):
2965
3040
  return mo
2966
3041
  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
3042
+ def extract_passthroughs(self,text,prefix=''):
3043
+ """ Extract the passthrough text and replace with temporary
3044
+ placeholders."""
3045
+ self.passthroughs = []
3046
+ for m in self.macros:
3047
+ if m.has_passthrough() and m.prefix == prefix:
3048
+ text = m.subs_passthroughs(text, self.passthroughs)
3049
+ return text
3050
+ def restore_passthroughs(self,text):
3051
+ """ Replace passthough placeholders with the original passthrough
3052
+ text."""
3053
+ for i,v in enumerate(self.passthroughs):
3054
+ text = text.replace('\t'+str(i)+'\t', self.passthroughs[i], 1)
3055
+ return text
3033
3056
 
3034
3057
  class Macro:
3035
3058
  def __init__(self):
@@ -3037,6 +3060,9 @@ class Macro:
3037
3060
  self.name = '' # Conf file macro name (None if implicit).
3038
3061
  self.prefix = '' # '' if inline, '+' if system, '#' if block.
3039
3062
  self.reo = None # Compiled pattern re object.
3063
+ self.subslist = None # Default subs for macros passtext group.
3064
+ def has_passthrough(self):
3065
+ return self.pattern.find(r'(?P<passtext>') >= 0
3040
3066
  def section_name(self,name=None):
3041
3067
  """Return macro markup template section name based on macro name and
3042
3068
  prefix. Return None section not found."""
@@ -3051,7 +3077,7 @@ class Macro:
3051
3077
  if config.sections.has_key(name+suffix):
3052
3078
  return name+suffix
3053
3079
  else:
3054
- warning('missing macro section: [%s]' % name+suffix)
3080
+ warning('missing macro section: [%s]' % (name+suffix))
3055
3081
  return None
3056
3082
  def equals(self,m):
3057
3083
  if self.pattern != m.pattern:
@@ -3066,29 +3092,161 @@ class Macro:
3066
3092
  if not e:
3067
3093
  raise EAsciiDoc,'malformed macro entry: %s' % entry
3068
3094
  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):
3095
+ raise EAsciiDoc,'illegal macro regular expression: %s' % e[0]
3096
+ pattern, name = e
3097
+ if name and name[0] in ('+','#'):
3098
+ prefix, name = name[0], name[1:]
3099
+ else:
3100
+ prefix = ''
3101
+ # Parse passthrough subslist.
3102
+ mo = re.match(r'^(?P<name>[^[]*)(\[(?P<subslist>.*)\])?$', name)
3103
+ name = mo.group('name')
3104
+ if name and not is_name(name):
3077
3105
  raise EAsciiDoc,'illegal section name in macro entry: %s' % entry
3106
+ subslist = mo.group('subslist')
3107
+ if subslist is not None:
3108
+ # Parse and validate passthrough subs.
3109
+ subslist = parse_options(subslist, SUBS_OPTIONS,
3110
+ 'illegal subs in macro entry: %s' % entry)
3111
+ self.pattern = pattern
3112
+ self.reo = re.compile(pattern)
3113
+ self.prefix = prefix
3114
+ self.name = name
3115
+ self.subslist = subslist
3116
+
3078
3117
  def subs(self,text):
3079
- global _macro
3080
- _macro = self # Pass the macro to _subs_macro().
3081
- return self.reo.sub(_subs_macro,text)
3118
+ def subs_func(mo):
3119
+ """Function called to perform inline macro substitution.
3120
+ Uses matched macro regular expression object and returns string
3121
+ containing the substituted macro body."""
3122
+ # Check if macro reference is escaped.
3123
+ if mo.group()[0] == '\\':
3124
+ return mo.group()[1:] # Strip leading backslash.
3125
+ d = mo.groupdict()
3126
+ # Delete groups that didn't participate in match.
3127
+ for k,v in d.items():
3128
+ if v is None: del d[k]
3129
+ if self.name:
3130
+ name = self.name
3131
+ else:
3132
+ if not d.has_key('name'):
3133
+ warning('missing macro name group: %s' % mo.re.pattern)
3134
+ return ''
3135
+ name = d['name']
3136
+ section_name = self.section_name(name)
3137
+ if not section_name:
3138
+ return ''
3139
+ # If we're dealing with a block macro get optional block ID and
3140
+ # block title.
3141
+ if self.prefix == '#':
3142
+ AttributeList.consume(d)
3143
+ BlockTitle.consume(d)
3144
+ # Parse macro attributes.
3145
+ if d.has_key('attrlist'):
3146
+ if d['attrlist'] in (None,''):
3147
+ del d['attrlist']
3148
+ else:
3149
+ if self.prefix == '':
3150
+ # Unescape ] characters in inline macros.
3151
+ d['attrlist'] = d['attrlist'].replace('\\]',']')
3152
+ parse_attributes(d['attrlist'],d)
3153
+ # Generate option attributes.
3154
+ if 'options' in d:
3155
+ options = parse_options(d['options'], (),
3156
+ '%s: illegal option name' % name)
3157
+ for option in options:
3158
+ d[option+'-option'] = ''
3159
+ if name == 'callout':
3160
+ listindex =int(d['index'])
3161
+ d['coid'] = calloutmap.add(listindex)
3162
+ # Unescape special characters in LaTeX target file names.
3163
+ if document.backend == 'latex' and d.has_key('target') and d['target']:
3164
+ if not d.has_key('0'):
3165
+ d['0'] = d['target']
3166
+ d['target']= config.subs_specialchars_reverse(d['target'])
3167
+ # BUG: We've already done attribute substitution on the macro which
3168
+ # means that any escaped attribute references are now unescaped and
3169
+ # will be substituted by config.subs_section() below. As a partial
3170
+ # fix have withheld {0} from substitution but this kludge doesn't
3171
+ # fix it for other attributes containing unescaped references.
3172
+ # Passthrough macros don't have this problem.
3173
+ a0 = d.get('0')
3174
+ if a0:
3175
+ d['0'] = chr(0) # Replace temporarily with unused character.
3176
+ body = config.subs_section(section_name,d)
3177
+ if len(body) == 0:
3178
+ result = ''
3179
+ elif len(body) == 1:
3180
+ result = body[0]
3181
+ else:
3182
+ if self.prefix == '#':
3183
+ result = writer.newline.join(body)
3184
+ else:
3185
+ # Internally processed inline macros use UNIX line
3186
+ # separator.
3187
+ result = '\n'.join(body)
3188
+ if a0:
3189
+ result = result.replace(chr(0), a0)
3190
+ return result
3191
+
3192
+ return self.reo.sub(subs_func, text)
3193
+
3082
3194
  def translate(self):
3083
3195
  """ Block macro translation."""
3084
3196
  assert self.prefix == '#'
3085
3197
  s = reader.read()
3086
- s = subs_attrs(s) # Substitute global attributes.
3198
+ if self.has_passthrough():
3199
+ s = macros.extract_passthroughs(s,'#')
3200
+ s = subs_attrs(s)
3087
3201
  if s:
3088
3202
  s = self.subs(s)
3203
+ if self.has_passthrough():
3204
+ s = macros.restore_passthroughs(s)
3089
3205
  if s:
3090
3206
  writer.write(s)
3091
3207
 
3208
+ def subs_passthroughs(self, text, passthroughs):
3209
+ """ Replace macro attribute lists in text with placeholders.
3210
+ Substitute and append the passthrough attribute lists to the
3211
+ passthroughs list."""
3212
+ def subs_func(mo):
3213
+ """Function called to perform inline macro substitution.
3214
+ Uses matched macro regular expression object and returns string
3215
+ containing the substituted macro body."""
3216
+ # Don't process escaped macro references.
3217
+ if mo.group()[0] == '\\':
3218
+ return mo.group()
3219
+ d = mo.groupdict()
3220
+ if not d.has_key('passtext'):
3221
+ warning('passthrough macro %s: missing passtext group' %
3222
+ d.get('name',''))
3223
+ return mo.group()
3224
+ passtext = d['passtext']
3225
+ if d.get('subslist'):
3226
+ if d['subslist'].startswith(':'):
3227
+ error('block macro cannot occur here: %s' % mo.group(),
3228
+ halt=True)
3229
+ subslist = parse_options(d['subslist'], SUBS_OPTIONS,
3230
+ 'illegal passthrough macro subs option')
3231
+ else:
3232
+ subslist = self.subslist
3233
+ passtext = Lex.subs_1(passtext,subslist)
3234
+ if passtext is None: passtext = ''
3235
+ if self.prefix == '':
3236
+ # Unescape ] characters in inline macros.
3237
+ passtext = passtext.replace('\\]',']')
3238
+ passthroughs.append(passtext)
3239
+ # Tabs guarantee the placeholders are unambiguous.
3240
+ result = (
3241
+ text[mo.start():mo.start('passtext')] +
3242
+ '\t' + str(len(passthroughs)-1) + '\t' +
3243
+ text[mo.end('passtext'):mo.end()]
3244
+ )
3245
+ return result
3246
+
3247
+ return self.reo.sub(subs_func, text)
3248
+
3249
+
3092
3250
  class CalloutMap:
3093
3251
  def __init__(self):
3094
3252
  self.comap = {} # key = list index, value = callouts list.
@@ -3119,7 +3277,7 @@ class CalloutMap:
3119
3277
  result += ' ' + self.calloutid(self.listnumber,coindex)
3120
3278
  return result.strip()
3121
3279
  else:
3122
- error('no callouts refer to list item '+str(listindex))
3280
+ warning('no callouts refer to list item '+str(listindex))
3123
3281
  return ''
3124
3282
  def validate(self,maxlistindex):
3125
3283
  # Check that all list indexes referenced by callouts exist.
@@ -3147,8 +3305,8 @@ class Reader1:
3147
3305
  self.tabsize = 8 # Tab expansion number of spaces.
3148
3306
  self.parent = None # Included reader's parent reader.
3149
3307
  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.
3308
+ self.current_depth = 0 # Current include depth.
3309
+ self.max_depth = 5 # Initial maxiumum allowed include depth.
3152
3310
  def open(self,fname):
3153
3311
  self.fname = fname
3154
3312
  verbose('reading: '+fname)
@@ -3196,12 +3354,13 @@ class Reader1:
3196
3354
  # Check for include macro.
3197
3355
  mo = macros.match('+',r'include[1]?',result)
3198
3356
  if mo and not skip:
3199
- # Perform attribute substitution on inlcude macro file name.
3357
+ # Don't process include macro once the maximum depth is reached.
3358
+ if self.current_depth >= self.max_depth:
3359
+ return result
3360
+ # Perform attribute substitution on include macro file name.
3200
3361
  fname = subs_attrs(mo.group('target'))
3201
3362
  if not fname:
3202
3363
  return Reader1.read(self) # Return next input line.
3203
- if self.include_depth >= self.include_max:
3204
- raise EAsciiDoc,'maxiumum inlcude depth exceeded'
3205
3364
  if self.fname != '<stdin>':
3206
3365
  fname = os.path.expandvars(os.path.expanduser(fname))
3207
3366
  fname = safe_filename(fname, os.path.dirname(self.fname))
@@ -3211,8 +3370,8 @@ class Reader1:
3211
3370
  if not config.dumping:
3212
3371
  # Store the include file in memory for later
3213
3372
  # retrieval by the {include1:} system attribute.
3214
- config.include1[fname] = \
3215
- [s.rstrip() for s in open(fname)]
3373
+ config.include1[fname] = [
3374
+ s.rstrip() for s in open(fname)]
3216
3375
  return '{include1:%s}' % fname
3217
3376
  else:
3218
3377
  # This is a configuration dump, just pass the macro
@@ -3225,11 +3384,21 @@ class Reader1:
3225
3384
  parent = Reader1()
3226
3385
  assign(parent,self)
3227
3386
  self.parent = parent
3387
+ # Set attributes in child.
3228
3388
  if attrs.has_key('tabsize'):
3229
- self.tabsize = int(validate(attrs['tabsize'],'int($)>=0', \
3389
+ self.tabsize = int(validate(attrs['tabsize'],
3390
+ 'int($)>=0',
3230
3391
  'illegal include macro tabsize argument'))
3392
+ else:
3393
+ self.tabsize = config.tabsize
3394
+ if attrs.has_key('depth'):
3395
+ attrs['depth'] = int(validate(attrs['depth'],
3396
+ 'int($)>=1',
3397
+ 'illegal include macro depth argument'))
3398
+ self.max_depth = self.current_depth + attrs['depth']
3399
+ # Process included file.
3231
3400
  self.open(fname)
3232
- self.include_depth = self.include_depth + 1
3401
+ self.current_depth = self.current_depth + 1
3233
3402
  result = Reader1.read(self)
3234
3403
  else:
3235
3404
  if not Reader1.eof(self):
@@ -3453,18 +3622,18 @@ class Writer:
3453
3622
  self.lines_out = self.lines_out + 1
3454
3623
  else:
3455
3624
  for arg in args:
3456
- if isinstance(arg,list) or isinstance(arg,tuple):
3625
+ if is_array(arg):
3457
3626
  for s in arg:
3458
3627
  self.write_line(s)
3459
3628
  elif arg is not None:
3460
3629
  self.write_line(arg)
3461
- def write_tag(self,tagname,content,subs=None,d=None):
3462
- """Write content enveloped by configuration file tag tagname.
3630
+ def write_tag(self,tag,content,subs=None,d=None):
3631
+ """Write content enveloped by tag.
3463
3632
  Substitutions specified in the 'subs' list are perform on the
3464
3633
  'content'."""
3465
3634
  if subs is None:
3466
3635
  subs = config.subsnormal
3467
- stag,etag = config.tag(tagname,d)
3636
+ stag,etag = subs_tag(tag,d)
3468
3637
  if stag:
3469
3638
  self.write(stag)
3470
3639
  if content:
@@ -3501,11 +3670,12 @@ def _subs_specialwords(mo):
3501
3670
 
3502
3671
  class Config:
3503
3672
  """Methods to process configuration files."""
3504
- # Predefined section name regexp's.
3505
- SPECIAL_SECTIONS= ('tags','miscellaneous','attributes','specialcharacters',
3673
+ # Non-template section name regexp's.
3674
+ ENTRIES_SECTIONS= ('tags','miscellaneous','attributes','specialcharacters',
3506
3675
  'specialwords','macros','replacements','quotes','titles',
3507
- r'paradef.+',r'listdef.+',r'blockdef.+',r'tabledef.*',
3508
- 'replacements2')
3676
+ r'paradef-.+',r'listdef-.+',r'blockdef-.+',r'tabledef-.+',
3677
+ r'tabletags-.+',r'listtags-.+','replacements2',
3678
+ r'old_tabledef-.+')
3509
3679
  def __init__(self):
3510
3680
  self.sections = OrderedDict() # Keyed by section name containing
3511
3681
  # lists of section lines.
@@ -3514,7 +3684,7 @@ class Config:
3514
3684
  self.header_footer = True # -s, --no-header-footer option.
3515
3685
  # [miscellaneous] section.
3516
3686
  self.tabsize = 8
3517
- self.textwidth = 70
3687
+ self.textwidth = 70 # DEPRECATED: Old tables only.
3518
3688
  self.newline = '\r\n'
3519
3689
  self.pagewidth = None
3520
3690
  self.pageunits = None
@@ -3530,7 +3700,7 @@ class Config:
3530
3700
  self.replacements2 = OrderedDict()
3531
3701
  self.specialsections = {} # Name is special section name pattern, value
3532
3702
  # is corresponding section name.
3533
- self.quotes = {} # Values contain corresponding tag name.
3703
+ self.quotes = OrderedDict() # Values contain corresponding tag name.
3534
3704
  self.fname = '' # Most recently loaded configuration file name.
3535
3705
  self.conf_attrs = {} # Glossary entries from conf files.
3536
3706
  self.cmd_attrs = {} # Attributes from command-line -a options.
@@ -3538,7 +3708,7 @@ class Config:
3538
3708
  self.include1 = {} # Holds include1::[] files for {include1:}.
3539
3709
  self.dumping = False # True if asciidoc -c option specified.
3540
3710
 
3541
- def load(self,fname,dir=None):
3711
+ def load_file(self,fname,dir=None):
3542
3712
  """Loads sections dictionary with sections from file fname.
3543
3713
  Existing sections are overlaid. Silently skips missing configuration
3544
3714
  files."""
@@ -3568,12 +3738,11 @@ class Config:
3568
3738
  if found:
3569
3739
  if section: # Store previous section.
3570
3740
  if sections.has_key(section) \
3571
- and self.is_special_section(section):
3741
+ and self.entries_section(section):
3572
3742
  if ''.join(contents):
3573
- # Merge special sections.
3743
+ # Merge entries.
3574
3744
  sections[section] = sections[section] + contents
3575
3745
  else:
3576
- print 'blank section'
3577
3746
  del sections[section]
3578
3747
  else:
3579
3748
  sections[section] = contents
@@ -3583,22 +3752,29 @@ class Config:
3583
3752
  contents.append(s)
3584
3753
  if section and contents: # Store last section.
3585
3754
  if sections.has_key(section) \
3586
- and self.is_special_section(section):
3755
+ and self.entries_section(section):
3587
3756
  if ''.join(contents):
3588
- # Merge special sections.
3757
+ # Merge entries.
3589
3758
  sections[section] = sections[section] + contents
3590
3759
  else:
3591
3760
  del sections[section]
3592
3761
  else:
3593
3762
  sections[section] = contents
3594
3763
  rdr.close()
3595
- # Delete blank lines from sections.
3764
+ self.load_sections(sections)
3765
+ self.loaded.append(os.path.realpath(fname))
3766
+
3767
+ def load_sections(self,sections):
3768
+ '''Loads sections dictionary. Each dictionary entry contains a
3769
+ list of lines.
3770
+ '''
3771
+ # Delete trailing blank lines from sections.
3596
3772
  for k in sections.keys():
3597
3773
  for i in range(len(sections[k])-1,-1,-1):
3598
3774
  if not sections[k][i]:
3599
3775
  del sections[k][i]
3600
- elif not self.is_special_section(k):
3601
- break # Only trailing blanks from non-special sections.
3776
+ elif not self.entries_section(k):
3777
+ break
3602
3778
  # Add/overwrite new sections.
3603
3779
  self.sections.update(sections)
3604
3780
  self.parse_tags()
@@ -3625,27 +3801,27 @@ class Config:
3625
3801
  paragraphs.load(sections)
3626
3802
  lists.load(sections)
3627
3803
  blocks.load(sections)
3804
+ tables_OLD.load(sections)
3628
3805
  tables.load(sections)
3629
3806
  macros.load(sections.get('macros',()))
3630
- self.loaded.append(os.path.realpath(fname))
3631
3807
 
3632
3808
  def load_all(self,dir):
3633
3809
  """Load the standard configuration files from directory 'dir'."""
3634
- self.load('asciidoc.conf',dir)
3810
+ self.load_file('asciidoc.conf',dir)
3635
3811
  conf = document.backend + '.conf'
3636
- self.load(conf,dir)
3812
+ self.load_file(conf,dir)
3637
3813
  conf = document.backend + '-' + document.doctype + '.conf'
3638
- self.load(conf,dir)
3814
+ self.load_file(conf,dir)
3639
3815
  lang = document.attributes.get('lang')
3640
3816
  if lang:
3641
3817
  conf = 'lang-' + lang + '.conf'
3642
- self.load(conf,dir)
3818
+ self.load_file(conf,dir)
3643
3819
  # Load ./filters/*.conf files if they exist.
3644
3820
  filters = os.path.join(dir,'filters')
3645
3821
  if os.path.isdir(filters):
3646
3822
  for f in os.listdir(filters):
3647
3823
  if re.match(r'^.+\.conf$',f):
3648
- self.load(f,filters)
3824
+ self.load_file(f,filters)
3649
3825
 
3650
3826
  def load_miscellaneous(self,d):
3651
3827
  """Set miscellaneous configuration entries from dictionary 'd'."""
@@ -3657,7 +3833,7 @@ class Config:
3657
3833
  else:
3658
3834
  setattr(self, name, validate(d[name],rule,errmsg))
3659
3835
  set_misc('tabsize','int($)>0',intval=True)
3660
- set_misc('textwidth','int($)>0',intval=True)
3836
+ set_misc('textwidth','int($)>0',intval=True) # DEPRECATED: Old tables only.
3661
3837
  set_misc('pagewidth','int($)>0',intval=True)
3662
3838
  set_misc('pageunits')
3663
3839
  set_misc('outfilesuffix')
@@ -3707,11 +3883,16 @@ class Config:
3707
3883
  paragraphs.validate()
3708
3884
  lists.validate()
3709
3885
  blocks.validate()
3886
+ tables_OLD.validate()
3710
3887
  tables.validate()
3711
3888
  macros.validate()
3712
3889
 
3713
- def is_special_section(self,section_name):
3714
- for name in self.SPECIAL_SECTIONS:
3890
+ def entries_section(self,section_name):
3891
+ """
3892
+ Return True if conf file section contains entries, not a markup
3893
+ template.
3894
+ """
3895
+ for name in self.ENTRIES_SECTIONS:
3715
3896
  if re.match(name,section_name):
3716
3897
  return True
3717
3898
  return False
@@ -3754,11 +3935,12 @@ class Config:
3754
3935
  paragraphs.dump()
3755
3936
  lists.dump()
3756
3937
  blocks.dump()
3938
+ tables_OLD.dump()
3757
3939
  tables.dump()
3758
3940
  macros.dump()
3759
3941
  # Dump remaining sections.
3760
3942
  for k in self.sections.keys():
3761
- if not self.is_special_section(k):
3943
+ if not self.entries_section(k):
3762
3944
  sys.stdout.write('[%s]%s' % (k,writer.newline))
3763
3945
  for line in self.sections[k]:
3764
3946
  sys.stdout.write('%s%s' % (line,writer.newline))
@@ -3782,7 +3964,7 @@ class Config:
3782
3964
  if v is None:
3783
3965
  if self.tags.has_key(k):
3784
3966
  del self.tags[k]
3785
- elif v == 'none':
3967
+ elif v == '':
3786
3968
  self.tags[k] = (None,None)
3787
3969
  else:
3788
3970
  mo = re.match(r'(?P<stag>.*)\|(?P<etag>.*)',v)
@@ -3836,19 +4018,25 @@ class Config:
3836
4018
 
3837
4019
  def parse_replacements(self,sect='replacements'):
3838
4020
  """Parse replacements section into self.replacements dictionary."""
3839
- replacements = getattr(self,sect)
3840
4021
  d = OrderedDict()
3841
4022
  parse_entries(self.sections.get(sect,()), d, unquote=True)
3842
4023
  for pat,rep in d.items():
3843
- pat = strip_quotes(pat)
3844
- if not is_regexp(pat):
4024
+ if not self.set_replacement(pat, rep, getattr(self,sect)):
3845
4025
  raise EAsciiDoc,'[%s] entry in %s is not a valid' \
3846
4026
  ' 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)
4027
+
4028
+ def set_replacement(pat, rep, replacements):
4029
+ """Add pattern and replacement to replacements dictionary."""
4030
+ pat = strip_quotes(pat)
4031
+ if not is_regexp(pat):
4032
+ return False
4033
+ if rep is None:
4034
+ if replacements.has_key(pat):
4035
+ del replacements[pat]
4036
+ else:
4037
+ replacements[pat] = strip_quotes(rep)
4038
+ return True
4039
+ set_replacement = staticmethod(set_replacement)
3852
4040
 
3853
4041
  def subs_replacements(self,s,sect='replacements'):
3854
4042
  """Substitute patterns from self.replacements in 's'."""
@@ -3970,14 +4158,514 @@ class Config:
3970
4158
  return (stag,etag)
3971
4159
 
3972
4160
 
4161
+ #---------------------------------------------------------------------------
4162
+ # Deprecated old table classes follow.
4163
+ # Naming convention is an _OLD name suffix.
4164
+ # These will be removed from future versions of AsciiDoc
4165
+ #
4166
+
4167
+ def join_lines_OLD(lines):
4168
+ """Return a list in which lines terminated with the backslash line
4169
+ continuation character are joined."""
4170
+ result = []
4171
+ s = ''
4172
+ continuation = False
4173
+ for line in lines:
4174
+ if line and line[-1] == '\\':
4175
+ s = s + line[:-1]
4176
+ continuation = True
4177
+ continue
4178
+ if continuation:
4179
+ result.append(s+line)
4180
+ s = ''
4181
+ continuation = False
4182
+ else:
4183
+ result.append(line)
4184
+ if continuation:
4185
+ result.append(s)
4186
+ return result
4187
+
4188
+ class Column_OLD:
4189
+ """Table column."""
4190
+ def __init__(self):
4191
+ self.colalign = None # 'left','right','center'
4192
+ self.rulerwidth = None
4193
+ self.colwidth = None # Output width in page units.
4194
+
4195
+ class Table_OLD(AbstractBlock):
4196
+ COL_STOP = r"(`|'|\.)" # RE.
4197
+ ALIGNMENTS = {'`':'left', "'":'right', '.':'center'}
4198
+ FORMATS = ('fixed','csv','dsv')
4199
+ def __init__(self):
4200
+ AbstractBlock.__init__(self)
4201
+ self.CONF_ENTRIES += ('template','fillchar','format','colspec',
4202
+ 'headrow','footrow','bodyrow','headdata',
4203
+ 'footdata', 'bodydata')
4204
+ # Configuration parameters.
4205
+ self.fillchar=None
4206
+ self.format=None # 'fixed','csv','dsv'
4207
+ self.colspec=None
4208
+ self.headrow=None
4209
+ self.footrow=None
4210
+ self.bodyrow=None
4211
+ self.headdata=None
4212
+ self.footdata=None
4213
+ self.bodydata=None
4214
+ # Calculated parameters.
4215
+ self.underline=None # RE matching current table underline.
4216
+ self.isnumeric=False # True if numeric ruler.
4217
+ self.tablewidth=None # Optional table width scale factor.
4218
+ self.columns=[] # List of Columns.
4219
+ # Other.
4220
+ self.check_msg='' # Message set by previous self.validate() call.
4221
+ def load(self,name,entries):
4222
+ AbstractBlock.load(self,name,entries)
4223
+ """Update table definition from section entries in 'entries'."""
4224
+ for k,v in entries.items():
4225
+ if k == 'fillchar':
4226
+ if v and len(v) == 1:
4227
+ self.fillchar = v
4228
+ else:
4229
+ raise EAsciiDoc,'malformed table fillchar: %s' % v
4230
+ elif k == 'format':
4231
+ if v in Table_OLD.FORMATS:
4232
+ self.format = v
4233
+ else:
4234
+ raise EAsciiDoc,'illegal table format: %s' % v
4235
+ elif k == 'colspec':
4236
+ self.colspec = v
4237
+ elif k == 'headrow':
4238
+ self.headrow = v
4239
+ elif k == 'footrow':
4240
+ self.footrow = v
4241
+ elif k == 'bodyrow':
4242
+ self.bodyrow = v
4243
+ elif k == 'headdata':
4244
+ self.headdata = v
4245
+ elif k == 'footdata':
4246
+ self.footdata = v
4247
+ elif k == 'bodydata':
4248
+ self.bodydata = v
4249
+ def dump(self):
4250
+ AbstractBlock.dump(self)
4251
+ write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
4252
+ write('fillchar='+self.fillchar)
4253
+ write('format='+self.format)
4254
+ if self.colspec:
4255
+ write('colspec='+self.colspec)
4256
+ if self.headrow:
4257
+ write('headrow='+self.headrow)
4258
+ if self.footrow:
4259
+ write('footrow='+self.footrow)
4260
+ write('bodyrow='+self.bodyrow)
4261
+ if self.headdata:
4262
+ write('headdata='+self.headdata)
4263
+ if self.footdata:
4264
+ write('footdata='+self.footdata)
4265
+ write('bodydata='+self.bodydata)
4266
+ write('')
4267
+ def validate(self):
4268
+ AbstractBlock.validate(self)
4269
+ """Check table definition and set self.check_msg if invalid else set
4270
+ self.check_msg to blank string."""
4271
+ # Check global table parameters.
4272
+ if config.textwidth is None:
4273
+ self.check_msg = 'missing [miscellaneous] textwidth entry'
4274
+ elif config.pagewidth is None:
4275
+ self.check_msg = 'missing [miscellaneous] pagewidth entry'
4276
+ elif config.pageunits is None:
4277
+ self.check_msg = 'missing [miscellaneous] pageunits entry'
4278
+ elif self.headrow is None:
4279
+ self.check_msg = 'missing headrow entry'
4280
+ elif self.footrow is None:
4281
+ self.check_msg = 'missing footrow entry'
4282
+ elif self.bodyrow is None:
4283
+ self.check_msg = 'missing bodyrow entry'
4284
+ elif self.headdata is None:
4285
+ self.check_msg = 'missing headdata entry'
4286
+ elif self.footdata is None:
4287
+ self.check_msg = 'missing footdata entry'
4288
+ elif self.bodydata is None:
4289
+ self.check_msg = 'missing bodydata entry'
4290
+ else:
4291
+ # No errors.
4292
+ self.check_msg = ''
4293
+ def isnext(self):
4294
+ return AbstractBlock.isnext(self)
4295
+ def parse_ruler(self,ruler):
4296
+ """Parse ruler calculating underline and ruler column widths."""
4297
+ fc = re.escape(self.fillchar)
4298
+ # Strip and save optional tablewidth from end of ruler.
4299
+ mo = re.match(r'^(.*'+fc+r'+)([\d\.]+)$',ruler)
4300
+ if mo:
4301
+ ruler = mo.group(1)
4302
+ self.tablewidth = float(mo.group(2))
4303
+ self.attributes['tablewidth'] = str(float(self.tablewidth))
4304
+ else:
4305
+ self.tablewidth = None
4306
+ self.attributes['tablewidth'] = '100.0'
4307
+ # Guess whether column widths are specified numerically or not.
4308
+ if ruler[1] != self.fillchar:
4309
+ # If the first column does not start with a fillchar then numeric.
4310
+ self.isnumeric = True
4311
+ elif ruler[1:] == self.fillchar*len(ruler[1:]):
4312
+ # The case of one column followed by fillchars is numeric.
4313
+ self.isnumeric = True
4314
+ else:
4315
+ self.isnumeric = False
4316
+ # Underlines must be 3 or more fillchars.
4317
+ self.underline = r'^' + fc + r'{3,}$'
4318
+ splits = re.split(self.COL_STOP,ruler)[1:]
4319
+ # Build self.columns.
4320
+ for i in range(0,len(splits),2):
4321
+ c = Column_OLD()
4322
+ c.colalign = self.ALIGNMENTS[splits[i]]
4323
+ s = splits[i+1]
4324
+ if self.isnumeric:
4325
+ # Strip trailing fillchars.
4326
+ s = re.sub(fc+r'+$','',s)
4327
+ if s == '':
4328
+ c.rulerwidth = None
4329
+ else:
4330
+ c.rulerwidth = int(validate(s,'int($)>0',
4331
+ 'malformed ruler: bad width'))
4332
+ else: # Calculate column width from inter-fillchar intervals.
4333
+ if not re.match(r'^'+fc+r'+$',s):
4334
+ raise EAsciiDoc,'malformed ruler: illegal fillchars'
4335
+ c.rulerwidth = len(s)+1
4336
+ self.columns.append(c)
4337
+ # Fill in unspecified ruler widths.
4338
+ if self.isnumeric:
4339
+ if self.columns[0].rulerwidth is None:
4340
+ prevwidth = 1
4341
+ for c in self.columns:
4342
+ if c.rulerwidth is None:
4343
+ c.rulerwidth = prevwidth
4344
+ prevwidth = c.rulerwidth
4345
+ def build_colspecs(self):
4346
+ """Generate colwidths and colspecs. This can only be done after the
4347
+ table arguments have been parsed since we use the table format."""
4348
+ self.attributes['cols'] = len(self.columns)
4349
+ # Calculate total ruler width.
4350
+ totalwidth = 0
4351
+ for c in self.columns:
4352
+ totalwidth = totalwidth + c.rulerwidth
4353
+ if totalwidth <= 0:
4354
+ raise EAsciiDoc,'zero width table'
4355
+ # Calculate marked up colwidths from rulerwidths.
4356
+ for c in self.columns:
4357
+ # Convert ruler width to output page width.
4358
+ width = float(c.rulerwidth)
4359
+ if self.format == 'fixed':
4360
+ if self.tablewidth is None:
4361
+ # Size proportional to ruler width.
4362
+ colfraction = width/config.textwidth
4363
+ else:
4364
+ # Size proportional to page width.
4365
+ colfraction = width/totalwidth
4366
+ else:
4367
+ # Size proportional to page width.
4368
+ colfraction = width/totalwidth
4369
+ c.colwidth = colfraction * config.pagewidth # To page units.
4370
+ if self.tablewidth is not None:
4371
+ c.colwidth = c.colwidth * self.tablewidth # Scale factor.
4372
+ if self.tablewidth > 1:
4373
+ c.colwidth = c.colwidth/100 # tablewidth is in percent.
4374
+ # Build colspecs.
4375
+ if self.colspec:
4376
+ cols = []
4377
+ i = 0
4378
+ for c in self.columns:
4379
+ i += 1
4380
+ self.attributes['colalign'] = c.colalign
4381
+ self.attributes['colwidth'] = str(int(c.colwidth))
4382
+ self.attributes['colnumber'] = str(i + 1)
4383
+ s = subs_attrs(self.colspec,self.attributes)
4384
+ if not s:
4385
+ warning('colspec dropped: contains undefined attribute')
4386
+ else:
4387
+ cols.append(s)
4388
+ self.attributes['colspecs'] = writer.newline.join(cols)
4389
+ def split_rows(self,rows):
4390
+ """Return a two item tuple containing a list of lines up to but not
4391
+ including the next underline (continued lines are joined ) and the
4392
+ tuple of all lines after the underline."""
4393
+ reo = re.compile(self.underline)
4394
+ i = 0
4395
+ while not reo.match(rows[i]):
4396
+ i = i+1
4397
+ if i == 0:
4398
+ raise EAsciiDoc,'missing table rows'
4399
+ if i >= len(rows):
4400
+ raise EAsciiDoc,'closing [%s] underline expected' % self.name
4401
+ return (join_lines_OLD(rows[:i]), rows[i+1:])
4402
+ def parse_rows(self, rows, rtag, dtag):
4403
+ """Parse rows list using the row and data tags. Returns a substituted
4404
+ list of output lines."""
4405
+ result = []
4406
+ # Source rows are parsed as single block, rather than line by line, to
4407
+ # allow the CSV reader to handle multi-line rows.
4408
+ if self.format == 'fixed':
4409
+ rows = self.parse_fixed(rows)
4410
+ elif self.format == 'csv':
4411
+ rows = self.parse_csv(rows)
4412
+ elif self.format == 'dsv':
4413
+ rows = self.parse_dsv(rows)
4414
+ else:
4415
+ assert True,'illegal table format'
4416
+ # Substitute and indent all data in all rows.
4417
+ stag,etag = subs_tag(rtag,self.attributes)
4418
+ for row in rows:
4419
+ result.append(' '+stag)
4420
+ for data in self.subs_row(row,dtag):
4421
+ result.append(' '+data)
4422
+ result.append(' '+etag)
4423
+ return result
4424
+ def subs_row(self, data, dtag):
4425
+ """Substitute the list of source row data elements using the data tag.
4426
+ Returns a substituted list of output table data items."""
4427
+ result = []
4428
+ if len(data) < len(self.columns):
4429
+ warning('fewer row data items then table columns')
4430
+ if len(data) > len(self.columns):
4431
+ warning('more row data items than table columns')
4432
+ for i in range(len(self.columns)):
4433
+ if i > len(data) - 1:
4434
+ d = '' # Fill missing column data with blanks.
4435
+ else:
4436
+ d = data[i]
4437
+ c = self.columns[i]
4438
+ self.attributes['colalign'] = c.colalign
4439
+ self.attributes['colwidth'] = str(int(c.colwidth))
4440
+ self.attributes['colnumber'] = str(i + 1)
4441
+ stag,etag = subs_tag(dtag,self.attributes)
4442
+ # Insert AsciiDoc line break (' +') where row data has newlines
4443
+ # ('\n'). This is really only useful when the table format is csv
4444
+ # and the output markup is HTML. It's also a bit dubious in that it
4445
+ # assumes the user has not modified the shipped line break pattern.
4446
+ subs = self.get_subs()[0]
4447
+ if 'replacements' in subs:
4448
+ # Insert line breaks in cell data.
4449
+ d = re.sub(r'(?m)\n',r' +\n',d)
4450
+ d = d.split('\n') # So writer.newline is written.
4451
+ else:
4452
+ d = [d]
4453
+ result = result + [stag] + Lex.subs(d,subs) + [etag]
4454
+ return result
4455
+ def parse_fixed(self,rows):
4456
+ """Parse the list of source table rows. Each row item in the returned
4457
+ list contains a list of cell data elements."""
4458
+ result = []
4459
+ for row in rows:
4460
+ data = []
4461
+ start = 0
4462
+ # build an encoded representation
4463
+ row = char_decode(row)
4464
+ for c in self.columns:
4465
+ end = start + c.rulerwidth
4466
+ if c is self.columns[-1]:
4467
+ # Text in last column can continue forever.
4468
+ # Use the encoded string to slice, but convert back
4469
+ # to plain string before further processing
4470
+ data.append(char_encode(row[start:]).strip())
4471
+ else:
4472
+ data.append(char_encode(row[start:end]).strip())
4473
+ start = end
4474
+ result.append(data)
4475
+ return result
4476
+ def parse_csv(self,rows):
4477
+ """Parse the list of source table rows. Each row item in the returned
4478
+ list contains a list of cell data elements."""
4479
+ import StringIO
4480
+ import csv
4481
+ result = []
4482
+ rdr = csv.reader(StringIO.StringIO('\r\n'.join(rows)),
4483
+ skipinitialspace=True)
4484
+ try:
4485
+ for row in rdr:
4486
+ result.append(row)
4487
+ except:
4488
+ raise EAsciiDoc,'csv parse error: %s' % row
4489
+ return result
4490
+ def parse_dsv(self,rows):
4491
+ """Parse the list of source table rows. Each row item in the returned
4492
+ list contains a list of cell data elements."""
4493
+ separator = self.attributes.get('separator',':')
4494
+ separator = eval('"'+separator+'"')
4495
+ if len(separator) != 1:
4496
+ raise EAsciiDoc,'malformed dsv separator: %s' % separator
4497
+ # TODO If separator is preceeded by an odd number of backslashes then
4498
+ # it is escaped and should not delimit.
4499
+ result = []
4500
+ for row in rows:
4501
+ # Skip blank lines
4502
+ if row == '': continue
4503
+ # Unescape escaped characters.
4504
+ row = eval('"'+row.replace('"','\\"')+'"')
4505
+ data = row.split(separator)
4506
+ data = [s.strip() for s in data]
4507
+ result.append(data)
4508
+ return result
4509
+ def translate(self):
4510
+ deprecated('old tables syntax')
4511
+ AbstractBlock.translate(self)
4512
+ # Reset instance specific properties.
4513
+ self.underline = None
4514
+ self.columns = []
4515
+ attrs = {}
4516
+ BlockTitle.consume(attrs)
4517
+ # Add relevant globals to table substitutions.
4518
+ attrs['pagewidth'] = str(config.pagewidth)
4519
+ attrs['pageunits'] = config.pageunits
4520
+ # Mix in document attribute list.
4521
+ AttributeList.consume(attrs)
4522
+ # Validate overridable attributes.
4523
+ for k,v in attrs.items():
4524
+ if k == 'format':
4525
+ if v not in self.FORMATS:
4526
+ raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.name,k,v)
4527
+ self.format = v
4528
+ elif k == 'tablewidth':
4529
+ try:
4530
+ self.tablewidth = float(attrs['tablewidth'])
4531
+ except:
4532
+ raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.name,k,v)
4533
+ self.merge_attributes(attrs)
4534
+ # Parse table ruler.
4535
+ ruler = reader.read()
4536
+ assert re.match(self.delimiter,ruler)
4537
+ self.parse_ruler(ruler)
4538
+ # Read the entire table.
4539
+ table = []
4540
+ while True:
4541
+ line = reader.read_next()
4542
+ # Table terminated by underline followed by a blank line or EOF.
4543
+ if len(table) > 0 and re.match(self.underline,table[-1]):
4544
+ if line in ('',None):
4545
+ break;
4546
+ if line is None:
4547
+ raise EAsciiDoc,'closing [%s] underline expected' % self.name
4548
+ table.append(reader.read())
4549
+ # EXPERIMENTAL: The number of lines in the table, requested by Benjamin Klum.
4550
+ self.attributes['rows'] = str(len(table))
4551
+ #TODO: Inherited validate() doesn't set check_msg, needs checking.
4552
+ if self.check_msg: # Skip if table definition was marked invalid.
4553
+ warning('skipping %s table: %s' % (self.name,self.check_msg))
4554
+ return
4555
+ # Generate colwidths and colspecs.
4556
+ self.build_colspecs()
4557
+ # Generate headrows, footrows, bodyrows.
4558
+ # Headrow, footrow and bodyrow data replaces same named attributes in
4559
+ # the table markup template. In order to ensure this data does not get
4560
+ # a second attribute substitution (which would interfere with any
4561
+ # already substituted inline passthroughs) unique placeholders are used
4562
+ # (the tab character does not appear elsewhere since it is expanded on
4563
+ # input) which are replaced after template attribute substitution.
4564
+ headrows = footrows = []
4565
+ bodyrows,table = self.split_rows(table)
4566
+ if table:
4567
+ headrows = bodyrows
4568
+ bodyrows,table = self.split_rows(table)
4569
+ if table:
4570
+ footrows,table = self.split_rows(table)
4571
+ if headrows:
4572
+ headrows = self.parse_rows(headrows, self.headrow, self.headdata)
4573
+ headrows = writer.newline.join(headrows)
4574
+ self.attributes['headrows'] = '\theadrows\t'
4575
+ if footrows:
4576
+ footrows = self.parse_rows(footrows, self.footrow, self.footdata)
4577
+ footrows = writer.newline.join(footrows)
4578
+ self.attributes['footrows'] = '\tfootrows\t'
4579
+ bodyrows = self.parse_rows(bodyrows, self.bodyrow, self.bodydata)
4580
+ bodyrows = writer.newline.join(bodyrows)
4581
+ self.attributes['bodyrows'] = '\tbodyrows\t'
4582
+ table = subs_attrs(config.sections[self.template],self.attributes)
4583
+ table = writer.newline.join(table)
4584
+ # Before we finish replace the table head, foot and body place holders
4585
+ # with the real data.
4586
+ if headrows:
4587
+ table = table.replace('\theadrows\t', headrows, 1)
4588
+ if footrows:
4589
+ table = table.replace('\tfootrows\t', footrows, 1)
4590
+ table = table.replace('\tbodyrows\t', bodyrows, 1)
4591
+ writer.write(table)
4592
+
4593
+ class Tables_OLD(AbstractBlocks):
4594
+ """List of tables."""
4595
+ BLOCK_TYPE = Table_OLD
4596
+ PREFIX = 'old_tabledef-'
4597
+ def __init__(self):
4598
+ AbstractBlocks.__init__(self)
4599
+ def load(self,sections):
4600
+ AbstractBlocks.load(self,sections)
4601
+ def validate(self):
4602
+ # Does not call AbstractBlocks.validate().
4603
+ # Check we have a default table definition,
4604
+ for i in range(len(self.blocks)):
4605
+ if self.blocks[i].name == 'old_tabledef-default':
4606
+ default = self.blocks[i]
4607
+ break
4608
+ else:
4609
+ raise EAsciiDoc,'missing [OLD_tabledef-default] section'
4610
+ # Set default table defaults.
4611
+ if default.format is None: default.subs = 'fixed'
4612
+ # Propagate defaults to unspecified table parameters.
4613
+ for b in self.blocks:
4614
+ if b is not default:
4615
+ if b.fillchar is None: b.fillchar = default.fillchar
4616
+ if b.format is None: b.format = default.format
4617
+ if b.template is None: b.template = default.template
4618
+ if b.colspec is None: b.colspec = default.colspec
4619
+ if b.headrow is None: b.headrow = default.headrow
4620
+ if b.footrow is None: b.footrow = default.footrow
4621
+ if b.bodyrow is None: b.bodyrow = default.bodyrow
4622
+ if b.headdata is None: b.headdata = default.headdata
4623
+ if b.footdata is None: b.footdata = default.footdata
4624
+ if b.bodydata is None: b.bodydata = default.bodydata
4625
+ # Check all tables have valid fill character.
4626
+ for b in self.blocks:
4627
+ if not b.fillchar or len(b.fillchar) != 1:
4628
+ raise EAsciiDoc,'[%s] missing or illegal fillchar' % b.name
4629
+ # Build combined tables delimiter patterns and assign defaults.
4630
+ delimiters = []
4631
+ for b in self.blocks:
4632
+ # Ruler is:
4633
+ # (ColStop,(ColWidth,FillChar+)?)+, FillChar+, TableWidth?
4634
+ b.delimiter = r'^(' + Table_OLD.COL_STOP \
4635
+ + r'(\d*|' + re.escape(b.fillchar) + r'*)' \
4636
+ + r')+' \
4637
+ + re.escape(b.fillchar) + r'+' \
4638
+ + '([\d\.]*)$'
4639
+ delimiters.append(b.delimiter)
4640
+ if not b.headrow:
4641
+ b.headrow = b.bodyrow
4642
+ if not b.footrow:
4643
+ b.footrow = b.bodyrow
4644
+ if not b.headdata:
4645
+ b.headdata = b.bodydata
4646
+ if not b.footdata:
4647
+ b.footdata = b.bodydata
4648
+ self.delimiter = join_regexp(delimiters)
4649
+ # Check table definitions are valid.
4650
+ for b in self.blocks:
4651
+ b.validate()
4652
+ if config.verbose:
4653
+ if b.check_msg:
4654
+ warning('[%s] table definition: %s' % (b.name,b.check_msg))
4655
+
4656
+ # End of deprecated old table classes.
4657
+ #---------------------------------------------------------------------------
4658
+
3973
4659
  #---------------------------------------------------------------------------
3974
4660
  # Application code.
3975
4661
  #---------------------------------------------------------------------------
3976
4662
  # Constants
3977
4663
  # ---------
4664
+ APP_FILE = None # This file's full path.
3978
4665
  APP_DIR = None # This file's directory.
3979
4666
  USER_DIR = None # ~/.asciidoc
3980
- CONF_DIR = '/etc/asciidoc' # Global configuration files directory.
4667
+ # Global configuration files directory (set by Makefile build target).
4668
+ CONF_DIR = '/etc/asciidoc'
3981
4669
  HELP_FILE = 'help.conf' # Default (English) help file.
3982
4670
 
3983
4671
  # Globals
@@ -3989,6 +4677,7 @@ writer = Writer() # Output stream line writer.
3989
4677
  paragraphs = Paragraphs() # Paragraph definitions.
3990
4678
  lists = Lists() # List definitions.
3991
4679
  blocks = DelimitedBlocks() # DelimitedBlock definitions.
4680
+ tables_OLD = Tables_OLD() # Table_OLD definitions.
3992
4681
  tables = Tables() # Table definitions.
3993
4682
  macros = Macros() # Macro definitions.
3994
4683
  calloutmap = CalloutMap() # Coordinates callouts and callout list.
@@ -4031,13 +4720,13 @@ def asciidoc(backend, doctype, confiles, infile, outfile, options):
4031
4720
  config.load_all(os.path.dirname(infile))
4032
4721
  if infile != '<stdin>':
4033
4722
  # 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')
4723
+ config.load_file(os.path.splitext(infile)[0] + '.conf')
4724
+ config.load_file(os.path.splitext(infile)[0] + '-' + backend + '.conf')
4036
4725
  # If user specified configuration file(s) overlay the defaults.
4037
4726
  if confiles:
4038
4727
  for conf in confiles:
4039
4728
  if os.path.isfile(conf):
4040
- config.load(conf)
4729
+ config.load_file(conf)
4041
4730
  else:
4042
4731
  raise EAsciiDoc,'configuration file %s missing' % conf
4043
4732
  document.init_attrs() # Add conf files.
@@ -4131,8 +4820,9 @@ def main():
4131
4820
  print_stderr('FAILED: Python 2.3 or better required.')
4132
4821
  sys.exit(1)
4133
4822
  # 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]))
4823
+ global APP_FILE,APP_DIR,USER_DIR
4824
+ APP_FILE = os.path.realpath(sys.argv[0])
4825
+ APP_DIR = os.path.dirname(APP_FILE)
4136
4826
  USER_DIR = os.environ.get('HOME')
4137
4827
  if USER_DIR is not None:
4138
4828
  USER_DIR = os.path.join(USER_DIR,'.asciidoc')