FooBarWidget-mizuho 0.9.1 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
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')