mizuho 0.9.10 → 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. data/README.markdown +18 -2
  2. data/asciidoc/BUGS +3 -6
  3. data/asciidoc/BUGS.txt +0 -3
  4. data/asciidoc/CHANGELOG +660 -397
  5. data/asciidoc/CHANGELOG.txt +237 -2
  6. data/asciidoc/INSTALL +14 -14
  7. data/asciidoc/MANIFEST +2 -0
  8. data/asciidoc/Makefile.in +9 -1
  9. data/asciidoc/README +2 -2
  10. data/asciidoc/a2x.py +101 -43
  11. data/asciidoc/asciidoc.conf +18 -11
  12. data/asciidoc/asciidoc.py +615 -260
  13. data/asciidoc/common.aap +2 -2
  14. data/asciidoc/configure +9 -9
  15. data/asciidoc/configure.ac +1 -1
  16. data/asciidoc/doc/a2x.1 +34 -4
  17. data/asciidoc/doc/a2x.1.txt +12 -0
  18. data/asciidoc/doc/article.pdf +0 -0
  19. data/asciidoc/doc/asciidoc.1 +73 -29
  20. data/asciidoc/doc/asciidoc.1.txt +56 -30
  21. data/asciidoc/doc/asciidoc.dict +23 -2
  22. data/asciidoc/doc/asciidoc.txt +468 -327
  23. data/asciidoc/doc/book.epub +0 -0
  24. data/asciidoc/doc/faq.txt +201 -25
  25. data/asciidoc/doc/latex-filter.pdf +0 -0
  26. data/asciidoc/doc/music-filter.pdf +0 -0
  27. data/asciidoc/doc/publishing-ebooks-with-asciidoc.txt +1 -1
  28. data/asciidoc/doc/source-highlight-filter.pdf +0 -0
  29. data/asciidoc/doc/source-highlight-filter.txt +48 -37
  30. data/asciidoc/docbook45.conf +4 -4
  31. data/asciidoc/examples/website/ASCIIMathML.js +938 -0
  32. data/asciidoc/examples/website/CHANGELOG.txt +3056 -0
  33. data/asciidoc/examples/website/INSTALL.txt +227 -0
  34. data/asciidoc/examples/website/LaTeXMathML.js +1223 -0
  35. data/asciidoc/examples/website/README-website.txt +29 -0
  36. data/asciidoc/examples/website/README.txt +35 -0
  37. data/asciidoc/examples/website/a2x.1.txt +358 -0
  38. data/asciidoc/examples/website/asciidoc-docbook-xsl.txt +65 -0
  39. data/asciidoc/examples/website/asciidoc-graphviz-sample.txt +170 -0
  40. data/asciidoc/examples/website/asciidoc.css +533 -0
  41. data/asciidoc/examples/website/asciidoc.js +189 -0
  42. data/asciidoc/examples/website/asciidocapi.txt +189 -0
  43. data/asciidoc/examples/website/asciimathml.txt +61 -0
  44. data/asciidoc/examples/website/build-website.sh +25 -0
  45. data/asciidoc/examples/website/customers.csv +18 -0
  46. data/asciidoc/examples/website/epub-notes.txt +210 -0
  47. data/asciidoc/examples/website/faq.txt +1298 -0
  48. data/asciidoc/examples/website/index.txt +502 -0
  49. data/asciidoc/examples/website/latex-backend.txt +192 -0
  50. data/asciidoc/examples/website/latex-bugs.txt +134 -0
  51. data/asciidoc/examples/website/latex-filter.txt +196 -0
  52. data/asciidoc/examples/website/latexmathml.txt +41 -0
  53. data/asciidoc/examples/website/layout1.conf +153 -0
  54. data/asciidoc/examples/website/layout1.css +65 -0
  55. data/asciidoc/examples/website/layout2.conf +153 -0
  56. data/asciidoc/examples/website/layout2.css +83 -0
  57. data/asciidoc/examples/website/main.aap +159 -0
  58. data/asciidoc/examples/website/manpage.txt +197 -0
  59. data/asciidoc/examples/website/music-filter.txt +148 -0
  60. data/asciidoc/examples/website/newlists.txt +40 -0
  61. data/asciidoc/examples/website/newtables.txt +743 -0
  62. data/asciidoc/examples/website/plugins.txt +91 -0
  63. data/asciidoc/examples/website/publishing-ebooks-with-asciidoc.txt +398 -0
  64. data/asciidoc/examples/website/slidy-example.txt +167 -0
  65. data/asciidoc/examples/website/slidy.txt +113 -0
  66. data/asciidoc/examples/website/source-highlight-filter.txt +239 -0
  67. data/asciidoc/examples/website/support.txt +5 -0
  68. data/asciidoc/examples/website/testasciidoc.txt +231 -0
  69. data/asciidoc/examples/website/userguide.txt +5991 -0
  70. data/asciidoc/examples/website/version83.txt +37 -0
  71. data/asciidoc/examples/website/xhtml11-quirks.css +43 -0
  72. data/asciidoc/filters/latex/latex2png.py +28 -12
  73. data/asciidoc/filters/music/music2png.py +22 -6
  74. data/asciidoc/filters/source/source-highlight-filter.conf +7 -5
  75. data/asciidoc/help.conf +147 -131
  76. data/asciidoc/html4.conf +1 -0
  77. data/asciidoc/html5.conf +37 -39
  78. data/asciidoc/javascripts/asciidoc.js +3 -3
  79. data/asciidoc/lang-de.conf +4 -0
  80. data/asciidoc/lang-es.conf +2 -0
  81. data/asciidoc/lang-fr.conf +1 -1
  82. data/asciidoc/lang-hu.conf +2 -0
  83. data/asciidoc/lang-it.conf +2 -0
  84. data/asciidoc/lang-nl.conf +5 -0
  85. data/asciidoc/lang-pt-BR.conf +2 -0
  86. data/asciidoc/lang-ru.conf +2 -3
  87. data/asciidoc/latex.conf +2 -2
  88. data/asciidoc/slidy.conf +4 -2
  89. data/asciidoc/stylesheets/asciidoc.css +29 -4
  90. data/asciidoc/stylesheets/docbook-xsl.css +12 -5
  91. data/asciidoc/stylesheets/toc2.css +1 -0
  92. data/asciidoc/stylesheets/xhtml11-quirks.css +1 -1
  93. data/asciidoc/tests/data/lang-de-man-test.txt +21 -0
  94. data/asciidoc/tests/data/lang-en-man-test.txt +21 -0
  95. data/asciidoc/tests/data/lang-es-man-test.txt +21 -0
  96. data/asciidoc/tests/data/lang-fr-man-test.txt +21 -0
  97. data/asciidoc/tests/data/lang-hu-man-test.txt +21 -0
  98. data/asciidoc/tests/data/lang-it-man-test.txt +21 -0
  99. data/asciidoc/tests/data/lang-it-test.txt +106 -0
  100. data/asciidoc/tests/data/lang-nl-man-test.txt +21 -0
  101. data/asciidoc/tests/data/lang-pt-BR-man-test.txt +21 -0
  102. data/asciidoc/tests/data/lang-ru-man-test.txt +21 -0
  103. data/asciidoc/tests/data/lang-uk-man-test.txt +21 -0
  104. data/asciidoc/tests/data/testcases.conf +10 -0
  105. data/asciidoc/tests/data/testcases.txt +40 -0
  106. data/asciidoc/tests/testasciidoc.conf +143 -17
  107. data/asciidoc/tests/testasciidoc.py +11 -2
  108. data/asciidoc/{stylesheets → themes/flask}/flask.css +0 -0
  109. data/asciidoc/{stylesheets → themes/volnitsky}/volnitsky.css +1 -1
  110. data/asciidoc/vim/ftdetect/asciidoc_filetype.vim +1 -1
  111. data/asciidoc/vim/syntax/asciidoc.vim +1 -1
  112. data/asciidoc/xhtml11-quirks.conf +2 -2
  113. data/asciidoc/xhtml11.conf +35 -37
  114. data/lib/mizuho.rb +1 -1
  115. data/lib/mizuho/generator.rb +3 -1
  116. data/source-highlight/darwin/source-highlight +0 -0
  117. data/templates/juvia.js +30 -5
  118. metadata +58 -9
  119. data/asciidoc/stylesheets/asciidoc-manpage.css +0 -18
  120. data/asciidoc/stylesheets/flask-manpage.css +0 -1
  121. data/asciidoc/stylesheets/volnitsky-manpage.css +0 -1
@@ -15,6 +15,7 @@ newline=\r\n
15
15
  backend-alias-html=xhtml11
16
16
  backend-alias-docbook=docbook45
17
17
  toclevels=2
18
+ toc-placement=auto
18
19
  sectids=
19
20
  iconsdir=./images/icons
20
21
  encoding=UTF-8
@@ -25,6 +26,7 @@ encoding=UTF-8
25
26
  # Uncomment to use deprecated quote attributes.
26
27
  #deprecated-quotes=
27
28
  empty=
29
+ sp=" "
28
30
  # Attribute and AttributeList element patterns.
29
31
  attributeentry-pattern=^:(?P<attrname>\w[^.]*?)(\.(?P<attrname2>.*?))?:(\s+(?P<attrvalue>.*))?$
30
32
  attributelist-pattern=(?u)(^\[\[(?P<id>[\w_:][\w_:.-]*)(,(?P<reftext>.*?))?\]\]$)|(^\[(?P<attrlist>.*)\]$)
@@ -169,10 +171,10 @@ template::[paragraph-styles]
169
171
 
170
172
  [paragraph-styles]
171
173
  normal-style=template="paragraph"
172
- verse-style=template="verseparagraph",posattrs=["style","attribution","citetitle"]
173
- quote-style=template="quoteparagraph",posattrs=["style","attribution","citetitle"]
174
- literal-style=template="literalparagraph",subs=["verbatim"]
175
- listing-style=template="listingparagraph",subs=["verbatim"]
174
+ verse-style=template="verseparagraph",posattrs=("style","attribution","citetitle")
175
+ quote-style=template="quoteparagraph",posattrs=("style","attribution","citetitle")
176
+ literal-style=template="literalparagraph",subs=("verbatim",)
177
+ listing-style=template="listingparagraph",subs=("verbatim",)
176
178
  NOTE-style=template="admonitionparagraph",name="note",caption="{note-caption}"
177
179
  TIP-style=template="admonitionparagraph",name="tip",caption="{tip-caption}"
178
180
  IMPORTANT-style=template="admonitionparagraph",name="important",caption="{important-caption}"
@@ -267,7 +269,7 @@ endif::no-inline-literal[]
267
269
  # Block macros
268
270
  #-------------
269
271
  # Macros using default syntax.
270
- (?u)^(?P<name>image|unfloat)::(?P<target>\S*?)(\[(?P<attrlist>.*?)\])$=#
272
+ (?u)^(?P<name>image|unfloat|toc)::(?P<target>\S*?)(\[(?P<attrlist>.*?)\])$=#
271
273
 
272
274
  # Passthrough macros.
273
275
  (?u)^(?P<name>pass)::(?P<subslist>\S*?)(\[(?P<passtext>.*?)\])$=#
@@ -276,8 +278,9 @@ endif::no-inline-literal[]
276
278
  ^<{3,}$=#pagebreak
277
279
  ^//(?P<passtext>[^/].*|)$=#comment[specialcharacters]
278
280
 
279
- [unfloat-blockmacro]
280
281
  # Implemented in HTML backends.
282
+ [unfloat-blockmacro]
283
+ [toc-blockmacro]
281
284
 
282
285
  #-----------------
283
286
  # Delimited blocks
@@ -285,6 +288,7 @@ endif::no-inline-literal[]
285
288
  [blockdef-comment]
286
289
  delimiter=^/{4,}$
287
290
  options=skip
291
+ posattrs=style
288
292
 
289
293
  [blockdef-sidebar]
290
294
  delimiter=^\*{4,}$
@@ -309,7 +313,7 @@ template=passblock
309
313
  # Default subs choosen for backward compatibility.
310
314
  subs=attributes,macros
311
315
  posattrs=style
312
- pass-style=template="passblock",subs=[]
316
+ pass-style=template="passblock",subs=()
313
317
 
314
318
  [blockdef-listing]
315
319
  delimiter=^-{4,}$
@@ -510,12 +514,12 @@ posattrs=style
510
514
  template=table
511
515
  default-style=tags="default"
512
516
  verse-style=tags="verse"
513
- literal-style=tags="literal",subs=["specialcharacters"]
517
+ literal-style=tags="literal",subs=("specialcharacters",)
514
518
  emphasis-style=tags="emphasis"
515
519
  strong-style=tags="strong"
516
520
  monospaced-style=tags="monospaced"
517
521
  header-style=tags="header"
518
- asciidoc-style=tags="asciidoc",subs=[],filter='python "{asciidoc-file}" -b {backend} {asciidoc-args}{lang? -a "lang={lang}@"}{icons? -a icons -a "iconsdir={iconsdir}"}{imagesdir? -a "imagesdir={imagesdir}"}{data-uri? -a data-uri} -a "indir={indir}"{trace? -a "trace={trace}"} -s -'
522
+ asciidoc-style=tags="asciidoc",subs=(),filter='"{python}" "{asciidoc-file}" -b {backend} {asciidoc-args}{lang? -a "lang={lang}@"}{icons? -a icons -a "iconsdir={iconsdir}"}{imagesdir? -a "imagesdir={imagesdir}"}{data-uri? -a data-uri} -a "indir={indir}"{trace? -a "trace={trace}"}{blockname? -a "blockname={blockname}"} -s -'
519
523
 
520
524
  [tabledef-nested]
521
525
  # Same as [tabledef-default] but with different delimiter and separator.
@@ -524,12 +528,12 @@ separator=((?<!\S)((?P<span>[\d.]+)(?P<op>[*+]))?(?P<align>[<\^>.]{,3})?(?P<styl
524
528
  posattrs=style
525
529
  template=table
526
530
  verse-style=tags="verse"
527
- literal-style=tags="literal",subs=["specialcharacters"]
531
+ literal-style=tags="literal",subs=("specialcharacters",)
528
532
  emphasis-style=tags="emphasis"
529
533
  strong-style=tags="strong"
530
534
  monospaced-style=tags="monospaced"
531
535
  header-style=tags="header"
532
- asciidoc-style=tags="asciidoc",subs=[],filter='python "{asciidoc-file}" -b {backend} {asciidoc-args}{lang? -a "lang={lang}@"} -s -'
536
+ asciidoc-style=tags="asciidoc",subs=(),filter='"{python}" "{asciidoc-file}" -b {backend} {asciidoc-args}{lang? -a "lang={lang}@"}{icons? -a icons -a "iconsdir={iconsdir}"}{imagesdir? -a "imagesdir={imagesdir}"}{data-uri? -a data-uri} -a "indir={indir}"{trace? -a "trace={trace}"}{blockname? -a "blockname={blockname}"} -s -'
533
537
 
534
538
  #----------------------------------------
535
539
  # Common block and macro markup templates
@@ -560,6 +564,9 @@ template::[pass-blockmacro]
560
564
  |
561
565
  template::[image-blockmacro]
562
566
 
567
+ [+docinfo]
568
+ # Blank section to suppress missing template warning.
569
+
563
570
  #----------------------------------
564
571
  # Default special section templates
565
572
  #----------------------------------
data/asciidoc/asciidoc.py CHANGED
@@ -6,12 +6,12 @@ Copyright (C) 2002-2010 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, subprocess, codecs, locale, unicodedata
9
+ import sys, os, re, time, traceback, tempfile, subprocess, codecs, locale, unicodedata, copy
10
10
 
11
11
  ### Used by asciidocapi.py ###
12
- VERSION = '8.6.5' # See CHANGLOG file for version history.
12
+ VERSION = '8.6.7' # See CHANGLOG file for version history.
13
13
 
14
- MIN_PYTHON_VERSION = 2.4 # Require this version of Python or better.
14
+ MIN_PYTHON_VERSION = '2.4' # Require this version of Python or better.
15
15
 
16
16
  #---------------------------------------------------------------------------
17
17
  # Program constants.
@@ -22,7 +22,7 @@ DEFAULT_DOCTYPE = 'article'
22
22
  # definition subs entry.
23
23
  SUBS_OPTIONS = ('specialcharacters','quotes','specialwords',
24
24
  'replacements', 'attributes','macros','callouts','normal','verbatim',
25
- 'none','replacements2')
25
+ 'none','replacements2','replacements3')
26
26
  # Default value for unspecified subs and presubs configuration file entries.
27
27
  SUBS_NORMAL = ('specialcharacters','quotes','attributes',
28
28
  'specialwords','replacements','macros','replacements2')
@@ -133,7 +133,7 @@ class Trace(object):
133
133
  """
134
134
  SUBS_NAMES = ('specialcharacters','quotes','specialwords',
135
135
  'replacements', 'attributes','macros','callouts',
136
- 'replacements2')
136
+ 'replacements2','replacements3')
137
137
  def __init__(self):
138
138
  self.name_re = '' # Regexp pattern to match trace names.
139
139
  self.linenos = True
@@ -255,7 +255,7 @@ def safe():
255
255
  return document.safe
256
256
 
257
257
  def is_safe_file(fname, directory=None):
258
- # A safe file must reside in directory directory (defaults to the source
258
+ # A safe file must reside in 'directory' (defaults to the source
259
259
  # file directory).
260
260
  if directory is None:
261
261
  if document.infile == '<stdin>':
@@ -273,15 +273,12 @@ def is_safe_file(fname, directory=None):
273
273
  def safe_filename(fname, parentdir):
274
274
  """
275
275
  Return file name which must reside in the parent file directory.
276
- Return None if file is not found or not safe.
276
+ Return None if file is not safe.
277
277
  """
278
278
  if not os.path.isabs(fname):
279
279
  # Include files are relative to parent document
280
280
  # directory.
281
281
  fname = os.path.normpath(os.path.join(parentdir,fname))
282
- if not os.path.isfile(fname):
283
- message.warning('include file not found: %s' % fname)
284
- return None
285
282
  if not is_safe_file(fname, parentdir):
286
283
  message.unsafe('include file: %s' % fname)
287
284
  return None
@@ -319,16 +316,6 @@ def re_join(relist):
319
316
  result = '('+result+')'
320
317
  return result
321
318
 
322
- def validate(value,rule,errmsg):
323
- """Validate value against rule expression. Throw EAsciiDoc exception with
324
- errmsg if validation fails."""
325
- try:
326
- if not eval(rule.replace('$',str(value))):
327
- raise EAsciiDoc,errmsg
328
- except Exception:
329
- raise EAsciiDoc,errmsg
330
- return value
331
-
332
319
  def lstrip_list(s):
333
320
  """
334
321
  Return list with empty items from start of list removed.
@@ -386,6 +373,102 @@ def dovetail_tags(stag,content,etag):
386
373
  include extraneous opening and closing line breaks."""
387
374
  return dovetail(dovetail(stag,content), etag)
388
375
 
376
+ # The following functions are so we don't have to use the dangerous
377
+ # built-in eval() function.
378
+ if float(sys.version[:3]) >= 2.6 or sys.platform[:4] == 'java':
379
+ # Use AST module if CPython >= 2.6 or Jython.
380
+ import ast
381
+ from ast import literal_eval
382
+
383
+ def get_args(val):
384
+ d = {}
385
+ args = ast.parse("d(" + val + ")", mode='eval').body.args
386
+ i = 1
387
+ for arg in args:
388
+ if isinstance(arg, ast.Name):
389
+ d[str(i)] = literal_eval(arg.id)
390
+ else:
391
+ d[str(i)] = literal_eval(arg)
392
+ i += 1
393
+ return d
394
+
395
+ def get_kwargs(val):
396
+ d = {}
397
+ args = ast.parse("d(" + val + ")", mode='eval').body.keywords
398
+ for arg in args:
399
+ d[arg.arg] = literal_eval(arg.value)
400
+ return d
401
+
402
+ def parse_to_list(val):
403
+ values = ast.parse("[" + val + "]", mode='eval').body.elts
404
+ return [literal_eval(v) for v in values]
405
+
406
+ else: # Use deprecated CPython compiler module.
407
+ import compiler
408
+ from compiler.ast import Const, Dict, Expression, Name, Tuple, UnarySub, Keyword
409
+
410
+ # Code from:
411
+ # http://mail.python.org/pipermail/python-list/2009-September/1219992.html
412
+ # Modified to use compiler.ast.List as this module has a List
413
+ def literal_eval(node_or_string):
414
+ """
415
+ Safely evaluate an expression node or a string containing a Python
416
+ expression. The string or node provided may only consist of the
417
+ following Python literal structures: strings, numbers, tuples,
418
+ lists, dicts, booleans, and None.
419
+ """
420
+ _safe_names = {'None': None, 'True': True, 'False': False}
421
+ if isinstance(node_or_string, basestring):
422
+ node_or_string = compiler.parse(node_or_string, mode='eval')
423
+ if isinstance(node_or_string, Expression):
424
+ node_or_string = node_or_string.node
425
+ def _convert(node):
426
+ if isinstance(node, Const) and isinstance(node.value,
427
+ (basestring, int, float, long, complex)):
428
+ return node.value
429
+ elif isinstance(node, Tuple):
430
+ return tuple(map(_convert, node.nodes))
431
+ elif isinstance(node, compiler.ast.List):
432
+ return list(map(_convert, node.nodes))
433
+ elif isinstance(node, Dict):
434
+ return dict((_convert(k), _convert(v)) for k, v
435
+ in node.items)
436
+ elif isinstance(node, Name):
437
+ if node.name in _safe_names:
438
+ return _safe_names[node.name]
439
+ elif isinstance(node, UnarySub):
440
+ return -_convert(node.expr)
441
+ raise ValueError('malformed string')
442
+ return _convert(node_or_string)
443
+
444
+ def get_args(val):
445
+ d = {}
446
+ args = compiler.parse("d(" + val + ")", mode='eval').node.args
447
+ i = 1
448
+ for arg in args:
449
+ if isinstance(arg, Keyword):
450
+ break
451
+ d[str(i)] = literal_eval(arg)
452
+ i = i + 1
453
+ return d
454
+
455
+ def get_kwargs(val):
456
+ d = {}
457
+ args = compiler.parse("d(" + val + ")", mode='eval').node.args
458
+ i = 0
459
+ for arg in args:
460
+ if isinstance(arg, Keyword):
461
+ break
462
+ i += 1
463
+ args = args[i:]
464
+ for arg in args:
465
+ d[str(arg.name)] = literal_eval(arg.expr)
466
+ return d
467
+
468
+ def parse_to_list(val):
469
+ values = compiler.parse("[" + val + "]", mode='eval').node.asList()
470
+ return [literal_eval(v) for v in values]
471
+
389
472
  def parse_attributes(attrs,dict):
390
473
  """Update a dictionary with name/value attributes from the attrs string.
391
474
  The attrs string is a comma separated list of values and keyword name=value
@@ -414,9 +497,10 @@ def parse_attributes(attrs,dict):
414
497
  dict['0'] = attrs
415
498
  # Replace line separators with spaces so line spanning works.
416
499
  s = re.sub(r'\s', ' ', attrs)
500
+ d = {}
417
501
  try:
418
- d = eval('f('+s+')')
419
- # Attributes must evaluate to strings, numbers or None.
502
+ d.update(get_args(s))
503
+ d.update(get_kwargs(s))
420
504
  for v in d.values():
421
505
  if not (isinstance(v,str) or isinstance(v,int) or isinstance(v,float) or v is None):
422
506
  raise Exception
@@ -426,7 +510,9 @@ def parse_attributes(attrs,dict):
426
510
  s = map(lambda x: '"' + x.strip() + '"', s)
427
511
  s = ','.join(s)
428
512
  try:
429
- d = eval('f('+s+')')
513
+ d = {}
514
+ d.update(get_args(s))
515
+ d.update(get_kwargs(s))
430
516
  except Exception:
431
517
  return # If there's a syntax error leave with {0}=attrs.
432
518
  for k in d.keys(): # Drop any empty positional arguments.
@@ -444,7 +530,8 @@ def parse_named_attributes(s,attrs):
444
530
  def f(**keywords): return keywords
445
531
 
446
532
  try:
447
- d = eval('f('+s+')')
533
+ d = {}
534
+ d = get_kwargs(s)
448
535
  attrs.update(d)
449
536
  return True
450
537
  except Exception:
@@ -454,7 +541,7 @@ def parse_list(s):
454
541
  """Parse comma separated string of Python literals. Return a tuple of of
455
542
  parsed values."""
456
543
  try:
457
- result = eval('tuple(['+s+'])')
544
+ result = tuple(parse_to_list(s))
458
545
  except Exception:
459
546
  raise EAsciiDoc,'malformed list: '+s
460
547
  return result
@@ -712,15 +799,19 @@ def filter_lines(filter_cmd, lines, attrs={}):
712
799
  message.warning('filter not found: %s' % cmd)
713
800
  if found:
714
801
  filter_cmd = '"' + found + '"' + mo.group('tail')
715
- if sys.platform == 'win32':
716
- # Windows doesn't like running scripts directly so explicitly
717
- # specify interpreter.
718
- if found:
719
- if cmd.endswith('.py'):
720
- filter_cmd = 'python ' + filter_cmd
721
- elif cmd.endswith('.rb'):
722
- filter_cmd = 'ruby ' + filter_cmd
802
+ if found:
803
+ if cmd.endswith('.py'):
804
+ filter_cmd = '"%s" %s' % (document.attributes['python'],
805
+ filter_cmd)
806
+ elif cmd.endswith('.rb'):
807
+ filter_cmd = 'ruby ' + filter_cmd
808
+
723
809
  message.verbose('filtering: ' + filter_cmd)
810
+ if os.name == 'nt':
811
+ # Remove redundant quoting -- this is not just
812
+ # cosmetic, unnecessary quoting appears to cause
813
+ # command line truncation.
814
+ filter_cmd = re.sub(r'"([^ ]+?)"', r'\1', filter_cmd)
724
815
  try:
725
816
  p = subprocess.Popen(filter_cmd, shell=True,
726
817
  stdin=subprocess.PIPE, stdout=subprocess.PIPE)
@@ -791,14 +882,24 @@ def system(name, args, is_macro=False, attrs=None):
791
882
  os.close(fd)
792
883
  try:
793
884
  cmd = args
794
- cmd = cmd + (' > %s' % tmp)
885
+ cmd = cmd + (' > "%s"' % tmp)
795
886
  if name == 'sys2':
796
887
  cmd = cmd + ' 2>&1'
888
+ if os.name == 'nt':
889
+ # Remove redundant quoting -- this is not just
890
+ # cosmetic, unnecessary quoting appears to cause
891
+ # command line truncation.
892
+ cmd = re.sub(r'"([^ ]+?)"', r'\1', cmd)
893
+ message.verbose('shelling: %s' % cmd)
797
894
  if os.system(cmd):
798
895
  message.warning('%s: non-zero exit status' % syntax)
799
896
  try:
800
897
  if os.path.isfile(tmp):
801
- lines = [s.rstrip() for s in open(tmp)]
898
+ f = open(tmp)
899
+ try:
900
+ lines = [s.rstrip() for s in f]
901
+ finally:
902
+ f.close()
802
903
  else:
803
904
  lines = []
804
905
  except Exception:
@@ -867,7 +968,11 @@ def system(name, args, is_macro=False, attrs=None):
867
968
  elif not is_safe_file(args):
868
969
  message.unsafe(syntax)
869
970
  else:
870
- result = [s.rstrip() for s in open(args)]
971
+ f = open(args)
972
+ try:
973
+ result = [s.rstrip() for s in f]
974
+ finally:
975
+ f.close()
871
976
  if result:
872
977
  result = subs_attrs(result)
873
978
  result = separator.join(result)
@@ -1258,7 +1363,7 @@ class Lex:
1258
1363
  result = subs_quotes(result)
1259
1364
  elif o == 'specialwords':
1260
1365
  result = config.subs_specialwords(result)
1261
- elif o in ('replacements','replacements2'):
1366
+ elif o in ('replacements','replacements2','replacements3'):
1262
1367
  result = config.subs_replacements(result,o)
1263
1368
  elif o == 'macros':
1264
1369
  result = macros.subs(result)
@@ -1349,7 +1454,10 @@ class Document(object):
1349
1454
  self.attributes['asciidoc-version'] = VERSION
1350
1455
  self.attributes['asciidoc-file'] = APP_FILE
1351
1456
  self.attributes['asciidoc-dir'] = APP_DIR
1352
- self.attributes['asciidoc-confdir'] = CONF_DIR
1457
+ if localapp():
1458
+ self.attributes['asciidoc-confdir'] = APP_DIR
1459
+ else:
1460
+ self.attributes['asciidoc-confdir'] = CONF_DIR
1353
1461
  self.attributes['user-dir'] = USER_DIR
1354
1462
  if config.verbose:
1355
1463
  self.attributes['verbose'] = ''
@@ -1518,7 +1626,7 @@ class Document(object):
1518
1626
  self.attributes['manpurpose'] = mo.group('manpurpose').strip()
1519
1627
  names = [s.strip() for s in self.attributes['manname'].split(',')]
1520
1628
  if len(names) > 9:
1521
- message.warning('to many manpage names')
1629
+ message.warning('too many manpage names')
1522
1630
  for i,name in enumerate(names):
1523
1631
  self.attributes['manname%d' % (i+1)] = name
1524
1632
  if has_header:
@@ -1844,14 +1952,13 @@ class AttributeList:
1844
1952
  reo = re.compile(r"^'.*'$")
1845
1953
  for k,v in attrs.items():
1846
1954
  if reo.match(str(v)):
1847
- attrs[k] = Lex.subs_1(v[1:-1],SUBS_NORMAL)
1955
+ attrs[k] = Lex.subs_1(v[1:-1], config.subsnormal)
1848
1956
  @staticmethod
1849
1957
  def style():
1850
1958
  return AttributeList.attrs.get('style') or AttributeList.attrs.get('1')
1851
1959
  @staticmethod
1852
- def consume(d):
1853
- """Add attribute list to the dictionary 'd' and reset the
1854
- list."""
1960
+ def consume(d={}):
1961
+ """Add attribute list to the dictionary 'd' and reset the list."""
1855
1962
  if AttributeList.attrs:
1856
1963
  d.update(AttributeList.attrs)
1857
1964
  AttributeList.attrs = {}
@@ -1890,7 +1997,7 @@ class BlockTitle:
1890
1997
  message.warning('blank block title')
1891
1998
  BlockTitle.title = s
1892
1999
  @staticmethod
1893
- def consume(d):
2000
+ def consume(d={}):
1894
2001
  """If there is a title add it to dictionary 'd' then reset title."""
1895
2002
  if BlockTitle.title:
1896
2003
  d['title'] = BlockTitle.title
@@ -2133,23 +2240,27 @@ class Section:
2133
2240
  """
2134
2241
  # Replace non-alpha numeric characters in title with underscores and
2135
2242
  # convert to lower case.
2136
- base_ident = char_encode(re.sub(r'(?u)\W+', '_',
2137
- char_decode(title)).strip('_').lower())
2243
+ base_id = re.sub(r'(?u)\W+', '_', char_decode(title)).strip('_').lower()
2244
+ if 'ascii-ids' in document.attributes:
2245
+ # Replace non-ASCII characters with ASCII equivalents.
2246
+ import unicodedata
2247
+ base_id = unicodedata.normalize('NFKD', base_id).encode('ascii','ignore')
2248
+ base_id = char_encode(base_id)
2138
2249
  # Prefix the ID name with idprefix attribute or underscore if not
2139
2250
  # defined. Prefix ensures the ID does not clash with existing IDs.
2140
2251
  idprefix = document.attributes.get('idprefix','_')
2141
- base_ident = idprefix + base_ident
2252
+ base_id = idprefix + base_id
2142
2253
  i = 1
2143
2254
  while True:
2144
2255
  if i == 1:
2145
- ident = base_ident
2256
+ id = base_id
2146
2257
  else:
2147
- ident = '%s_%d' % (base_ident, i)
2148
- if ident not in Section.ids:
2149
- Section.ids.append(ident)
2150
- return ident
2258
+ id = '%s_%d' % (base_id, i)
2259
+ if id not in Section.ids:
2260
+ Section.ids.append(id)
2261
+ return id
2151
2262
  else:
2152
- ident = base_ident
2263
+ id = base_id
2153
2264
  i += 1
2154
2265
  @staticmethod
2155
2266
  def set_id():
@@ -2212,12 +2323,15 @@ class Section:
2212
2323
  message.error('empty section is not valid')
2213
2324
 
2214
2325
  class AbstractBlock:
2326
+
2327
+ blocknames = [] # Global stack of names for push_blockname() and pop_blockname().
2328
+
2215
2329
  def __init__(self):
2216
2330
  # Configuration parameter names common to all blocks.
2217
2331
  self.CONF_ENTRIES = ('delimiter','options','subs','presubs','postsubs',
2218
2332
  'posattrs','style','.*-style','template','filter')
2219
2333
  self.start = None # File reader cursor at start delimiter.
2220
- self.name=None # Configuration file section name.
2334
+ self.defname=None # Configuration file block definition section name.
2221
2335
  # Configuration parameters.
2222
2336
  self.delimiter=None # Regular expression matching block delimiter.
2223
2337
  self.delimiter_reo=None # Compiled delimiter.
@@ -2241,14 +2355,14 @@ class AbstractBlock:
2241
2355
  # Leading delimiter match object.
2242
2356
  self.mo=None
2243
2357
  def short_name(self):
2244
- """ Return the text following the last dash in the section name."""
2245
- i = self.name.rfind('-')
2358
+ """ Return the text following the first dash in the section name."""
2359
+ i = self.defname.find('-')
2246
2360
  if i == -1:
2247
- return self.name
2361
+ return self.defname
2248
2362
  else:
2249
- return self.name[i+1:]
2363
+ return self.defname[i+1:]
2250
2364
  def error(self, msg, cursor=None, halt=False):
2251
- message.error('[%s] %s' % (self.name,msg), cursor, halt)
2365
+ message.error('[%s] %s' % (self.defname,msg), cursor, halt)
2252
2366
  def is_conf_entry(self,param):
2253
2367
  """Return True if param matches an allowed configuration file entry
2254
2368
  name."""
@@ -2256,9 +2370,9 @@ class AbstractBlock:
2256
2370
  if re.match('^'+s+'$',param):
2257
2371
  return True
2258
2372
  return False
2259
- def load(self,name,entries):
2373
+ def load(self,defname,entries):
2260
2374
  """Update block definition from section 'entries' dictionary."""
2261
- self.name = name
2375
+ self.defname = defname
2262
2376
  self.update_parameters(entries, self, all=True)
2263
2377
  def update_parameters(self, src, dst=None, all=False):
2264
2378
  """
@@ -2267,7 +2381,7 @@ class AbstractBlock:
2267
2381
  If all is True then copy src entries that aren't parameter names.
2268
2382
  """
2269
2383
  dst = dst or self.parameters
2270
- msg = '[%s] malformed entry %%s: %%s' % self.name
2384
+ msg = '[%s] malformed entry %%s: %%s' % self.defname
2271
2385
  def copy(obj,k,v):
2272
2386
  if isinstance(obj,dict):
2273
2387
  obj[k] = v
@@ -2346,7 +2460,7 @@ class AbstractBlock:
2346
2460
  def dump(self):
2347
2461
  """Write block definition to stdout."""
2348
2462
  write = lambda s: sys.stdout.write('%s%s' % (s,writer.newline))
2349
- write('['+self.name+']')
2463
+ write('['+self.defname+']')
2350
2464
  if self.is_conf_entry('delimiter'):
2351
2465
  write('delimiter='+self.delimiter)
2352
2466
  if self.template:
@@ -2374,14 +2488,14 @@ class AbstractBlock:
2374
2488
  def validate(self):
2375
2489
  """Validate block after the complete configuration has been loaded."""
2376
2490
  if self.is_conf_entry('delimiter') and not self.delimiter:
2377
- raise EAsciiDoc,'[%s] missing delimiter' % self.name
2491
+ raise EAsciiDoc,'[%s] missing delimiter' % self.defname
2378
2492
  if self.style:
2379
2493
  if not is_name(self.style):
2380
2494
  raise EAsciiDoc, 'illegal style name: %s' % self.style
2381
2495
  if not self.style in self.styles:
2382
2496
  if not isinstance(self,List): # Lists don't have templates.
2383
2497
  message.warning('[%s] \'%s\' style not in %s' % (
2384
- self.name,self.style,self.styles.keys()))
2498
+ self.defname,self.style,self.styles.keys()))
2385
2499
  # Check all styles for missing templates.
2386
2500
  all_styles_have_template = True
2387
2501
  for k,v in self.styles.items():
@@ -2403,7 +2517,7 @@ class AbstractBlock:
2403
2517
  % self.template)
2404
2518
  elif not all_styles_have_template:
2405
2519
  if not isinstance(self,List): # Lists don't have templates.
2406
- message.warning('missing styles templates: [%s]' % self.name)
2520
+ message.warning('missing styles templates: [%s]' % self.defname)
2407
2521
  def isnext(self):
2408
2522
  """Check if this block is next in document reader."""
2409
2523
  result = False
@@ -2423,9 +2537,31 @@ class AbstractBlock:
2423
2537
  self.presubs = config.subsnormal
2424
2538
  if reader.cursor:
2425
2539
  self.start = reader.cursor[:]
2540
+ def push_blockname(self, blockname=None):
2541
+ '''
2542
+ On block entry set the 'blockname' attribute.
2543
+ Only applies to delimited blocks, lists and tables.
2544
+ '''
2545
+ if blockname is None:
2546
+ blockname = self.attributes.get('style', self.short_name()).lower()
2547
+ trace('push blockname', blockname)
2548
+ self.blocknames.append(blockname)
2549
+ document.attributes['blockname'] = blockname
2550
+ def pop_blockname(self):
2551
+ '''
2552
+ On block exits restore previous (parent) 'blockname' attribute or
2553
+ undefine it if we're no longer inside a block.
2554
+ '''
2555
+ assert len(self.blocknames) > 0
2556
+ blockname = self.blocknames.pop()
2557
+ trace('pop blockname', blockname)
2558
+ if len(self.blocknames) == 0:
2559
+ document.attributes['blockname'] = None
2560
+ else:
2561
+ document.attributes['blockname'] = self.blocknames[-1]
2426
2562
  def merge_attributes(self,attrs,params=[]):
2427
2563
  """
2428
- Use the current blocks attribute list (attrs dictionary) to build a
2564
+ Use the current block's attribute list (attrs dictionary) to build a
2429
2565
  dictionary of block processing parameters (self.parameters) and tag
2430
2566
  substitution attributes (self.attributes).
2431
2567
 
@@ -2452,8 +2588,8 @@ class AbstractBlock:
2452
2588
  def check_array_parameter(param):
2453
2589
  # Check the parameter is a sequence type.
2454
2590
  if not is_array(self.parameters[param]):
2455
- message.error('malformed presubs attribute: %s' %
2456
- self.parameters[param])
2591
+ message.error('malformed %s parameter: %s' %
2592
+ (param, self.parameters[param]))
2457
2593
  # Revert to default value.
2458
2594
  self.parameters[param] = getattr(self,param)
2459
2595
 
@@ -2484,7 +2620,7 @@ class AbstractBlock:
2484
2620
  style = self.style
2485
2621
  # Lists have implicit styles and do their own style checks.
2486
2622
  elif style not in self.styles and not isinstance(self,List):
2487
- message.warning('missing style: [%s]: %s' % (self.name,style))
2623
+ message.warning('missing style: [%s]: %s' % (self.defname,style))
2488
2624
  style = self.style
2489
2625
  if style in self.styles:
2490
2626
  self.attributes['style'] = style
@@ -2522,7 +2658,7 @@ class AbstractBlocks:
2522
2658
  d = {}
2523
2659
  parse_entries(sections.get(k,()),d)
2524
2660
  for b in self.blocks:
2525
- if b.name == k:
2661
+ if b.defname == k:
2526
2662
  break
2527
2663
  else:
2528
2664
  b = self.BLOCK_TYPE()
@@ -2612,7 +2748,7 @@ class Paragraphs(AbstractBlocks):
2612
2748
  AbstractBlocks.validate(self)
2613
2749
  # Check we have a default paragraph definition, put it last in list.
2614
2750
  for b in self.blocks:
2615
- if b.name == 'paradef-default':
2751
+ if b.defname == 'paradef-default':
2616
2752
  self.blocks.append(b)
2617
2753
  self.default = b
2618
2754
  self.blocks.remove(b)
@@ -2627,7 +2763,7 @@ class List(AbstractBlock):
2627
2763
  AbstractBlock.__init__(self)
2628
2764
  self.CONF_ENTRIES += ('type','tags')
2629
2765
  self.PARAM_NAMES += ('tags',)
2630
- # tabledef conf file parameters.
2766
+ # listdef conf file parameters.
2631
2767
  self.type=None
2632
2768
  self.tags=None # Name of listtags-<tags> conf section.
2633
2769
  # Calculated parameters.
@@ -2799,6 +2935,7 @@ class List(AbstractBlock):
2799
2935
  BlockTitle.consume(attrs)
2800
2936
  AttributeList.consume(attrs)
2801
2937
  self.merge_attributes(attrs,['tags'])
2938
+ self.push_blockname()
2802
2939
  if self.type in ('numbered','callout'):
2803
2940
  self.number_style = self.attributes.get('style')
2804
2941
  if self.number_style not in self.NUMBER_STYLES:
@@ -2833,7 +2970,7 @@ class List(AbstractBlock):
2833
2970
  elif self.type == 'labeled':
2834
2971
  self.translate_entry()
2835
2972
  else:
2836
- raise AssertionError,'illegal [%s] list type' % self.name
2973
+ raise AssertionError,'illegal [%s] list type' % self.defname
2837
2974
  if etag:
2838
2975
  writer.write(etag,trace='list close')
2839
2976
  if self.type == 'callout':
@@ -2842,6 +2979,7 @@ class List(AbstractBlock):
2842
2979
  lists.open.pop()
2843
2980
  if len(lists.open):
2844
2981
  document.attributes['listindex'] = str(lists.open[-1].ordinal)
2982
+ self.pop_blockname()
2845
2983
 
2846
2984
  class Lists(AbstractBlocks):
2847
2985
  """List of List objects."""
@@ -2889,7 +3027,7 @@ class Lists(AbstractBlocks):
2889
3027
  for b in self.blocks:
2890
3028
  # Check list has valid type.
2891
3029
  if not b.type in Lists.TYPES:
2892
- raise EAsciiDoc,'[%s] illegal type' % b.name
3030
+ raise EAsciiDoc,'[%s] illegal type' % b.defname
2893
3031
  b.validate()
2894
3032
  def dump(self):
2895
3033
  AbstractBlocks.dump(self)
@@ -2911,20 +3049,20 @@ class DelimitedBlock(AbstractBlock):
2911
3049
  def translate(self):
2912
3050
  AbstractBlock.translate(self)
2913
3051
  reader.read() # Discard delimiter.
2914
- attrs = {}
2915
- if self.short_name() != 'comment':
2916
- BlockTitle.consume(attrs)
2917
- AttributeList.consume(attrs)
2918
- self.merge_attributes(attrs)
3052
+ self.merge_attributes(AttributeList.attrs)
3053
+ if not 'skip' in self.parameters.options:
3054
+ BlockTitle.consume(self.attributes)
3055
+ AttributeList.consume()
3056
+ self.push_blockname()
2919
3057
  options = self.parameters.options
2920
3058
  if 'skip' in options:
2921
3059
  reader.read_until(self.delimiter,same_file=True)
2922
- elif safe() and self.name == 'blockdef-backend':
3060
+ elif safe() and self.defname == 'blockdef-backend':
2923
3061
  message.unsafe('Backend Block')
2924
3062
  reader.read_until(self.delimiter,same_file=True)
2925
3063
  else:
2926
3064
  template = self.parameters.template
2927
- template = subs_attrs(template,attrs)
3065
+ template = subs_attrs(template,self.attributes)
2928
3066
  name = self.short_name()+' block'
2929
3067
  if 'sectionbody' in options:
2930
3068
  # The body is treated like a section body.
@@ -2950,6 +3088,7 @@ class DelimitedBlock(AbstractBlock):
2950
3088
  else:
2951
3089
  delimiter = reader.read() # Discard delimiter line.
2952
3090
  assert re.match(self.delimiter,delimiter)
3091
+ self.pop_blockname()
2953
3092
 
2954
3093
  class DelimitedBlocks(AbstractBlocks):
2955
3094
  """List of delimited blocks."""
@@ -2979,12 +3118,19 @@ class Cell:
2979
3118
  self.span, self.vspan = Table.parse_span_spec(span_spec)
2980
3119
  self.halign, self.valign = Table.parse_align_spec(align_spec)
2981
3120
  self.style = style
3121
+ self.reserved = False
2982
3122
  def __repr__(self):
2983
3123
  return '<Cell: %d.%d %s.%s %s "%s">' % (
2984
3124
  self.span, self.vspan,
2985
3125
  self.halign, self.valign,
2986
3126
  self.style or '',
2987
3127
  self.data)
3128
+ def clone_reserve(self):
3129
+ """Return a clone of self to reserve vertically spanned cell."""
3130
+ result = copy.copy(self)
3131
+ result.vspan = 1
3132
+ result.reserved = True
3133
+ return result
2988
3134
 
2989
3135
  class Table(AbstractBlock):
2990
3136
  ALIGN = {'<':'left', '>':'right', '^':'center'}
@@ -3055,7 +3201,7 @@ class Table(AbstractBlock):
3055
3201
  self.error('missing section: [tabletags-%s]' % t,halt=True)
3056
3202
  if self.separator:
3057
3203
  # Evaluate escape characters.
3058
- self.separator = eval('"'+self.separator+'"')
3204
+ self.separator = literal_eval('"'+self.separator+'"')
3059
3205
  #TODO: Move to class Tables
3060
3206
  # Check global table parameters.
3061
3207
  elif config.pagewidth is None:
@@ -3226,18 +3372,33 @@ class Table(AbstractBlock):
3226
3372
  Parse the table source text into self.rows (a list of rows, each row
3227
3373
  is a list of Cells.
3228
3374
  """
3229
- reserved = {} # Cols reserved by rowspans (indexed by row number).
3375
+ reserved = {} # Reserved cells generated by rowspans.
3230
3376
  if self.parameters.format in ('psv','dsv'):
3377
+ colcount = len(self.columns)
3378
+ parsed_cells = self.parse_psv_dsv(text)
3231
3379
  ri = 0 # Current row index 0..
3232
- cells = self.parse_psv_dsv(text)
3233
- row = []
3234
3380
  ci = 0 # Column counter 0..colcount
3235
- for cell in cells:
3236
- colcount = len(self.columns) - reserved.get(ri,0)
3237
- if cell.vspan > 1:
3238
- # Reserve spanned columns from ensuing rows.
3239
- for i in range(1, cell.vspan):
3240
- reserved[ri+i] = reserved.get(ri+i, 0) + cell.span
3381
+ row = []
3382
+ i = 0
3383
+ while True:
3384
+ resv = reserved.get(ri) and reserved[ri].get(ci)
3385
+ if resv:
3386
+ # We have a cell generated by a previous row span so
3387
+ # process it before continuing with the current parsed
3388
+ # cell.
3389
+ cell = resv
3390
+ else:
3391
+ if i >= len(parsed_cells):
3392
+ break # No more parsed or reserved cells.
3393
+ cell = parsed_cells[i]
3394
+ i += 1
3395
+ if cell.vspan > 1:
3396
+ # Generate ensuing reserved cells spanned vertically by
3397
+ # the current cell.
3398
+ for j in range(1, cell.vspan):
3399
+ if not ri+j in reserved:
3400
+ reserved[ri+j] = {}
3401
+ reserved[ri+j][ci] = cell.clone_reserve()
3241
3402
  ci += cell.span
3242
3403
  if ci <= colcount:
3243
3404
  row.append(cell)
@@ -3246,28 +3407,30 @@ class Table(AbstractBlock):
3246
3407
  ri += 1
3247
3408
  row = []
3248
3409
  ci = 0
3249
- if ci > colcount:
3250
- message.warning('table row %d: span exceeds number of columns'
3251
- % ri)
3252
3410
  elif self.parameters.format == 'csv':
3253
3411
  self.rows = self.parse_csv(text)
3254
3412
  else:
3255
3413
  assert True,'illegal table format'
3414
+ # Check for empty rows containing only reserved (spanned) cells.
3415
+ for ri,row in enumerate(self.rows):
3416
+ empty = True
3417
+ for cell in row:
3418
+ if not cell.reserved:
3419
+ empty = False
3420
+ break
3421
+ if empty:
3422
+ message.warning('table row %d: empty spanned row' % (ri+1))
3256
3423
  # Check that all row spans match.
3257
3424
  for ri,row in enumerate(self.rows):
3258
3425
  row_span = 0
3259
3426
  for cell in row:
3260
3427
  row_span += cell.span
3261
- row_span += reserved.get(ri,0)
3262
3428
  if ri == 0:
3263
3429
  header_span = row_span
3264
3430
  if row_span < header_span:
3265
3431
  message.warning('table row %d: does not span all columns' % (ri+1))
3266
3432
  if row_span > header_span:
3267
3433
  message.warning('table row %d: exceeds columns span' % (ri+1))
3268
- # Check that now row spans exceed the number of rows.
3269
- if len([x for x in reserved.keys() if x >= len(self.rows)]) > 0:
3270
- message.warning('one or more cell spans exceed the available rows')
3271
3434
  def subs_rows(self, rows, rowtype='body'):
3272
3435
  """
3273
3436
  Return a string of output markup from a list of rows, each row
@@ -3295,6 +3458,10 @@ class Table(AbstractBlock):
3295
3458
  result = []
3296
3459
  i = 0
3297
3460
  for cell in row:
3461
+ if cell.reserved:
3462
+ # Skip vertically spanned placeholders.
3463
+ i += cell.span
3464
+ continue
3298
3465
  if i >= len(self.columns):
3299
3466
  break # Skip cells outside the header width.
3300
3467
  col = self.columns[i]
@@ -3435,8 +3602,9 @@ class Table(AbstractBlock):
3435
3602
  delimiter = reader.read() # Discard closing delimiter.
3436
3603
  assert re.match(self.delimiter,delimiter)
3437
3604
  if len(text) == 0:
3438
- message.warning('[%s] table is empty' % self.name)
3605
+ message.warning('[%s] table is empty' % self.defname)
3439
3606
  return
3607
+ self.push_blockname('table')
3440
3608
  cols = attrs.get('cols')
3441
3609
  if not cols:
3442
3610
  # Calculate column count from number of items in first line.
@@ -3484,6 +3652,7 @@ class Table(AbstractBlock):
3484
3652
  if bodyrows:
3485
3653
  table = table.replace('\x07bodyrows\x07', bodyrows, 1)
3486
3654
  writer.write(table,trace='table')
3655
+ self.pop_blockname()
3487
3656
 
3488
3657
  class Tables(AbstractBlocks):
3489
3658
  """List of tables."""
@@ -3520,7 +3689,7 @@ class Tables(AbstractBlocks):
3520
3689
  AbstractBlocks.validate(self)
3521
3690
  # Check we have a default table definition,
3522
3691
  for i in range(len(self.blocks)):
3523
- if self.blocks[i].name == 'tabledef-default':
3692
+ if self.blocks[i].defname == 'tabledef-default':
3524
3693
  default = self.blocks[i]
3525
3694
  break
3526
3695
  else:
@@ -3641,7 +3810,7 @@ class Macros:
3641
3810
  if mo:
3642
3811
  if m.name == name:
3643
3812
  return mo
3644
- if re.match(name,mo.group('name')):
3813
+ if re.match(name, mo.group('name')):
3645
3814
  return mo
3646
3815
  return None
3647
3816
  def extract_passthroughs(self,text,prefix=''):
@@ -3977,8 +4146,12 @@ class Reader1:
3977
4146
  del self.next[0]
3978
4147
  result = self.cursor[2]
3979
4148
  # Check for include macro.
3980
- mo = macros.match('+',r'include[1]?',result)
4149
+ mo = macros.match('+',r'^include[1]?$',result)
3981
4150
  if mo and not skip:
4151
+ # Parse include macro attributes.
4152
+ attrs = {}
4153
+ parse_attributes(mo.group('attrlist'),attrs)
4154
+ warnings = attrs.get('warnings', True)
3982
4155
  # Don't process include macro once the maximum depth is reached.
3983
4156
  if self.current_depth >= self.max_depth:
3984
4157
  return result
@@ -3991,37 +4164,53 @@ class Reader1:
3991
4164
  fname = safe_filename(fname, os.path.dirname(self.fname))
3992
4165
  if not fname:
3993
4166
  return Reader1.read(self) # Return next input line.
4167
+ if not os.path.isfile(fname):
4168
+ if warnings:
4169
+ message.warning('include file not found: %s' % fname)
4170
+ return Reader1.read(self) # Return next input line.
3994
4171
  if mo.group('name') == 'include1':
3995
4172
  if not config.dumping:
3996
- # Store the include file in memory for later
3997
- # retrieval by the {include1:} system attribute.
3998
- config.include1[fname] = [
3999
- s.rstrip() for s in open(fname)]
4173
+ if fname not in config.include1:
4174
+ message.verbose('include1: ' + fname, linenos=False)
4175
+ # Store the include file in memory for later
4176
+ # retrieval by the {include1:} system attribute.
4177
+ f = open(fname)
4178
+ try:
4179
+ config.include1[fname] = [
4180
+ s.rstrip() for s in f]
4181
+ finally:
4182
+ f.close()
4000
4183
  return '{include1:%s}' % fname
4001
4184
  else:
4002
4185
  # This is a configuration dump, just pass the macro
4003
4186
  # call through.
4004
4187
  return result
4005
- # Parse include macro attributes.
4006
- attrs = {}
4007
- parse_attributes(mo.group('attrlist'),attrs)
4008
4188
  # Clone self and set as parent (self assumes the role of child).
4009
4189
  parent = Reader1()
4010
4190
  assign(parent,self)
4011
4191
  self.parent = parent
4012
4192
  # Set attributes in child.
4013
4193
  if 'tabsize' in attrs:
4014
- self.tabsize = int(validate(attrs['tabsize'],
4015
- 'int($)>=0',
4016
- 'illegal include macro tabsize argument'))
4194
+ try:
4195
+ val = int(attrs['tabsize'])
4196
+ if not val >= 0:
4197
+ raise ValueError, "not >= 0"
4198
+ self.tabsize = val
4199
+ except ValueError:
4200
+ raise EAsciiDoc, 'illegal include macro tabsize argument'
4017
4201
  else:
4018
4202
  self.tabsize = config.tabsize
4019
4203
  if 'depth' in attrs:
4020
- attrs['depth'] = int(validate(attrs['depth'],
4021
- 'int($)>=1',
4022
- 'illegal include macro depth argument'))
4023
- self.max_depth = self.current_depth + attrs['depth']
4204
+ try:
4205
+ val = int(attrs['depth'])
4206
+ if not val >= 1:
4207
+ raise ValueError, "not >= 1"
4208
+ self.max_depth = self.current_depth + val
4209
+ except ValueError:
4210
+ raise EAsciiDoc, 'illegal include macro depth argument'
4211
+
4024
4212
  # Process included file.
4213
+ message.verbose('include: ' + fname, linenos=False)
4025
4214
  self.open(fname)
4026
4215
  self.current_depth = self.current_depth + 1
4027
4216
  result = Reader1.read(self)
@@ -4125,6 +4314,9 @@ class Reader(Reader1):
4125
4314
  else:
4126
4315
  self.skip = defined
4127
4316
  elif name == 'ifeval':
4317
+ if safe():
4318
+ message.unsafe('ifeval invalid')
4319
+ raise EAsciiDoc,'ifeval invalid safe document'
4128
4320
  if not attrlist:
4129
4321
  raise EAsciiDoc,'missing ifeval condition: %s' % result
4130
4322
  cond = False
@@ -4134,6 +4326,7 @@ class Reader(Reader1):
4134
4326
  cond = eval(attrlist)
4135
4327
  except Exception,e:
4136
4328
  raise EAsciiDoc,'error evaluating ifeval condition: %s: %s' % (result, str(e))
4329
+ message.verbose('ifeval: %s: %r' % (attrlist, cond))
4137
4330
  self.skip = not cond
4138
4331
  if not attrlist or name == 'ifeval':
4139
4332
  if self.skip:
@@ -4147,10 +4340,8 @@ class Reader(Reader1):
4147
4340
  if mo:
4148
4341
  action = mo.group('name')
4149
4342
  cmd = mo.group('attrlist')
4150
- s = system(action, cmd, is_macro=True)
4151
- if s is not None:
4152
- self.cursor[2] = s # So we don't re-evaluate.
4153
- result = s
4343
+ result = system(action, cmd, is_macro=True)
4344
+ self.cursor[2] = result # So we don't re-evaluate.
4154
4345
  if result:
4155
4346
  # Unescape escaped system macros.
4156
4347
  if macros.match('+',r'\\eval|\\sys|\\sys2|\\ifdef|\\ifndef|\\endif|\\include|\\include1',result):
@@ -4312,7 +4503,7 @@ class Config:
4312
4503
  ENTRIES_SECTIONS= ('tags','miscellaneous','attributes','specialcharacters',
4313
4504
  'specialwords','macros','replacements','quotes','titles',
4314
4505
  r'paradef-.+',r'listdef-.+',r'blockdef-.+',r'tabledef-.+',
4315
- r'tabletags-.+',r'listtags-.+','replacements2',
4506
+ r'tabletags-.+',r'listtags-.+','replacements[23]',
4316
4507
  r'old_tabledef-.+')
4317
4508
  def __init__(self):
4318
4509
  self.sections = OrderedDict() # Keyed by section name containing
@@ -4336,6 +4527,7 @@ class Config:
4336
4527
  self.replacements = OrderedDict() # Key is find pattern, value is
4337
4528
  #replace pattern.
4338
4529
  self.replacements2 = OrderedDict()
4530
+ self.replacements3 = OrderedDict()
4339
4531
  self.specialsections = {} # Name is special section name pattern, value
4340
4532
  # is corresponding section name.
4341
4533
  self.quotes = OrderedDict() # Values contain corresponding tag name.
@@ -4345,6 +4537,7 @@ class Config:
4345
4537
  self.loaded = [] # Loaded conf files.
4346
4538
  self.include1 = {} # Holds include1::[] files for {include1:}.
4347
4539
  self.dumping = False # True if asciidoc -c option specified.
4540
+ self.filters = [] # Filter names specified by --filter option.
4348
4541
 
4349
4542
  def init(self, cmd):
4350
4543
  """
@@ -4352,8 +4545,9 @@ class Config:
4352
4545
  directory.
4353
4546
  cmd is the asciidoc command or asciidoc.py path.
4354
4547
  """
4355
- if float(sys.version[:3]) < MIN_PYTHON_VERSION:
4356
- message.stderr('FAILED: Python 2.3 or better required')
4548
+ if float(sys.version[:3]) < float(MIN_PYTHON_VERSION):
4549
+ message.stderr('FAILED: Python %s or better required' %
4550
+ MIN_PYTHON_VERSION)
4357
4551
  sys.exit(1)
4358
4552
  if not os.path.exists(cmd):
4359
4553
  message.stderr('FAILED: Missing asciidoc command: %s' % cmd)
@@ -4377,6 +4571,25 @@ class Config:
4377
4571
  The 'exclude' list contains section names not to be loaded.
4378
4572
  Return False if no file was found in any of the locations.
4379
4573
  """
4574
+ def update_section(section):
4575
+ """ Update section in sections with contents. """
4576
+ if section and contents:
4577
+ if section in sections and self.entries_section(section):
4578
+ if ''.join(contents):
4579
+ # Merge entries.
4580
+ sections[section] += contents
4581
+ else:
4582
+ del sections[section]
4583
+ else:
4584
+ if section.startswith('+'):
4585
+ # Append section.
4586
+ if section in sections:
4587
+ sections[section] += contents
4588
+ else:
4589
+ sections[section] = contents
4590
+ else:
4591
+ # Replace section.
4592
+ sections[section] = contents
4380
4593
  if dir:
4381
4594
  fname = os.path.join(dir, fname)
4382
4595
  # Sliently skip missing configuration file.
@@ -4391,7 +4604,7 @@ class Config:
4391
4604
  rdr.open(fname)
4392
4605
  message.linenos = None
4393
4606
  self.fname = fname
4394
- reo = re.compile(r'(?u)^\[(?P<section>[^\W\d][\w-]*)\]\s*$')
4607
+ reo = re.compile(r'(?u)^\[(?P<section>\+?[^\W\d][\w-]*)\]\s*$')
4395
4608
  sections = OrderedDict()
4396
4609
  section,contents = '',[]
4397
4610
  while not rdr.eof():
@@ -4403,30 +4616,12 @@ class Config:
4403
4616
  s = s.rstrip()
4404
4617
  found = reo.findall(s)
4405
4618
  if found:
4406
- if section: # Store previous section.
4407
- if section in sections \
4408
- and self.entries_section(section):
4409
- if ''.join(contents):
4410
- # Merge entries.
4411
- sections[section] = sections[section] + contents
4412
- else:
4413
- del sections[section]
4414
- else:
4415
- sections[section] = contents
4619
+ update_section(section) # Store previous section.
4416
4620
  section = found[0].lower()
4417
4621
  contents = []
4418
4622
  else:
4419
4623
  contents.append(s)
4420
- if section and contents: # Store last section.
4421
- if section in sections \
4422
- and self.entries_section(section):
4423
- if ''.join(contents):
4424
- # Merge entries.
4425
- sections[section] = sections[section] + contents
4426
- else:
4427
- del sections[section]
4428
- else:
4429
- sections[section] = contents
4624
+ update_section(section) # Store last section.
4430
4625
  rdr.close()
4431
4626
  if include:
4432
4627
  for s in set(sections) - set(include):
@@ -4455,8 +4650,18 @@ class Config:
4455
4650
  del sections[k][i]
4456
4651
  elif not self.entries_section(k):
4457
4652
  break
4458
- # Add/overwrite new sections.
4459
- self.sections.update(sections)
4653
+ # Update new sections.
4654
+ for k,v in sections.items():
4655
+ if k.startswith('+'):
4656
+ # Append section.
4657
+ k = k[1:]
4658
+ if k in self.sections:
4659
+ self.sections[k] += v
4660
+ else:
4661
+ self.sections[k] = v
4662
+ else:
4663
+ # Replace section.
4664
+ self.sections[k] = v
4460
4665
  self.parse_tags()
4461
4666
  # Internally [miscellaneous] section entries are just attributes.
4462
4667
  d = {}
@@ -4475,6 +4680,7 @@ class Config:
4475
4680
  self.parse_specialwords()
4476
4681
  self.parse_replacements()
4477
4682
  self.parse_replacements('replacements2')
4683
+ self.parse_replacements('replacements3')
4478
4684
  self.parse_specialsections()
4479
4685
  paragraphs.load(sections)
4480
4686
  lists.load(sections)
@@ -4530,19 +4736,34 @@ class Config:
4530
4736
  """
4531
4737
  Load the backend configuration files from dirs list.
4532
4738
  If dirs not specified try all the well known locations.
4739
+ If a <backend>.conf file was found return it's full path name,
4740
+ if not found return None.
4533
4741
  """
4742
+ result = None
4534
4743
  if dirs is None:
4535
4744
  dirs = self.get_load_dirs()
4536
- for d in dirs:
4537
- conf = document.backend + '.conf'
4538
- self.load_file(conf,d)
4539
- conf = document.backend + '-' + document.doctype + '.conf'
4540
- self.load_file(conf,d)
4745
+ conf = document.backend + '.conf'
4746
+ conf2 = document.backend + '-' + document.doctype + '.conf'
4747
+ # First search for filter backends.
4748
+ for d in [os.path.join(d, 'backends', document.backend) for d in dirs]:
4749
+ if self.load_file(conf,d):
4750
+ result = os.path.join(d, conf)
4751
+ self.load_file(conf2,d)
4752
+ if not result:
4753
+ # Search in the normal locations.
4754
+ for d in dirs:
4755
+ if self.load_file(conf,d):
4756
+ result = os.path.join(d, conf)
4757
+ self.load_file(conf2,d)
4758
+ return result
4541
4759
 
4542
4760
  def load_filters(self, dirs=None):
4543
4761
  """
4544
4762
  Load filter configuration files from 'filters' directory in dirs list.
4545
- If dirs not specified try all the well known locations.
4763
+ If dirs not specified try all the well known locations. Suppress
4764
+ loading if a file named __noautoload__ is in same directory as the conf
4765
+ file unless the filter has been specified with the --filter
4766
+ command-line option (in which case it is loaded unconditionally).
4546
4767
  """
4547
4768
  if dirs is None:
4548
4769
  dirs = self.get_load_dirs()
@@ -4550,29 +4771,66 @@ class Config:
4550
4771
  # Load filter .conf files.
4551
4772
  filtersdir = os.path.join(d,'filters')
4552
4773
  for dirpath,dirnames,filenames in os.walk(filtersdir):
4553
- for f in filenames:
4554
- if re.match(r'^.+\.conf$',f):
4555
- self.load_file(f,dirpath)
4774
+ subdirs = dirpath[len(filtersdir):].split(os.path.sep)
4775
+ # True if processing a filter specified by a --filter option.
4776
+ filter_opt = len(subdirs) > 1 and subdirs[1] in self.filters
4777
+ if '__noautoload__' not in filenames or filter_opt:
4778
+ for f in filenames:
4779
+ if re.match(r'^.+\.conf$',f):
4780
+ self.load_file(f,dirpath)
4781
+
4782
+ def find_config_dir(self, *dirnames):
4783
+ """
4784
+ Return path of configuration directory.
4785
+ Try all the well known locations.
4786
+ Return None if directory not found.
4787
+ """
4788
+ for d in [os.path.join(d, *dirnames) for d in self.get_load_dirs()]:
4789
+ if os.path.isdir(d):
4790
+ return d
4791
+ return None
4792
+
4793
+ def set_theme_attributes(self):
4794
+ theme = document.attributes.get('theme')
4795
+ if theme and 'themedir' not in document.attributes:
4796
+ themedir = self.find_config_dir('themes', theme)
4797
+ if themedir:
4798
+ document.attributes['themedir'] = themedir
4799
+ iconsdir = os.path.join(themedir, 'icons')
4800
+ if 'data-uri' in document.attributes and os.path.isdir(iconsdir):
4801
+ document.attributes['iconsdir'] = iconsdir
4802
+ else:
4803
+ message.warning('missing theme: %s' % theme, linenos=False)
4556
4804
 
4557
4805
  def load_miscellaneous(self,d):
4558
4806
  """Set miscellaneous configuration entries from dictionary 'd'."""
4559
- def set_misc(name,rule='True',intval=False):
4807
+ def set_if_int_gt_zero(name, d):
4560
4808
  if name in d:
4561
- errmsg = 'illegal [miscellaneous] %s entry' % name
4562
- if intval:
4563
- setattr(self, name, int(validate(d[name],rule,errmsg)))
4564
- else:
4565
- setattr(self, name, validate(d[name],rule,errmsg))
4566
- set_misc('tabsize','int($)>0',intval=True)
4567
- set_misc('textwidth','int($)>0',intval=True) # DEPRECATED: Old tables only.
4568
- set_misc('pagewidth','"%f" % $')
4809
+ try:
4810
+ val = int(d[name])
4811
+ if not val > 0:
4812
+ raise ValueError, "not > 0"
4813
+ if val > 0:
4814
+ setattr(self, name, val)
4815
+ except ValueError:
4816
+ raise EAsciiDoc, 'illegal [miscellaneous] %s entry' % name
4817
+ set_if_int_gt_zero('tabsize', d)
4818
+ set_if_int_gt_zero('textwidth', d) # DEPRECATED: Old tables only.
4819
+
4569
4820
  if 'pagewidth' in d:
4570
- self.pagewidth = float(self.pagewidth)
4571
- set_misc('pageunits')
4572
- set_misc('outfilesuffix')
4821
+ try:
4822
+ val = float(d['pagewidth'])
4823
+ self.pagewidth = val
4824
+ except ValueError:
4825
+ raise EAsciiDoc, 'illegal [miscellaneous] pagewidth entry'
4826
+
4827
+ if 'pageunits' in d:
4828
+ self.pageunits = d['pageunits']
4829
+ if 'outfilesuffix' in d:
4830
+ self.outfilesuffix = d['outfilesuffix']
4573
4831
  if 'newline' in d:
4574
4832
  # Convert escape sequences to their character values.
4575
- self.newline = eval('"'+d['newline']+'"')
4833
+ self.newline = literal_eval('"'+d['newline']+'"')
4576
4834
  if 'subsnormal' in d:
4577
4835
  self.subsnormal = parse_options(d['subsnormal'],SUBS_OPTIONS,
4578
4836
  'illegal [%s] %s: %s' %
@@ -4664,6 +4922,7 @@ class Config:
4664
4922
  dump_section('specialwords',d)
4665
4923
  dump_section('replacements',self.replacements)
4666
4924
  dump_section('replacements2',self.replacements2)
4925
+ dump_section('replacements3',self.replacements3)
4667
4926
  dump_section('specialsections',self.specialsections)
4668
4927
  d = {}
4669
4928
  for k,v in self.tags.items():
@@ -5060,8 +5319,13 @@ class Table_OLD(AbstractBlock):
5060
5319
  if s == '':
5061
5320
  c.rulerwidth = None
5062
5321
  else:
5063
- c.rulerwidth = int(validate(s,'int($)>0',
5064
- 'malformed ruler: bad width'))
5322
+ try:
5323
+ val = int(s)
5324
+ if not val > 0:
5325
+ raise ValueError, 'not > 0'
5326
+ c.rulerwidth = val
5327
+ except ValueError:
5328
+ raise EAsciiDoc, 'malformed ruler: bad width'
5065
5329
  else: # Calculate column width from inter-fillchar intervals.
5066
5330
  if not re.match(r'^'+fc+r'+$',s):
5067
5331
  raise EAsciiDoc,'malformed ruler: illegal fillchars'
@@ -5130,7 +5394,7 @@ class Table_OLD(AbstractBlock):
5130
5394
  if i == 0:
5131
5395
  raise EAsciiDoc,'missing table rows'
5132
5396
  if i >= len(rows):
5133
- raise EAsciiDoc,'closing [%s] underline expected' % self.name
5397
+ raise EAsciiDoc,'closing [%s] underline expected' % self.defname
5134
5398
  return (join_lines_OLD(rows[:i]), rows[i+1:])
5135
5399
  def parse_rows(self, rows, rtag, dtag):
5136
5400
  """Parse rows list using the row and data tags. Returns a substituted
@@ -5177,7 +5441,7 @@ class Table_OLD(AbstractBlock):
5177
5441
  # and the output markup is HTML. It's also a bit dubious in that it
5178
5442
  # assumes the user has not modified the shipped line break pattern.
5179
5443
  subs = self.get_subs()[0]
5180
- if 'replacements' in subs:
5444
+ if 'replacements2' in subs:
5181
5445
  # Insert line breaks in cell data.
5182
5446
  d = re.sub(r'(?m)\n',r' +\n',d)
5183
5447
  d = d.split('\n') # So writer.newline is written.
@@ -5224,7 +5488,7 @@ class Table_OLD(AbstractBlock):
5224
5488
  """Parse the list of source table rows. Each row item in the returned
5225
5489
  list contains a list of cell data elements."""
5226
5490
  separator = self.attributes.get('separator',':')
5227
- separator = eval('"'+separator+'"')
5491
+ separator = literal_eval('"'+separator+'"')
5228
5492
  if len(separator) != 1:
5229
5493
  raise EAsciiDoc,'malformed dsv separator: %s' % separator
5230
5494
  # TODO If separator is preceeded by an odd number of backslashes then
@@ -5234,7 +5498,7 @@ class Table_OLD(AbstractBlock):
5234
5498
  # Skip blank lines
5235
5499
  if row == '': continue
5236
5500
  # Unescape escaped characters.
5237
- row = eval('"'+row.replace('"','\\"')+'"')
5501
+ row = literal_eval('"'+row.replace('"','\\"')+'"')
5238
5502
  data = row.split(separator)
5239
5503
  data = [s.strip() for s in data]
5240
5504
  result.append(data)
@@ -5256,13 +5520,13 @@ class Table_OLD(AbstractBlock):
5256
5520
  for k,v in attrs.items():
5257
5521
  if k == 'format':
5258
5522
  if v not in self.FORMATS:
5259
- raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.name,k,v)
5523
+ raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.defname,k,v)
5260
5524
  self.format = v
5261
5525
  elif k == 'tablewidth':
5262
5526
  try:
5263
5527
  self.tablewidth = float(attrs['tablewidth'])
5264
5528
  except Exception:
5265
- raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.name,k,v)
5529
+ raise EAsciiDoc, 'illegal [%s] %s: %s' % (self.defname,k,v)
5266
5530
  self.merge_attributes(attrs)
5267
5531
  # Parse table ruler.
5268
5532
  ruler = reader.read()
@@ -5277,13 +5541,14 @@ class Table_OLD(AbstractBlock):
5277
5541
  if line in ('',None):
5278
5542
  break;
5279
5543
  if line is None:
5280
- raise EAsciiDoc,'closing [%s] underline expected' % self.name
5544
+ raise EAsciiDoc,'closing [%s] underline expected' % self.defname
5281
5545
  table.append(reader.read())
5282
5546
  # EXPERIMENTAL: The number of lines in the table, requested by Benjamin Klum.
5283
5547
  self.attributes['rows'] = str(len(table))
5284
5548
  if self.check_msg: # Skip if table definition was marked invalid.
5285
- message.warning('skipping %s table: %s' % (self.name,self.check_msg))
5549
+ message.warning('skipping [%s] table: %s' % (self.defname,self.check_msg))
5286
5550
  return
5551
+ self.push_blockname('table')
5287
5552
  # Generate colwidths and colspecs.
5288
5553
  self.build_colspecs()
5289
5554
  # Generate headrows, footrows, bodyrows.
@@ -5321,6 +5586,7 @@ class Table_OLD(AbstractBlock):
5321
5586
  table = table.replace('\x07footrows\x07', footrows, 1)
5322
5587
  table = table.replace('\x07bodyrows\x07', bodyrows, 1)
5323
5588
  writer.write(table,trace='table')
5589
+ self.pop_blockname()
5324
5590
 
5325
5591
  class Tables_OLD(AbstractBlocks):
5326
5592
  """List of tables."""
@@ -5334,7 +5600,7 @@ class Tables_OLD(AbstractBlocks):
5334
5600
  # Does not call AbstractBlocks.validate().
5335
5601
  # Check we have a default table definition,
5336
5602
  for i in range(len(self.blocks)):
5337
- if self.blocks[i].name == 'old_tabledef-default':
5603
+ if self.blocks[i].defname == 'old_tabledef-default':
5338
5604
  default = self.blocks[i]
5339
5605
  break
5340
5606
  else:
@@ -5357,7 +5623,7 @@ class Tables_OLD(AbstractBlocks):
5357
5623
  # Check all tables have valid fill character.
5358
5624
  for b in self.blocks:
5359
5625
  if not b.fillchar or len(b.fillchar) != 1:
5360
- raise EAsciiDoc,'[%s] missing or illegal fillchar' % b.name
5626
+ raise EAsciiDoc,'[%s] missing or illegal fillchar' % b.defname
5361
5627
  # Build combined tables delimiter patterns and assign defaults.
5362
5628
  delimiters = []
5363
5629
  for b in self.blocks:
@@ -5383,13 +5649,13 @@ class Tables_OLD(AbstractBlocks):
5383
5649
  b.validate()
5384
5650
  if config.verbose:
5385
5651
  if b.check_msg:
5386
- message.warning('[%s] table definition: %s' % (b.name,b.check_msg))
5652
+ message.warning('[%s] table definition: %s' % (b.defname,b.check_msg))
5387
5653
 
5388
5654
  # End of deprecated old table classes.
5389
5655
  #---------------------------------------------------------------------------
5390
5656
 
5391
5657
  #---------------------------------------------------------------------------
5392
- # Filter commands.
5658
+ # filter and theme plugin commands.
5393
5659
  #---------------------------------------------------------------------------
5394
5660
  import shutil, zipfile
5395
5661
 
@@ -5397,7 +5663,7 @@ def die(msg):
5397
5663
  message.stderr(msg)
5398
5664
  sys.exit(1)
5399
5665
 
5400
- def unzip(zip_file, destdir):
5666
+ def extract_zip(zip_file, destdir):
5401
5667
  """
5402
5668
  Unzip Zip file to destination directory.
5403
5669
  Throws exception if error occurs.
@@ -5414,7 +5680,14 @@ def unzip(zip_file, destdir):
5414
5680
  outfile = os.path.join(directory, outfile)
5415
5681
  perms = (zi.external_attr >> 16) & 0777
5416
5682
  message.verbose('extracting: %s' % outfile)
5417
- fh = os.open(outfile, os.O_CREAT | os.O_WRONLY, perms)
5683
+ flags = os.O_CREAT | os.O_WRONLY
5684
+ if sys.platform == 'win32':
5685
+ flags |= os.O_BINARY
5686
+ if perms == 0:
5687
+ # Zip files created under Windows do not include permissions.
5688
+ fh = os.open(outfile, flags)
5689
+ else:
5690
+ fh = os.open(outfile, flags, perms)
5418
5691
  try:
5419
5692
  os.write(fh, zipo.read(zi.filename))
5420
5693
  finally:
@@ -5422,100 +5695,162 @@ def unzip(zip_file, destdir):
5422
5695
  finally:
5423
5696
  zipo.close()
5424
5697
 
5425
- class Filter:
5698
+ def create_zip(zip_file, src, skip_hidden=False):
5426
5699
  """
5427
- --filter option commands.
5700
+ Create Zip file. If src is a directory archive all contained files and
5701
+ subdirectories, if src is a file archive the src file.
5702
+ Files and directories names starting with . are skipped
5703
+ if skip_hidden is True.
5704
+ Throws exception if error occurs.
5428
5705
  """
5706
+ zipo = zipfile.ZipFile(zip_file, 'w')
5707
+ try:
5708
+ if os.path.isfile(src):
5709
+ arcname = os.path.basename(src)
5710
+ message.verbose('archiving: %s' % arcname)
5711
+ zipo.write(src, arcname, zipfile.ZIP_DEFLATED)
5712
+ elif os.path.isdir(src):
5713
+ srcdir = os.path.abspath(src)
5714
+ if srcdir[-1] != os.path.sep:
5715
+ srcdir += os.path.sep
5716
+ for root, dirs, files in os.walk(srcdir):
5717
+ arcroot = os.path.abspath(root)[len(srcdir):]
5718
+ if skip_hidden:
5719
+ for d in dirs[:]:
5720
+ if d.startswith('.'):
5721
+ message.verbose('skipping: %s' % os.path.join(arcroot, d))
5722
+ del dirs[dirs.index(d)]
5723
+ for f in files:
5724
+ filename = os.path.join(root,f)
5725
+ arcname = os.path.join(arcroot, f)
5726
+ if skip_hidden and f.startswith('.'):
5727
+ message.verbose('skipping: %s' % arcname)
5728
+ continue
5729
+ message.verbose('archiving: %s' % arcname)
5730
+ zipo.write(filename, arcname, zipfile.ZIP_DEFLATED)
5731
+ else:
5732
+ raise ValueError,'src must specify directory or file: %s' % src
5733
+ finally:
5734
+ zipo.close()
5735
+
5736
+ class Plugin:
5737
+ """
5738
+ --filter and --theme option commands.
5739
+ """
5740
+ CMDS = ('install','remove','list','build')
5741
+
5742
+ type = None # 'backend', 'filter' or 'theme'.
5429
5743
 
5430
5744
  @staticmethod
5431
- def get_filters_dir():
5745
+ def get_dir():
5432
5746
  """
5433
- Return path of .asciidoc/filters in user's home direcory or None if
5434
- user home not defined.
5747
+ Return plugins path (.asciidoc/filters or .asciidoc/themes) in user's
5748
+ home direcory or None if user home not defined.
5435
5749
  """
5436
5750
  result = userdir()
5437
5751
  if result:
5438
- result = os.path.join(result,'.asciidoc','filters')
5752
+ result = os.path.join(result, '.asciidoc', Plugin.type+'s')
5439
5753
  return result
5440
5754
 
5441
5755
  @staticmethod
5442
5756
  def install(args):
5443
5757
  """
5444
- Install filter Zip file.
5445
- args[0] is filter zip file path.
5446
- args[1] is optional destination filters directory.
5758
+ Install plugin Zip file.
5759
+ args[0] is plugin zip file path.
5760
+ args[1] is optional destination plugins directory.
5447
5761
  """
5448
5762
  if len(args) not in (1,2):
5449
- die('invalid number of arguments: --filter install %s'
5450
- % ' '.join(args))
5763
+ die('invalid number of arguments: --%s install %s'
5764
+ % (Plugin.type, ' '.join(args)))
5451
5765
  zip_file = args[0]
5452
5766
  if not os.path.isfile(zip_file):
5453
5767
  die('file not found: %s' % zip_file)
5454
5768
  reo = re.match(r'^\w+',os.path.split(zip_file)[1])
5455
5769
  if not reo:
5456
- die('filter file name does not start with legal filter name: %s'
5457
- % zip_file)
5458
- filter_name = reo.group()
5770
+ die('file name does not start with legal %s name: %s'
5771
+ % (Plugin.type, zip_file))
5772
+ plugin_name = reo.group()
5459
5773
  if len(args) == 2:
5460
- filters_dir = args[1]
5461
- if not os.path.isdir(filters_dir):
5462
- die('directory not found: %s' % filters_dir)
5774
+ plugins_dir = args[1]
5775
+ if not os.path.isdir(plugins_dir):
5776
+ die('directory not found: %s' % plugins_dir)
5463
5777
  else:
5464
- filters_dir = Filter.get_filters_dir()
5465
- if not filters_dir:
5778
+ plugins_dir = Plugin.get_dir()
5779
+ if not plugins_dir:
5466
5780
  die('user home directory is not defined')
5467
- filter_dir = os.path.join(filters_dir, filter_name)
5468
- if os.path.exists(filter_dir):
5469
- die('filter is already installed: %s' % filter_dir)
5781
+ plugin_dir = os.path.join(plugins_dir, plugin_name)
5782
+ if os.path.exists(plugin_dir):
5783
+ die('%s is already installed: %s' % (Plugin.type, plugin_dir))
5470
5784
  try:
5471
- os.makedirs(filter_dir)
5785
+ os.makedirs(plugin_dir)
5472
5786
  except Exception,e:
5473
- die('failed to create filter directory: %s' % str(e))
5787
+ die('failed to create %s directory: %s' % (Plugin.type, str(e)))
5474
5788
  try:
5475
- unzip(zip_file, filter_dir)
5789
+ extract_zip(zip_file, plugin_dir)
5476
5790
  except Exception,e:
5477
- die('failed to extract filter: %s' % str(e))
5791
+ if os.path.isdir(plugin_dir):
5792
+ shutil.rmtree(plugin_dir)
5793
+ die('failed to extract %s: %s' % (Plugin.type, str(e)))
5478
5794
 
5479
5795
  @staticmethod
5480
5796
  def remove(args):
5481
5797
  """
5482
- Delete filter from .asciidoc/filters/ in user's home directory.
5483
- args[0] is filter name.
5484
- args[1] is optional filters directory.
5798
+ Delete plugin directory.
5799
+ args[0] is plugin name.
5800
+ args[1] is optional plugin directory (defaults to ~/.asciidoc/<plugin_name>).
5485
5801
  """
5486
5802
  if len(args) not in (1,2):
5487
- die('invalid number of arguments: --filter remove %s'
5488
- % ' '.join(args))
5489
- filter_name = args[0]
5490
- if not re.match(r'^\w+$',filter_name):
5491
- die('illegal filter name: %s' % filter_name)
5803
+ die('invalid number of arguments: --%s remove %s'
5804
+ % (Plugin.type, ' '.join(args)))
5805
+ plugin_name = args[0]
5806
+ if not re.match(r'^\w+$',plugin_name):
5807
+ die('illegal %s name: %s' % (Plugin.type, plugin_name))
5492
5808
  if len(args) == 2:
5493
5809
  d = args[1]
5494
5810
  if not os.path.isdir(d):
5495
5811
  die('directory not found: %s' % d)
5496
5812
  else:
5497
- d = Filter.get_filters_dir()
5813
+ d = Plugin.get_dir()
5498
5814
  if not d:
5499
5815
  die('user directory is not defined')
5500
- filter_dir = os.path.join(d, filter_name)
5501
- if not os.path.isdir(filter_dir):
5502
- die('cannot find filter: %s' % filter_dir)
5816
+ plugin_dir = os.path.join(d, plugin_name)
5817
+ if not os.path.isdir(plugin_dir):
5818
+ die('cannot find %s: %s' % (Plugin.type, plugin_dir))
5503
5819
  try:
5504
- message.verbose('removing: %s' % filter_dir)
5505
- shutil.rmtree(filter_dir)
5820
+ message.verbose('removing: %s' % plugin_dir)
5821
+ shutil.rmtree(plugin_dir)
5506
5822
  except Exception,e:
5507
- die('failed to delete filter: %s' % str(e))
5823
+ die('failed to delete %s: %s' % (Plugin.type, str(e)))
5508
5824
 
5509
5825
  @staticmethod
5510
- def list():
5826
+ def list(args):
5511
5827
  """
5512
- List all filter directories (global and local).
5828
+ List all plugin directories (global and local).
5513
5829
  """
5514
- for d in [os.path.join(d,'filters') for d in config.get_load_dirs()]:
5830
+ for d in [os.path.join(d, Plugin.type+'s') for d in config.get_load_dirs()]:
5515
5831
  if os.path.isdir(d):
5516
5832
  for f in os.walk(d).next()[1]:
5517
5833
  message.stdout(os.path.join(d,f))
5518
5834
 
5835
+ @staticmethod
5836
+ def build(args):
5837
+ """
5838
+ Create plugin Zip file.
5839
+ args[0] is Zip file name.
5840
+ args[1] is plugin directory.
5841
+ """
5842
+ if len(args) != 2:
5843
+ die('invalid number of arguments: --%s build %s'
5844
+ % (Plugin.type, ' '.join(args)))
5845
+ zip_file = args[0]
5846
+ plugin_source = args[1]
5847
+ if not (os.path.isdir(plugin_source) or os.path.isfile(plugin_source)):
5848
+ die('plugin source not found: %s' % plugin_source)
5849
+ try:
5850
+ create_zip(zip_file, plugin_source, skip_hidden=True)
5851
+ except Exception,e:
5852
+ die('failed to create %s: %s' % (zip_file, str(e)))
5853
+
5519
5854
 
5520
5855
  #---------------------------------------------------------------------------
5521
5856
  # Application code.
@@ -5564,9 +5899,12 @@ def asciidoc(backend, doctype, confiles, infile, outfile, options):
5564
5899
  if os.path.isfile(f):
5565
5900
  config.load_file(f, include=include, exclude=exclude)
5566
5901
  else:
5567
- raise EAsciiDoc,'configuration file %s missing' % f
5568
-
5902
+ raise EAsciiDoc,'missing configuration file: %s' % f
5569
5903
  try:
5904
+ document.attributes['python'] = sys.executable
5905
+ for f in config.filters:
5906
+ if not config.find_config_dir('filters', f):
5907
+ raise EAsciiDoc,'missing filter: %s' % f
5570
5908
  if doctype not in (None,'article','manpage','book'):
5571
5909
  raise EAsciiDoc,'illegal document type'
5572
5910
  # Set processing options.
@@ -5602,12 +5940,14 @@ def asciidoc(backend, doctype, confiles, infile, outfile, options):
5602
5940
  has_header = document.parse_header(doctype,backend)
5603
5941
  # doctype is now finalized.
5604
5942
  document.attributes['doctype-'+document.doctype] = ''
5943
+ config.set_theme_attributes()
5605
5944
  # Load backend configuration files.
5606
5945
  if '-e' not in options:
5607
5946
  f = document.backend + '.conf'
5608
- if not config.find_in_dirs(f):
5609
- message.warning('missing backend conf file: %s' % f, linenos=False)
5610
- config.load_backend()
5947
+ conffile = config.load_backend()
5948
+ if not conffile:
5949
+ raise EAsciiDoc,'missing backend conf file: %s' % f
5950
+ document.attributes['backend-confdir'] = os.path.dirname(conffile)
5611
5951
  # backend is now known.
5612
5952
  document.attributes['backend-'+document.backend] = ''
5613
5953
  document.attributes[document.backend+'-'+document.doctype] = ''
@@ -5656,10 +5996,13 @@ def asciidoc(backend, doctype, confiles, infile, outfile, options):
5656
5996
  # Document header attributes override conf file attributes.
5657
5997
  document.attributes.update(AttributeEntry.attributes)
5658
5998
  document.update_attributes()
5659
- # Configuration is fully loaded so can expand templates.
5999
+ # Configuration is fully loaded.
5660
6000
  config.expand_all_templates()
5661
6001
  # Check configuration for consistency.
5662
6002
  config.validate()
6003
+ # Initialize top level block name.
6004
+ if document.attributes.get('blockname'):
6005
+ AbstractBlock.blocknames.append(document.attributes['blockname'])
5663
6006
  paragraphs.initialize()
5664
6007
  lists.initialize()
5665
6008
  if config.dumping:
@@ -5765,7 +6108,7 @@ def execute(cmd,opts,args):
5765
6108
  """
5766
6109
  config.init(cmd)
5767
6110
  if len(args) > 1:
5768
- usage('To many arguments')
6111
+ usage('Too many arguments')
5769
6112
  sys.exit(1)
5770
6113
  backend = None
5771
6114
  doctype = None
@@ -5786,19 +6129,22 @@ def execute(cmd,opts,args):
5786
6129
  sys.exit(0)
5787
6130
  if o in ('-b','--backend'):
5788
6131
  backend = v
5789
- # config.cmd_attrs['backend'] = v
5790
6132
  if o in ('-c','--dump-conf'):
5791
6133
  options.append('-c')
5792
6134
  if o in ('-d','--doctype'):
5793
6135
  doctype = v
5794
- # config.cmd_attrs['doctype'] = v
5795
6136
  if o in ('-e','--no-conf'):
5796
6137
  options.append('-e')
5797
6138
  if o in ('-f','--conf-file'):
5798
6139
  confiles.append(v)
6140
+ if o == '--filter':
6141
+ config.filters.append(v)
5799
6142
  if o in ('-n','--section-numbers'):
5800
6143
  o = '-a'
5801
6144
  v = 'numbered'
6145
+ if o == '--theme':
6146
+ o = '-a'
6147
+ v = 'theme='+v
5802
6148
  if o in ('-a','--attribute'):
5803
6149
  e = parse_entry(v, allow_name_only=True)
5804
6150
  if not e:
@@ -5828,9 +6174,6 @@ def execute(cmd,opts,args):
5828
6174
  if len(args) == 0:
5829
6175
  usage('No source file specified')
5830
6176
  sys.exit(1)
5831
- # if not backend:
5832
- # usage('No --backend option specified')
5833
- # sys.exit(1)
5834
6177
  stdin,stdout = sys.stdin,sys.stdout
5835
6178
  try:
5836
6179
  infile = args[0]
@@ -5868,11 +6211,12 @@ if __name__ == '__main__':
5868
6211
  ['attribute=','backend=','conf-file=','doctype=','dump-conf',
5869
6212
  'help','no-conf','no-header-footer','out-file=',
5870
6213
  'section-numbers','verbose','version','safe','unsafe',
5871
- 'doctest','filter'])
6214
+ 'doctest','filter=','theme='])
5872
6215
  except getopt.GetoptError:
5873
6216
  message.stderr('illegal command options')
5874
6217
  sys.exit(1)
5875
- if '--doctest' in [opt[0] for opt in opts]:
6218
+ opt_names = [opt[0] for opt in opts]
6219
+ if '--doctest' in opt_names:
5876
6220
  # Run module doctests.
5877
6221
  import doctest
5878
6222
  options = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS
@@ -5882,21 +6226,32 @@ if __name__ == '__main__':
5882
6226
  sys.exit(0)
5883
6227
  else:
5884
6228
  sys.exit(1)
5885
- if '--filter' in [opt[0] for opt in opts]:
6229
+ # Look for plugin management commands.
6230
+ count = 0
6231
+ for o,v in opts:
6232
+ if o in ('-b','--backend','--filter','--theme'):
6233
+ if o == '-b':
6234
+ o = '--backend'
6235
+ plugin = o[2:]
6236
+ cmd = v
6237
+ if cmd not in Plugin.CMDS:
6238
+ continue
6239
+ count += 1
6240
+ if count > 1:
6241
+ die('--backend, --filter and --theme options are mutually exclusive')
6242
+ if count == 1:
6243
+ # Execute plugin management commands.
6244
+ if not cmd:
6245
+ die('missing --%s command' % plugin)
6246
+ if cmd not in Plugin.CMDS:
6247
+ die('illegal --%s command: %s' % (plugin, cmd))
6248
+ Plugin.type = plugin
5886
6249
  config.init(sys.argv[0])
5887
- config.verbose = bool(set(['-v','--verbose']) & set([opt[0] for opt in opts]))
5888
- if not args:
5889
- die('missing --filter command')
5890
- elif args[0] == 'install':
5891
- Filter.install(args[1:])
5892
- elif args[0] == 'remove':
5893
- Filter.remove(args[1:])
5894
- elif args[0] == 'list':
5895
- Filter.list()
5896
- else:
5897
- die('illegal --filter command: %s' % args[0])
5898
- sys.exit(0)
5899
- try:
5900
- execute(sys.argv[0],opts,args)
5901
- except KeyboardInterrupt:
5902
- sys.exit(1)
6250
+ config.verbose = bool(set(['-v','--verbose']) & set(opt_names))
6251
+ getattr(Plugin,cmd)(args)
6252
+ else:
6253
+ # Execute asciidoc.
6254
+ try:
6255
+ execute(sys.argv[0],opts,args)
6256
+ except KeyboardInterrupt:
6257
+ sys.exit(1)