PageRenderTime 58ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/asciidoc-8.4.5/asciidoc.py

#
Python | 5315 lines | 5123 code | 23 blank | 169 comment | 216 complexity | 3d8c49106162df3e11f65d3c3dd23451 MD5 | raw file
Possible License(s): GPL-2.0

Large files files are truncated, but you can click here to view the full file

  1. #!/usr/bin/env python
  2. """
  3. asciidoc - converts an AsciiDoc text file to DocBook, HTML or LinuxDoc
  4. Copyright (C) 2002-2009 Stuart Rackham. Free use of this software is granted
  5. under the terms of the GNU General Public License (GPL).
  6. """
  7. import sys, os, re, time, traceback, tempfile, subprocess, codecs, locale
  8. ### Used by asciidocapi.py ###
  9. VERSION = '8.4.5' # See CHANGLOG file for version history.
  10. MIN_PYTHON_VERSION = 2.4 # Require this version of Python or better.
  11. #---------------------------------------------------------------------------
  12. # Program constants.
  13. #---------------------------------------------------------------------------
  14. DEFAULT_BACKEND = 'xhtml11'
  15. DEFAULT_DOCTYPE = 'article'
  16. # Allowed substitution options for List, Paragraph and DelimitedBlock
  17. # definition subs entry.
  18. SUBS_OPTIONS = ('specialcharacters','quotes','specialwords',
  19. 'replacements', 'attributes','macros','callouts','normal','verbatim',
  20. 'none','replacements2')
  21. # Default value for unspecified subs and presubs configuration file entries.
  22. SUBS_NORMAL = ('specialcharacters','quotes','attributes',
  23. 'specialwords','replacements','macros','replacements2')
  24. SUBS_VERBATIM = ('specialcharacters','callouts')
  25. NAME_RE = r'(?u)[^\W\d][-\w]*' # Valid section or attrbibute name.
  26. #---------------------------------------------------------------------------
  27. # Utility functions and classes.
  28. #---------------------------------------------------------------------------
  29. class EAsciiDoc(Exception): pass
  30. class OrderedDict(dict):
  31. """
  32. Dictionary ordered by insertion order.
  33. Python Cookbook: Ordered Dictionary, Submitter: David Benjamin.
  34. http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/107747
  35. """
  36. def __init__(self, d=None, **kwargs):
  37. self._keys = []
  38. if d is None: d = kwargs
  39. dict.__init__(self, d)
  40. def __delitem__(self, key):
  41. dict.__delitem__(self, key)
  42. self._keys.remove(key)
  43. def __setitem__(self, key, item):
  44. dict.__setitem__(self, key, item)
  45. if key not in self._keys: self._keys.append(key)
  46. def clear(self):
  47. dict.clear(self)
  48. self._keys = []
  49. def copy(self):
  50. d = dict.copy(self)
  51. d._keys = self._keys[:]
  52. return d
  53. def items(self):
  54. return zip(self._keys, self.values())
  55. def keys(self):
  56. return self._keys
  57. def popitem(self):
  58. try:
  59. key = self._keys[-1]
  60. except IndexError:
  61. raise KeyError('dictionary is empty')
  62. val = self[key]
  63. del self[key]
  64. return (key, val)
  65. def setdefault(self, key, failobj = None):
  66. dict.setdefault(self, key, failobj)
  67. if key not in self._keys: self._keys.append(key)
  68. def update(self, d=None, **kwargs):
  69. if d is None:
  70. d = kwargs
  71. dict.update(self, d)
  72. for key in d.keys():
  73. if key not in self._keys: self._keys.append(key)
  74. def values(self):
  75. return map(self.get, self._keys)
  76. class AttrDict(dict):
  77. """
  78. Like a dictionary except values can be accessed as attributes i.e. obj.foo
  79. can be used in addition to obj['foo'].
  80. If an item is not present None is returned.
  81. """
  82. def __getattr__(self, key):
  83. try: return self[key]
  84. except KeyError, k: return None
  85. def __setattr__(self, key, value):
  86. self[key] = value
  87. def __delattr__(self, key):
  88. try: del self[key]
  89. except KeyError, k: raise AttributeError, k
  90. def __repr__(self):
  91. return '<AttrDict ' + dict.__repr__(self) + '>'
  92. def __getstate__(self):
  93. return dict(self)
  94. def __setstate__(self,value):
  95. for k,v in value.items(): self[k]=v
  96. class Trace(object):
  97. """
  98. Used in conjunction with the 'trace' attribute to generate diagnostic
  99. output. There is a single global instance of this class named trace.
  100. """
  101. SUBS_NAMES = ('specialcharacters','quotes','specialwords',
  102. 'replacements', 'attributes','macros','callouts',
  103. 'replacements2')
  104. def __init__(self):
  105. self.name_re = '' # Regexp pattern to match trace names.
  106. self.linenos = True
  107. self.offset = 0
  108. def __call__(self, name, before, after=None):
  109. """
  110. Print trace message if tracing is on and the trace 'name' matches the
  111. document 'trace' attribute (treated as a regexp).
  112. The 'before' and 'after' messages are only printed if they differ.
  113. """
  114. name_re = document.attributes.get('trace')
  115. if name_re == 'subs': # Alias for all the inline substitutions.
  116. name_re = '|'.join(self.SUBS_NAMES)
  117. self.name_re = name_re
  118. if self.name_re is not None:
  119. msg = message.format(name, 'TRACE: ', self.linenos, offset=self.offset)
  120. if before != after and re.match(self.name_re,name):
  121. if is_array(before):
  122. before = '\n'.join(before)
  123. if after is None:
  124. msg += '\n%s\n' % before
  125. else:
  126. if is_array(after):
  127. after = '\n'.join(after)
  128. msg += '\n<<<\n%s\n>>>\n%s\n' % (before,after)
  129. message.stderr(msg)
  130. class Message:
  131. """
  132. Message functions.
  133. """
  134. def __init__(self):
  135. self.linenos = None # Used to globally override line numbers.
  136. self.messages = []
  137. def stderr(self,line=''):
  138. self.messages.append(line)
  139. if __name__ == '__main__':
  140. sys.stderr.write(line+os.linesep)
  141. def verbose(self, msg,linenos=True):
  142. if config.verbose:
  143. msg = self.format(msg,linenos=linenos)
  144. self.stderr(msg)
  145. def warning(self, msg,linenos=True,offset=0):
  146. msg = self.format(msg,'WARNING: ',linenos,offset=offset)
  147. document.has_warnings = True
  148. self.stderr(msg)
  149. def deprecated(self, msg, linenos=True):
  150. msg = self.format(msg, 'DEPRECATED: ', linenos)
  151. self.stderr(msg)
  152. def format(self, msg, prefix='', linenos=True, cursor=None, offset=0):
  153. """Return formatted message string."""
  154. if self.linenos is not False and ((linenos or self.linenos) and reader.cursor):
  155. if cursor is None:
  156. cursor = reader.cursor
  157. prefix += '%s: line %d: ' % (os.path.basename(cursor[0]),cursor[1]+offset)
  158. return prefix + msg
  159. def error(self, msg, cursor=None, halt=False):
  160. """
  161. Report fatal error.
  162. If halt=True raise EAsciiDoc exception.
  163. If halt=False don't exit application, continue in the hope of reporting
  164. all fatal errors finishing with a non-zero exit code.
  165. """
  166. if halt:
  167. raise EAsciiDoc, self.format(msg,linenos=False,cursor=cursor)
  168. else:
  169. msg = self.format(msg,'ERROR: ',cursor=cursor)
  170. self.stderr(msg)
  171. document.has_errors = True
  172. def unsafe(self, msg):
  173. self.error('unsafe: '+msg)
  174. def file_in(fname, directory):
  175. """Return True if file fname resides inside directory."""
  176. assert os.path.isfile(fname)
  177. # Empty directory (not to be confused with None) is the current directory.
  178. if directory == '':
  179. directory = os.getcwd()
  180. else:
  181. assert os.path.isdir(directory)
  182. directory = os.path.realpath(directory)
  183. fname = os.path.realpath(fname)
  184. return os.path.commonprefix((directory, fname)) == directory
  185. def safe():
  186. return document.safe
  187. def is_safe_file(fname, directory=None):
  188. # A safe file must reside in directory directory (defaults to the source
  189. # file directory).
  190. if directory is None:
  191. if document.infile == '<stdin>':
  192. return not safe()
  193. directory = os.path.dirname(document.infile)
  194. elif directory == '':
  195. directory = '.'
  196. return not safe() or file_in(fname, directory)
  197. def safe_filename(fname, parentdir):
  198. """
  199. Return file name which must reside in the parent file directory.
  200. Return None if file is not found or not safe.
  201. """
  202. if not os.path.isabs(fname):
  203. # Include files are relative to parent document
  204. # directory.
  205. fname = os.path.join(parentdir,fname)
  206. if not os.path.isfile(fname):
  207. message.warning('include file not found: %s' % fname)
  208. return None
  209. if not is_safe_file(fname, parentdir):
  210. message.unsafe('include file: %s' % fname)
  211. return None
  212. return fname
  213. def assign(dst,src):
  214. """Assign all attributes from 'src' object to 'dst' object."""
  215. for a,v in src.__dict__.items():
  216. setattr(dst,a,v)
  217. def strip_quotes(s):
  218. """Trim white space and, if necessary, quote characters from s."""
  219. s = s.strip()
  220. # Strip quotation mark characters from quoted strings.
  221. if len(s) >= 3 and s[0] == '"' and s[-1] == '"':
  222. s = s[1:-1]
  223. return s
  224. def is_re(s):
  225. """Return True if s is a valid regular expression else return False."""
  226. try: re.compile(s)
  227. except: return False
  228. else: return True
  229. def re_join(relist):
  230. """Join list of regular expressions re1,re2,... to single regular
  231. expression (re1)|(re2)|..."""
  232. if len(relist) == 0:
  233. return None
  234. result = []
  235. # Delete named groups to avoid ambiguity.
  236. for s in relist:
  237. result.append(re.sub(r'\?P<\S+?>','',s))
  238. result = ')|('.join(result)
  239. result = '('+result+')'
  240. return result
  241. def validate(value,rule,errmsg):
  242. """Validate value against rule expression. Throw EAsciiDoc exception with
  243. errmsg if validation fails."""
  244. try:
  245. if not eval(rule.replace('$',str(value))):
  246. raise EAsciiDoc,errmsg
  247. except Exception:
  248. raise EAsciiDoc,errmsg
  249. return value
  250. def lstrip_list(s):
  251. """
  252. Return list with empty items from start of list removed.
  253. """
  254. for i in range(len(s)):
  255. if s[i]: break
  256. else:
  257. return []
  258. return s[i:]
  259. def rstrip_list(s):
  260. """
  261. Return list with empty items from end of list removed.
  262. """
  263. for i in range(len(s)-1,-1,-1):
  264. if s[i]: break
  265. else:
  266. return []
  267. return s[:i+1]
  268. def strip_list(s):
  269. """
  270. Return list with empty items from start and end of list removed.
  271. """
  272. s = lstrip_list(s)
  273. s = rstrip_list(s)
  274. return s
  275. def is_array(obj):
  276. """
  277. Return True if object is list or tuple type.
  278. """
  279. return isinstance(obj,list) or isinstance(obj,tuple)
  280. def dovetail(lines1, lines2):
  281. """
  282. Append list or tuple of strings 'lines2' to list 'lines1'. Join the last
  283. non-blank item in 'lines1' with the first non-blank item in 'lines2' into a
  284. single string.
  285. """
  286. assert is_array(lines1)
  287. assert is_array(lines2)
  288. lines1 = strip_list(lines1)
  289. lines2 = strip_list(lines2)
  290. if not lines1 or not lines2:
  291. return list(lines1) + list(lines2)
  292. result = list(lines1[:-1])
  293. result.append(lines1[-1] + lines2[0])
  294. result += list(lines2[1:])
  295. return result
  296. def dovetail_tags(stag,content,etag):
  297. """Merge the end tag with the first content line and the last
  298. content line with the end tag. This ensures verbatim elements don't
  299. include extraneous opening and closing line breaks."""
  300. return dovetail(dovetail(stag,content), etag)
  301. def parse_attributes(attrs,dict):
  302. """Update a dictionary with name/value attributes from the attrs string.
  303. The attrs string is a comma separated list of values and keyword name=value
  304. pairs. Values must preceed keywords and are named '1','2'... The entire
  305. attributes list is named '0'. If keywords are specified string values must
  306. be quoted. Examples:
  307. attrs: ''
  308. dict: {}
  309. attrs: 'hello,world'
  310. dict: {'2': 'world', '0': 'hello,world', '1': 'hello'}
  311. attrs: '"hello", planet="earth"'
  312. dict: {'planet': 'earth', '0': '"hello",planet="earth"', '1': 'hello'}
  313. """
  314. def f(*args,**keywords):
  315. # Name and add aguments '1','2'... to keywords.
  316. for i in range(len(args)):
  317. if not str(i+1) in keywords:
  318. keywords[str(i+1)] = args[i]
  319. return keywords
  320. if not attrs:
  321. return
  322. dict['0'] = attrs
  323. # Replace line separators with spaces so line spanning works.
  324. s = re.sub(r'\s', ' ', attrs)
  325. try:
  326. d = eval('f('+s+')')
  327. # Attributes must evaluate to strings, numbers or None.
  328. for v in d.values():
  329. if not (isinstance(v,str) or isinstance(v,int) or isinstance(v,float) or v is None):
  330. raise
  331. except Exception:
  332. s = s.replace('"','\\"')
  333. s = s.split(',')
  334. s = map(lambda x: '"' + x.strip() + '"', s)
  335. s = ','.join(s)
  336. try:
  337. d = eval('f('+s+')')
  338. except Exception:
  339. return # If there's a syntax error leave with {0}=attrs.
  340. for k in d.keys(): # Drop any empty positional arguments.
  341. if d[k] == '': del d[k]
  342. dict.update(d)
  343. assert len(d) > 0
  344. def parse_named_attributes(s,attrs):
  345. """Update a attrs dictionary with name="value" attributes from the s string.
  346. Returns False if invalid syntax.
  347. Example:
  348. attrs: 'star="sun",planet="earth"'
  349. dict: {'planet':'earth', 'star':'sun'}
  350. """
  351. def f(**keywords): return keywords
  352. try:
  353. d = eval('f('+s+')')
  354. attrs.update(d)
  355. return True
  356. except Exception:
  357. return False
  358. def parse_list(s):
  359. """Parse comma separated string of Python literals. Return a tuple of of
  360. parsed values."""
  361. try:
  362. result = eval('tuple(['+s+'])')
  363. except Exception:
  364. raise EAsciiDoc,'malformed list: '+s
  365. return result
  366. def parse_options(options,allowed,errmsg):
  367. """Parse comma separated string of unquoted option names and return as a
  368. tuple of valid options. 'allowed' is a list of allowed option values.
  369. If allowed=() then all legitimate names are allowed.
  370. 'errmsg' is an error message prefix if an illegal option error is thrown."""
  371. result = []
  372. if options:
  373. for s in re.split(r'\s*,\s*',options):
  374. if (allowed and s not in allowed) or not is_name(s):
  375. raise EAsciiDoc,'%s: %s' % (errmsg,s)
  376. result.append(s)
  377. return tuple(result)
  378. def symbolize(s):
  379. """Drop non-symbol characters and convert to lowercase."""
  380. return re.sub(r'(?u)[^\w\-_]', '', s).lower()
  381. def is_name(s):
  382. """Return True if s is valid attribute, macro or tag name
  383. (starts with alpha containing alphanumeric and dashes only)."""
  384. return re.match(r'^'+NAME_RE+r'$',s) is not None
  385. def subs_quotes(text):
  386. """Quoted text is marked up and the resulting text is
  387. returned."""
  388. keys = config.quotes.keys()
  389. for q in keys:
  390. i = q.find('|')
  391. if i != -1 and q != '|' and q != '||':
  392. lq = q[:i] # Left quote.
  393. rq = q[i+1:] # Right quote.
  394. else:
  395. lq = rq = q
  396. tag = config.quotes[q]
  397. # Unconstrained quotes prefix the tag name with a hash.
  398. if tag[0] == '#':
  399. tag = tag[1:]
  400. # Unconstrained quotes can appear anywhere.
  401. reo = re.compile(r'(?msu)(^|.)(\[(?P<attrlist>[^[\]]+?)\])?' \
  402. + r'(?:' + re.escape(lq) + r')' \
  403. + r'(?P<content>.+?)(?:'+re.escape(rq)+r')')
  404. else:
  405. # The text within constrained quotes must be bounded by white space.
  406. # Non-word (\W) characters are allowed at boundaries to accomodate
  407. # enveloping quotes.
  408. reo = re.compile(r'(?msu)(^|\W)(\[(?P<attrlist>[^[\]]+?)\])?' \
  409. + r'(?:' + re.escape(lq) + r')' \
  410. + r'(?P<content>\S|\S.*?\S)(?:'+re.escape(rq)+r')(?=\W|$)')
  411. pos = 0
  412. while True:
  413. mo = reo.search(text,pos)
  414. if not mo: break
  415. if text[mo.start()] == '\\':
  416. # Delete leading backslash.
  417. text = text[:mo.start()] + text[mo.start()+1:]
  418. # Skip past start of match.
  419. pos = mo.start() + 1
  420. else:
  421. attrlist = {}
  422. parse_attributes(mo.group('attrlist'), attrlist)
  423. stag,etag = config.tag(tag, attrlist)
  424. s = mo.group(1) + stag + mo.group('content') + etag
  425. text = text[:mo.start()] + s + text[mo.end():]
  426. pos = mo.start() + len(s)
  427. return text
  428. def subs_tag(tag,dict={}):
  429. """Perform attribute substitution and split tag string returning start, end
  430. tag tuple (c.f. Config.tag())."""
  431. if not tag:
  432. return [None,None]
  433. s = subs_attrs(tag,dict)
  434. if not s:
  435. message.warning('tag \'%s\' dropped: contains undefined attribute' % tag)
  436. return [None,None]
  437. result = s.split('|')
  438. if len(result) == 1:
  439. return result+[None]
  440. elif len(result) == 2:
  441. return result
  442. else:
  443. raise EAsciiDoc,'malformed tag: %s' % tag
  444. def parse_entry(entry, dict=None, unquote=False, unique_values=False,
  445. allow_name_only=False, escape_delimiter=True):
  446. """Parse name=value entry to dictionary 'dict'. Return tuple (name,value)
  447. or None if illegal entry.
  448. If name= then value is set to ''.
  449. If name and allow_name_only=True then value is set to ''.
  450. If name! and allow_name_only=True then value is set to None.
  451. Leading and trailing white space is striped from 'name' and 'value'.
  452. 'name' can contain any printable characters.
  453. If the '=' delimiter character is allowed in the 'name' then
  454. it must be escaped with a backslash and escape_delimiter must be True.
  455. If 'unquote' is True leading and trailing double-quotes are stripped from
  456. 'name' and 'value'.
  457. If unique_values' is True then dictionary entries with the same value are
  458. removed before the parsed entry is added."""
  459. if escape_delimiter:
  460. mo = re.search(r'(?:[^\\](=))',entry)
  461. else:
  462. mo = re.search(r'(=)',entry)
  463. if mo: # name=value entry.
  464. if mo.group(1):
  465. name = entry[:mo.start(1)]
  466. if escape_delimiter:
  467. name = name.replace(r'\=','=') # Unescape \= in name.
  468. value = entry[mo.end(1):]
  469. elif allow_name_only and entry: # name or name! entry.
  470. name = entry
  471. if name[-1] == '!':
  472. name = name[:-1]
  473. value = None
  474. else:
  475. value = ''
  476. else:
  477. return None
  478. if unquote:
  479. name = strip_quotes(name)
  480. if value is not None:
  481. value = strip_quotes(value)
  482. else:
  483. name = name.strip()
  484. if value is not None:
  485. value = value.strip()
  486. if not name:
  487. return None
  488. if dict is not None:
  489. if unique_values:
  490. for k,v in dict.items():
  491. if v == value: del dict[k]
  492. dict[name] = value
  493. return name,value
  494. def parse_entries(entries, dict, unquote=False, unique_values=False,
  495. allow_name_only=False,escape_delimiter=True):
  496. """Parse name=value entries from from lines of text in 'entries' into
  497. dictionary 'dict'. Blank lines are skipped."""
  498. entries = config.expand_templates(entries)
  499. for entry in entries:
  500. if entry and not parse_entry(entry, dict, unquote, unique_values,
  501. allow_name_only, escape_delimiter):
  502. raise EAsciiDoc,'malformed section entry: %s' % entry
  503. def load_conf_file(sections, fname, dir, namepat=NAME_RE):
  504. """Loads sections dictionary with sections from file fname.
  505. Existing sections are overlaid. Silently skips missing configuration
  506. files."""
  507. if dir:
  508. fname = os.path.join(dir, fname)
  509. # Sliently skip missing configuration file.
  510. if not os.path.isfile(fname):
  511. return
  512. reo = re.compile(r'^\[(?P<section>'+namepat+')\]\s*$')
  513. section,contents = '',[]
  514. for line in open(fname):
  515. if line and line[0] == '#': # Skip comment lines.
  516. continue
  517. line = line.rstrip()
  518. found = reo.findall(line)
  519. if found:
  520. if section: # Store previous section.
  521. sections[section] = contents
  522. section = found[0].lower()
  523. contents = []
  524. else:
  525. contents.append(line)
  526. if section and contents: # Store last section.
  527. sections[section] = contents
  528. def dump_section(name,dict,f=sys.stdout):
  529. """Write parameters in 'dict' as in configuration file section format with
  530. section 'name'."""
  531. f.write('[%s]%s' % (name,writer.newline))
  532. for k,v in dict.items():
  533. k = str(k)
  534. k = k.replace('=',r'\=') # Escape = in name.
  535. # Quote if necessary.
  536. if len(k) != len(k.strip()):
  537. k = '"'+k+'"'
  538. if v and len(v) != len(v.strip()):
  539. v = '"'+v+'"'
  540. if v is None:
  541. # Don't dump undefined attributes.
  542. continue
  543. else:
  544. s = k+'='+v
  545. if s[0] == '#':
  546. s = '\\' + s # Escape so not treated as comment lines.
  547. f.write('%s%s' % (s,writer.newline))
  548. f.write(writer.newline)
  549. def update_attrs(attrs,dict):
  550. """Update 'attrs' dictionary with parsed attributes in dictionary 'dict'."""
  551. for k,v in dict.items():
  552. if not is_name(k):
  553. raise EAsciiDoc,'illegal attribute name: %s' % k
  554. attrs[k] = v
  555. def filter_lines(filter_cmd, lines, attrs={}):
  556. """
  557. Run 'lines' through the 'filter_cmd' shell command and return the result.
  558. The 'attrs' dictionary contains additional filter attributes.
  559. """
  560. def findfilter(name,dir,filter):
  561. """Find filter file 'fname' with style name 'name' in directory
  562. 'dir'. Return found file path or None if not found."""
  563. if name:
  564. result = os.path.join(dir,'filters',name,filter)
  565. if os.path.isfile(result):
  566. return result
  567. result = os.path.join(dir,'filters',filter)
  568. if os.path.isfile(result):
  569. return result
  570. return None
  571. # Return input lines if there's not filter.
  572. if not filter_cmd or not filter_cmd.strip():
  573. return lines
  574. # Perform attributes substitution on the filter command.
  575. s = subs_attrs(filter_cmd, attrs)
  576. if not s:
  577. raise EAsciiDoc,'undefined filter attribute in command: %s' % filter_cmd
  578. filter_cmd = s.strip()
  579. # Parse for quoted and unquoted command and command tail.
  580. # Double quoted.
  581. mo = re.match(r'^"(?P<cmd>[^"]+)"(?P<tail>.*)$', filter_cmd)
  582. if not mo:
  583. # Single quoted.
  584. mo = re.match(r"^'(?P<cmd>[^']+)'(?P<tail>.*)$", filter_cmd)
  585. if not mo:
  586. # Unquoted catch all.
  587. mo = re.match(r'^(?P<cmd>\S+)(?P<tail>.*)$', filter_cmd)
  588. cmd = mo.group('cmd').strip()
  589. found = None
  590. if not os.path.dirname(cmd):
  591. # Filter command has no directory path so search filter directories.
  592. filtername = attrs.get('style')
  593. if USER_DIR:
  594. found = findfilter(filtername, USER_DIR, cmd)
  595. if not found:
  596. found = findfilter(filtername, CONF_DIR, cmd)
  597. if not found:
  598. found = findfilter(filtername, DATA_DIR, cmd)
  599. if not found:
  600. found = findfilter(filtername, APP_DIR, cmd)
  601. else:
  602. if os.path.isfile(cmd):
  603. found = cmd
  604. else:
  605. message.warning('filter not found: %s' % cmd)
  606. if found:
  607. filter_cmd = '"' + found + '"' + mo.group('tail')
  608. if sys.platform == 'win32':
  609. # Windows doesn't like running scripts directly so explicitly
  610. # specify interpreter.
  611. if found:
  612. if cmd.endswith('.py'):
  613. filter_cmd = 'python ' + filter_cmd
  614. elif cmd.endswith('.rb'):
  615. filter_cmd = 'ruby ' + filter_cmd
  616. message.verbose('filtering: ' + filter_cmd)
  617. try:
  618. p = subprocess.Popen(filter_cmd, shell=True,
  619. stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  620. output = p.communicate(os.linesep.join(lines))[0]
  621. except Exception:
  622. raise EAsciiDoc,'filter error: %s: %s' % (filter_cmd, sys.exc_info()[1])
  623. if output:
  624. result = [s.rstrip() for s in output.split(os.linesep)]
  625. else:
  626. result = []
  627. filter_status = p.wait()
  628. if filter_status:
  629. message.warning('filter non-zero exit code: %s: returned %d' %
  630. (filter_cmd, filter_status))
  631. if lines and not result:
  632. message.warning('no output from filter: %s' % filter_cmd)
  633. return result
  634. def system(name, args, is_macro=False):
  635. """
  636. Evaluate a system attribute ({name:args}) or system block macro
  637. (name::[args]). If is_macro is True then we are processing a system
  638. block macro otherwise it's a system attribute.
  639. NOTE: The include1 attribute is used internally by the include1::[] macro
  640. and is not for public use.
  641. """
  642. if is_macro:
  643. syntax = '%s::[%s]'
  644. separator = '\n'
  645. else:
  646. syntax = '{%s:%s}'
  647. separator = writer.newline
  648. if name not in ('eval','sys','sys2','include','include1'):
  649. if is_macro:
  650. msg = 'illegal system macro name: %s' % name
  651. else:
  652. msg = 'illegal system attribute name: %s' % name
  653. message.warning(msg)
  654. return None
  655. if is_macro:
  656. s = subs_attrs(args)
  657. if s is None:
  658. message.warning('skipped %s: undefined attribute in: %s' % (name,args))
  659. return None
  660. args = s
  661. if name != 'include1':
  662. message.verbose(('evaluating: '+syntax) % (name,args))
  663. if safe() and name not in ('include','include1'):
  664. message.unsafe(syntax % (name,args))
  665. return None
  666. result = None
  667. if name == 'eval':
  668. try:
  669. result = eval(args)
  670. if result is True:
  671. result = ''
  672. elif result is False:
  673. result = None
  674. elif result is not None:
  675. result = str(result)
  676. except Exception:
  677. message.warning((syntax+': expression evaluation error') % (name,args))
  678. elif name in ('sys','sys2'):
  679. result = ''
  680. fd,tmp = tempfile.mkstemp()
  681. os.close(fd)
  682. try:
  683. cmd = args
  684. cmd = cmd + (' > %s' % tmp)
  685. if name == 'sys2':
  686. cmd = cmd + ' 2>&1'
  687. if os.system(cmd):
  688. message.warning((syntax+': non-zero exit status') % (name,args))
  689. try:
  690. if os.path.isfile(tmp):
  691. lines = [s.rstrip() for s in open(tmp)]
  692. else:
  693. lines = []
  694. except Exception:
  695. raise EAsciiDoc,(syntax+': temp file read error') % (name,args)
  696. result = separator.join(lines)
  697. finally:
  698. if os.path.isfile(tmp):
  699. os.remove(tmp)
  700. elif name == 'include':
  701. if not os.path.exists(args):
  702. message.warning((syntax+': file does not exist') % (name,args))
  703. elif not is_safe_file(args):
  704. message.unsafe(syntax % (name,args))
  705. else:
  706. result = [s.rstrip() for s in open(args)]
  707. if result:
  708. result = subs_attrs(result)
  709. result = separator.join(result)
  710. result = result.expandtabs(reader.tabsize)
  711. else:
  712. result = ''
  713. elif name == 'include1':
  714. result = separator.join(config.include1[args])
  715. else:
  716. assert False
  717. return result
  718. def subs_attrs(lines, dictionary=None):
  719. """Substitute 'lines' of text with attributes from the global
  720. document.attributes dictionary and from 'dictionary' ('dictionary'
  721. entries take precedence). Return a tuple of the substituted lines. 'lines'
  722. containing undefined attributes are deleted. If 'lines' is a string then
  723. return a string.
  724. - Attribute references are substituted in the following order: simple,
  725. conditional, system.
  726. - Attribute references inside 'dictionary' entry values are substituted.
  727. """
  728. def end_brace(text,start):
  729. """Return index following end brace that matches brace at start in
  730. text."""
  731. assert text[start] == '{'
  732. n = 0
  733. result = start
  734. for c in text[start:]:
  735. # Skip braces that are followed by a backslash.
  736. if result == len(text)-1 or text[result+1] != '\\':
  737. if c == '{': n = n + 1
  738. elif c == '}': n = n - 1
  739. result = result + 1
  740. if n == 0: break
  741. return result
  742. if type(lines) == str:
  743. string_result = True
  744. lines = [lines]
  745. else:
  746. string_result = False
  747. lines = list(lines)
  748. if dictionary is None:
  749. attrs = document.attributes
  750. else:
  751. # Remove numbered document attributes so they don't clash with
  752. # attribute list positional attributes.
  753. attrs = {}
  754. for k,v in document.attributes.items():
  755. if not re.match(r'^\d+$', k):
  756. attrs[k] = v
  757. # Substitute attribute references inside dictionary values.
  758. dictionary = dictionary.copy()
  759. for k,v in dictionary.items():
  760. if v is None:
  761. del dictionary[k]
  762. else:
  763. v = subs_attrs(str(v))
  764. if v is None:
  765. del dictionary[k]
  766. else:
  767. dictionary[k] = v
  768. attrs.update(dictionary)
  769. # Substitute all attributes in all lines.
  770. for i in range(len(lines)-1,-1,-1): # Reverse iterate lines.
  771. text = lines[i]
  772. # Make it easier for regular expressions.
  773. text = text.replace('\\{','{\\')
  774. text = text.replace('\\}','}\\')
  775. # Expand simple attributes ({name}).
  776. # Nested attributes not allowed.
  777. reo = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w]*?)\}(?!\\)')
  778. pos = 0
  779. while True:
  780. mo = reo.search(text,pos)
  781. if not mo: break
  782. s = attrs.get(mo.group('name'))
  783. if s is None:
  784. pos = mo.end()
  785. else:
  786. s = str(s)
  787. text = text[:mo.start()] + s + text[mo.end():]
  788. pos = mo.start() + len(s)
  789. # Expand conditional attributes.
  790. reo = re.compile(r'(?su)\{(?P<name>[^\\\W][-\w]*?)' \
  791. r'(?P<op>\=|\?|!|#|%|@|\$)' \
  792. r'(?P<value>.*?)\}(?!\\)')
  793. pos = 0
  794. while True:
  795. mo = reo.search(text,pos)
  796. if not mo: break
  797. attr = mo.group()
  798. name = mo.group('name')
  799. lval = attrs.get(name)
  800. op = mo.group('op')
  801. # mo.end() is not good enough because '{x={y}}' matches '{x={y}'.
  802. end = end_brace(text,mo.start())
  803. rval = text[mo.start('value'):end-1]
  804. if lval is None:
  805. if op == '=': s = rval
  806. elif op == '?': s = ''
  807. elif op == '!': s = rval
  808. elif op == '#': s = '{'+name+'}' # So the line is dropped.
  809. elif op == '%': s = rval
  810. elif op in ('@','$'):
  811. s = '{'+name+'}' # So the line is dropped.
  812. else:
  813. assert False, 'illegal attribute: %s' % attr
  814. else:
  815. if op == '=': s = lval
  816. elif op == '?': s = rval
  817. elif op == '!': s = ''
  818. elif op == '#': s = rval
  819. elif op == '%': s = '{zzzzz}' # So the line is dropped.
  820. elif op in ('@','$'):
  821. v = re.split(r'(?<!\\):',rval)
  822. if len(v) not in (2,3):
  823. message.error('illegal attribute syntax: %s' % attr)
  824. s = ''
  825. elif not is_re('^'+v[0]+'$'):
  826. message.error('illegal attribute regexp: %s' % attr)
  827. s = ''
  828. else:
  829. v = [s.replace('\\:',':') for s in v]
  830. re_mo = re.match('^'+v[0]+'$',lval)
  831. if op == '@':
  832. if re_mo:
  833. s = v[1] # {<name>@<re>:<v1>[:<v2>]}
  834. else:
  835. if len(v) == 3: # {<name>@<re>:<v1>:<v2>}
  836. s = v[2]
  837. else: # {<name>@<re>:<v1>}
  838. s = ''
  839. else:
  840. if re_mo:
  841. if len(v) == 2: # {<name>$<re>:<v1>}
  842. s = v[1]
  843. elif v[1] == '': # {<name>$<re>::<v2>}
  844. s = '{zzzzz}' # So the line is dropped.
  845. else: # {<name>$<re>:<v1>:<v2>}
  846. s = v[1]
  847. else:
  848. if len(v) == 2: # {<name>$<re>:<v1>}
  849. s = '{zzzzz}' # So the line is dropped.
  850. else: # {<name>$<re>:<v1>:<v2>}
  851. s = v[2]
  852. else:
  853. assert False, 'illegal attribute: %s' % attr
  854. s = str(s)
  855. text = text[:mo.start()] + s + text[end:]
  856. pos = mo.start() + len(s)
  857. # Drop line if it contains unsubstituted {name} references.
  858. skipped = re.search(r'(?su)\{[^\\\W][-\w]*?\}(?!\\)', text)
  859. if skipped:
  860. del lines[i]
  861. continue;
  862. # Expand system attributes.
  863. reo = re.compile(r'(?su)\{(?P<action>[^\\\W][-\w]*?):(?P<expr>.*?)\}(?!\\)')
  864. skipped = False
  865. pos = 0
  866. while True:
  867. mo = reo.search(text,pos)
  868. if not mo: break
  869. expr = mo.group('expr')
  870. expr = expr.replace('{\\','{')
  871. expr = expr.replace('}\\','}')
  872. s = system(mo.group('action'),expr)
  873. if s is None:
  874. skipped = True
  875. break
  876. text = text[:mo.start()] + s + text[mo.end():]
  877. pos = mo.start() + len(s)
  878. # Drop line if the action returns None.
  879. if skipped:
  880. del lines[i]
  881. continue;
  882. # Remove backslash from escaped entries.
  883. text = text.replace('{\\','{')
  884. text = text.replace('}\\','}')
  885. lines[i] = text
  886. if string_result:
  887. if lines:
  888. return '\n'.join(lines)
  889. else:
  890. return None
  891. else:
  892. return tuple(lines)
  893. def char_encoding():
  894. encoding = document.attributes.get('encoding')
  895. if encoding:
  896. try:
  897. codecs.lookup(encoding)
  898. except LookupError,e:
  899. raise EAsciiDoc,str(e)
  900. return encoding
  901. def char_len(s):
  902. return len(char_decode(s))
  903. def char_decode(s):
  904. if char_encoding():
  905. try:
  906. return s.decode(char_encoding())
  907. except Exception:
  908. raise EAsciiDoc, \
  909. "'%s' codec can't decode \"%s\"" % (char_encoding(), s)
  910. else:
  911. return s
  912. def char_encode(s):
  913. if char_encoding():
  914. return s.encode(char_encoding())
  915. else:
  916. return s
  917. def time_str(t):
  918. """Convert seconds since the Epoch to formatted local time string."""
  919. t = time.localtime(t)
  920. s = time.strftime('%H:%M:%S',t)
  921. if time.daylight:
  922. result = s + ' ' + time.tzname[1]
  923. else:
  924. result = s + ' ' + time.tzname[0]
  925. # Attempt to convert the localtime to the output encoding.
  926. try:
  927. result = char_encode(result.decode(locale.getdefaultlocale()[1]))
  928. except Exception:
  929. pass
  930. return result
  931. def date_str(t):
  932. """Convert seconds since the Epoch to formatted local date string."""
  933. t = time.localtime(t)
  934. return time.strftime('%Y-%m-%d',t)
  935. class Lex:
  936. """Lexical analysis routines. Static methods and attributes only."""
  937. prev_element = None
  938. prev_cursor = None
  939. def __init__(self):
  940. raise AssertionError,'no class instances allowed'
  941. @staticmethod
  942. def next():
  943. """Returns class of next element on the input (None if EOF). The
  944. reader is assumed to be at the first line following a previous element,
  945. end of file or line one. Exits with the reader pointing to the first
  946. line of the next element or EOF (leading blank lines are skipped)."""
  947. reader.skip_blank_lines()
  948. if reader.eof(): return None
  949. # Optimization: If we've already checked for an element at this
  950. # position return the element.
  951. if Lex.prev_element and Lex.prev_cursor == reader.cursor:
  952. return Lex.prev_element
  953. if AttributeEntry.isnext():
  954. result = AttributeEntry
  955. elif AttributeList.isnext():
  956. result = AttributeList
  957. elif Title.isnext():
  958. result = Title
  959. elif macros.isnext():
  960. result = macros.current
  961. elif lists.isnext():
  962. result = lists.current
  963. elif blocks.isnext():
  964. result = blocks.current
  965. elif tables_OLD.isnext():
  966. result = tables_OLD.current
  967. elif tables.isnext():
  968. result = tables.current
  969. elif BlockTitle.isnext():
  970. result = BlockTitle
  971. else:
  972. if not paragraphs.isnext():
  973. raise EAsciiDoc,'paragraph expected'
  974. result = paragraphs.current
  975. # Optimization: Cache answer.
  976. Lex.prev_cursor = reader.cursor
  977. Lex.prev_element = result
  978. return result
  979. @staticmethod
  980. def canonical_subs(options):
  981. """Translate composite subs values."""
  982. if len(options) == 1:
  983. if options[0] == 'none':
  984. options = ()
  985. elif options[0] == 'normal':
  986. options = config.subsnormal
  987. elif options[0] == 'verbatim':
  988. options = config.subsverbatim
  989. return options
  990. @staticmethod
  991. def subs_1(s,options):
  992. """Perform substitution specified in 'options' (in 'options' order) on
  993. Does not process 'attributes' substitutions."""
  994. if not s:
  995. return s
  996. result = s
  997. options = Lex.canonical_subs(options)
  998. for o in options:
  999. if o == 'specialcharacters':
  1000. result = config.subs_specialchars(result)
  1001. elif o == 'attributes':
  1002. result = subs_attrs(result)
  1003. elif o == 'quotes':
  1004. result = subs_quotes(result)
  1005. elif o == 'specialwords':
  1006. result = config.subs_specialwords(result)
  1007. elif o in ('replacements','replacements2'):
  1008. result = config.subs_replacements(result,o)
  1009. elif o == 'macros':
  1010. result = macros.subs(result)
  1011. elif o == 'callouts':
  1012. result = macros.subs(result,callouts=True)
  1013. else:
  1014. raise EAsciiDoc,'illegal substitution option: %s' % o
  1015. trace(o, s, result)
  1016. if not result:
  1017. break
  1018. return result
  1019. @staticmethod
  1020. def subs(lines,options):
  1021. """Perform inline processing specified by 'options' (in 'options'
  1022. order) on sequence of 'lines'."""
  1023. if not lines or not options:
  1024. return lines
  1025. options = Lex.canonical_subs(options)
  1026. # Join lines so quoting can span multiple lines.
  1027. para = '\n'.join(lines)
  1028. if 'macros' in options:
  1029. para = macros.extract_passthroughs(para)
  1030. for o in options:
  1031. if o == 'attributes':
  1032. # If we don't substitute attributes line-by-line then a single
  1033. # undefined attribute will drop the entire paragraph.
  1034. lines = subs_attrs(para.split('\n'))
  1035. para = '\n'.join(lines)
  1036. else:
  1037. para = Lex.subs_1(para,(o,))
  1038. if 'macros' in options:
  1039. para = macros.restore_passthroughs(para)
  1040. return para.splitlines()
  1041. @staticmethod
  1042. def set_margin(lines, margin=0):
  1043. """Utility routine that sets the left margin to 'margin' space in a
  1044. block of non-blank lines."""
  1045. # Calculate width of block margin.
  1046. lines = list(lines)
  1047. width = len(lines[0])
  1048. for s in lines:
  1049. i = re.search(r'\S',s).start()
  1050. if i < width: width = i
  1051. # Strip margin width from all lines.
  1052. for i in range(len(lines)):
  1053. lines[i] = ' '*margin + lines[i][width:]
  1054. return lines
  1055. #---------------------------------------------------------------------------
  1056. # Document element classes parse AsciiDoc reader input and write DocBook writer
  1057. # output.
  1058. #---------------------------------------------------------------------------
  1059. class Document:
  1060. def __init__(self):
  1061. self.doctype = None # 'article','manpage' or 'book'.
  1062. self.backend = None # -b option argument.
  1063. self.infile = None # Source file name.
  1064. self.outfile = None # Output file name.
  1065. self.attributes = {}
  1066. self.level = 0 # 0 => front matter. 1,2,3 => sect1,2,3.
  1067. self.has_errors = False # Set true if processing errors were flagged.
  1068. self.has_warnings = False # Set true if warnings were flagged.
  1069. self.safe = False # Default safe mode.
  1070. def update_attributes(self):
  1071. # Set implicit attributes.
  1072. if self.infile and os.path.exists(self.infile):
  1073. t = os.path.getmtime(self.infile)
  1074. elif self.infile == '<stdin>':
  1075. t = time.time()
  1076. else:
  1077. t = None
  1078. if t:
  1079. self.attributes['doctime'] = time_str(t)
  1080. self.attributes['docdate'] = date_str(t)
  1081. t = time.time()
  1082. self.attributes['localtime'] = time_str(t)
  1083. self.attributes['localdate'] = date_str(t)
  1084. self.attributes['asciidoc-version'] = VERSION
  1085. self.attributes['backend'] = document.backend
  1086. self.attributes['doctype'] = document.doctype
  1087. self.attributes['backend-'+document.backend] = ''
  1088. self.attributes['doctype-'+document.doctype] = ''
  1089. self.attributes[document.backend+'-'+document.doctype] = ''
  1090. self.attributes['asciidoc-file'] = APP_FILE
  1091. self.attributes['asciidoc-dir'] = APP_DIR
  1092. self.attributes['user-dir'] = USER_DIR
  1093. if self.infile != '<stdin>':
  1094. self.attributes['infile'] = self.infile
  1095. self.attributes['indir'] = os.path.dirname(self.infile)
  1096. self.attributes['docfile'] = self.infile
  1097. self.attributes['docdir'] = os.path.dirname(self.infile)
  1098. self.attributes['docname'] = os.path.splitext(
  1099. os.path.basename(self.infile))[0]
  1100. if config.verbose:
  1101. self.attributes['verbose'] = ''
  1102. # Update with configuration file attributes.
  1103. self.attributes.update(config.conf_attrs)
  1104. # Update with command-line attributes.
  1105. self.attributes.update(config.cmd_attrs)
  1106. # Extract miscellaneous configuration section entries from attributes.
  1107. config.load_miscellaneous(config.conf_attrs)
  1108. config.load_miscellaneous(config.cmd_attrs)
  1109. self.attributes['newline'] = config.newline
  1110. if self.outfile:
  1111. if self.outfile != '<stdout>':
  1112. self.attributes['outfile'] = self.outfile
  1113. self.attributes['outdir'] = os.path.dirname(self.outfile)
  1114. self.attributes['docname'] = os.path.splitext(
  1115. os.path.basename(self.outfile))[0]
  1116. ext = os.path.splitext(self.outfile)[1][1:]
  1117. elif config.outfilesuffix:
  1118. ext = config.outfilesuffix[1:]
  1119. else:
  1120. ext = ''
  1121. if ext:
  1122. self.attributes['filetype'] = ext
  1123. self.attributes['filetype-'+ext] = ''
  1124. def load_lang(self,linenos=False):
  1125. """
  1126. Load language configuration file.
  1127. """
  1128. lang = self.attributes.get('lang')
  1129. message.linenos = linenos
  1130. if lang:
  1131. if not config.load_lang(lang):
  1132. message.error('missing language conf file: lang-%s.conf' % lang)
  1133. self.attributes['lang'] = lang # Reinstate new lang attribute.
  1134. else:
  1135. message.error('language attribute (lang) is not defined')
  1136. message.linenos = None # Restore default line number behavior.
  1137. def set_deprecated_attribute(self,old,new):
  1138. """
  1139. Ensures the 'old' name of an attribute that was renamed to 'new' is
  1140. still honored.
  1141. """
  1142. if self.attributes.get(new) is None:
  1143. if self.attributes.get(old) is not None:
  1144. self.attributes[new] = self.attributes[old]
  1145. else:
  1146. self.attributes[old] = self.attributes[new]
  1147. def translate(self):
  1148. assert self.doctype in ('article','manpage','book'), \
  1149. 'illegal document type'
  1150. assert self.level == 0
  1151. config.expand_all_templates()
  1152. self.load_lang()
  1153. # Skip leading comments and attribute entries.
  1154. finished = False
  1155. attr_count = 0
  1156. while not finished:
  1157. finished = True
  1158. if blocks.isnext() and 'skip' in blocks.current.options:
  1159. finished = False
  1160. blocks.current.translate()
  1161. if macros.isnext() and macros.current.name == 'comment':
  1162. finished = False
  1163. macros.current.translate()
  1164. if AttributeEntry.isnext():
  1165. finished = False
  1166. AttributeEntry.translate()
  1167. if AttributeEntry.name == 'lang':
  1168. self.load_lang(linenos=True)
  1169. if attr_count > 0:
  1170. message.error('lang attribute should be first entry')
  1171. attr_count += 1
  1172. message.verbose('writing: '+writer.fname,False)
  1173. # Process document header.
  1174. has_header = Lex.next() is Title and Title.level == 0
  1175. if self.doctype == 'manpage' and not has_header:
  1176. message.error('manpage document title is mandatory')
  1177. if has_header:
  1178. Header.translate()
  1179. # Command-line entries override header derived entries.
  1180. self.attributes.update(config.cmd_attrs)
  1181. # DEPRECATED: revision renamed to revnumber.
  1182. self.set_deprecated_attribute('revision','revnumber')
  1183. # DEPRECATED: date renamed to revdate.
  1184. self.set_deprecated_attribute('date','revdate')
  1185. if config.header_footer:
  1186. hdr = config.subs_section('header',{})
  1187. writer.write(hdr,trace='header')
  1188. if self.doctype in ('article','book'):
  1189. # Translate 'preamble' (untitled elements between header
  1190. # and first section title).
  1191. if Lex.next() is not Title:
  1192. stag,etag = config.section2tags('preamble')
  1193. writer.write(stag,trace='preamble open')
  1194. Section.translate_body()
  1195. writer.write(etag,trace='preamble close')
  1196. else:
  1197. document.process_author_names()
  1198. if config.header_footer:
  1199. hdr = config.subs_section('header',{})
  1200. writer.write(hdr,trace='header')
  1201. if Lex.next() is not Title:
  1202. Section.translate_body()
  1203. # Process remaining sections.
  1204. while not reader.eof():
  1205. if Lex.next() is not Title:
  1206. raise EAsciiDoc,'section title expected'
  1207. Section.translate()
  1208. Section.setlevel(0) # Write remaining unwritten section close tags.
  1209. # Substitute document parameters and write document footer.
  1210. if config.header_footer:
  1211. ftr = config.subs_section('footer',{})
  1212. writer.write(ftr,trace='footer')
  1213. def parse_author(self,s):
  1214. """ Return False if the author is malformed."""
  1215. attrs = self.attributes # Alias for readability.
  1216. s = s.strip()
  1217. mo = re.match(r'^(?P<name1>[^<>\s]+)'
  1218. '(\s+(?P<name2>[^<>\s]+))?'
  1219. '(\s+(?P<name3>[^<>\s]+))?'
  1220. '(\s+<(?P<email>\S+)>)?$',s)
  1221. if not mo:
  1222. message.error('malformed author: %s' % s)
  1223. return False
  1224. firstname = mo.group('name1')
  1225. if mo.group('name3'):
  1226. middlename = mo.group('name2')
  1227. lastname = mo.group('name3')
  1228. else:
  1229. middlename = None
  1230. lastname = mo.group('name2')
  1231. firstname = firstname.replace('_',' ')
  1232. if middlename:
  1233. middlename = middlename.replace('_',' ')
  1234. if lastname:
  1235. lastname = lastname.replace('_',' ')
  1236. email = mo.group('email')
  1237. if firstname:
  1238. attrs['firstname'] = firstname
  1239. if middlename:
  1240. attrs['middlename'] = middlename
  1241. if lastname:
  1242. attrs['lastname'] = lastname
  1243. if email:
  1244. attrs['email'] = email
  1245. return True
  1246. def process_author_names(self):
  1247. """ Calculate any missing author related attributes."""
  1248. attrs = self.attributes # Alias for readability.
  1249. firstname = attrs.get('firstname','')
  1250. middlename = attrs.get('middlename','')
  1251. lastname = attrs.get('lastname','')
  1252. author = attrs.get('author')
  1253. initials = attrs.get('authorinitials')
  1254. if author and not (firstname or middlename or lastname):
  1255. if not self.parse_author(author):
  1256. return
  1257. attrs['author'] = author.replace('_',' ')
  1258. self.process_author_names()
  1259. return
  1260. if not author:
  1261. author = '%s %s %s' % (firstname, middlename, lastname)
  1262. author = author.strip()
  1263. author

Large files files are truncated, but you can click here to view the full file