attr_list.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. # markdown is released under the BSD license
  2. # Copyright 2007, 2008 The Python Markdown Project (v. 1.7 and later)
  3. # Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
  4. # Copyright 2004 Manfred Stienstra (the original version)
  5. #
  6. # All rights reserved.
  7. #
  8. # Redistribution and use in source and binary forms, with or without
  9. # modification, are permitted provided that the following conditions are met:
  10. #
  11. # * Redistributions of source code must retain the above copyright
  12. # notice, this list of conditions and the following disclaimer.
  13. # * Redistributions in binary form must reproduce the above copyright
  14. # notice, this list of conditions and the following disclaimer in the
  15. # documentation and/or other materials provided with the distribution.
  16. # * Neither the name of the <organization> nor the
  17. # names of its contributors may be used to endorse or promote products
  18. # derived from this software without specific prior written permission.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE PYTHON MARKDOWN PROJECT ''AS IS'' AND ANY
  21. # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  22. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  23. # DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTORS TO THE PYTHON MARKDOWN PROJECT
  24. # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  25. # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  26. # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  27. # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  28. # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  29. # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  30. # POSSIBILITY OF SUCH DAMAGE.
  31. """
  32. Attribute List Extension for Python-Markdown
  33. ============================================
  34. Adds attribute list syntax. Inspired by
  35. [maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s
  36. feature of the same name.
  37. Copyright 2011 [Waylan Limberg](http://achinghead.com/).
  38. Contact: markdown@freewisdom.org
  39. License: BSD (see ../LICENSE.md for details)
  40. Dependencies:
  41. * [Python 2.4+](http://python.org)
  42. * [Markdown 2.1+](http://packages.python.org/Markdown/)
  43. """
  44. from __future__ import absolute_import
  45. from __future__ import unicode_literals
  46. from . import Extension
  47. from ..treeprocessors import Treeprocessor
  48. from ..util import isBlockLevel
  49. import re
  50. try:
  51. Scanner = re.Scanner
  52. except AttributeError:
  53. # must be on Python 2.4
  54. from sre import Scanner
  55. def _handle_double_quote(s, t):
  56. k, v = t.split('=')
  57. return k, v.strip('"')
  58. def _handle_single_quote(s, t):
  59. k, v = t.split('=')
  60. return k, v.strip("'")
  61. def _handle_key_value(s, t):
  62. return t.split('=')
  63. def _handle_word(s, t):
  64. if t.startswith('.'):
  65. return '.', t[1:]
  66. if t.startswith('#'):
  67. return 'id', t[1:]
  68. return t, t
  69. _scanner = Scanner([
  70. (r'[^ ]+=".*?"', _handle_double_quote),
  71. (r"[^ ]+='.*?'", _handle_single_quote),
  72. (r'[^ ]+=[^ ]*', _handle_key_value),
  73. (r'[^ ]+', _handle_word),
  74. (r' ', None)
  75. ])
  76. def get_attrs(str):
  77. """ Parse attribute list and return a list of attribute tuples. """
  78. return _scanner.scan(str)[0]
  79. def isheader(elem):
  80. return elem.tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
  81. class AttrListTreeprocessor(Treeprocessor):
  82. BASE_RE = r'\{\:?([^\}]*)\}'
  83. HEADER_RE = re.compile(r'[ ]*%s[ ]*$' % BASE_RE)
  84. BLOCK_RE = re.compile(r'\n[ ]*%s[ ]*$' % BASE_RE)
  85. INLINE_RE = re.compile(r'^%s' % BASE_RE)
  86. NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff\u0370-\u037d'
  87. r'\u037f-\u1fff\u200c-\u200d\u2070-\u218f\u2c00-\u2fef'
  88. r'\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd'
  89. r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+')
  90. def run(self, doc):
  91. for elem in doc.getiterator():
  92. if isBlockLevel(elem.tag):
  93. # Block level: check for attrs on last line of text
  94. RE = self.BLOCK_RE
  95. if isheader(elem):
  96. # header: check for attrs at end of line
  97. RE = self.HEADER_RE
  98. if len(elem) and elem[-1].tail:
  99. # has children. Get from tail of last child
  100. m = RE.search(elem[-1].tail)
  101. if m:
  102. self.assign_attrs(elem, m.group(1))
  103. elem[-1].tail = elem[-1].tail[:m.start()]
  104. if isheader(elem):
  105. # clean up trailing #s
  106. elem[-1].tail = elem[-1].tail.rstrip('#').rstrip()
  107. elif elem.text:
  108. # no children. Get from text.
  109. m = RE.search(elem.text)
  110. if m:
  111. self.assign_attrs(elem, m.group(1))
  112. elem.text = elem.text[:m.start()]
  113. if isheader(elem):
  114. # clean up trailing #s
  115. elem.text = elem.text.rstrip('#').rstrip()
  116. else:
  117. # inline: check for attrs at start of tail
  118. if elem.tail:
  119. m = self.INLINE_RE.match(elem.tail)
  120. if m:
  121. self.assign_attrs(elem, m.group(1))
  122. elem.tail = elem.tail[m.end():]
  123. def assign_attrs(self, elem, attrs):
  124. """ Assign attrs to element. """
  125. for k, v in get_attrs(attrs):
  126. if k == '.':
  127. # add to class
  128. cls = elem.get('class')
  129. if cls:
  130. elem.set('class', '%s %s' % (cls, v))
  131. else:
  132. elem.set('class', v)
  133. else:
  134. # assign attr k with v
  135. elem.set(self.sanitize_name(k), v)
  136. def sanitize_name(self, name):
  137. """
  138. Sanitize name as 'an XML Name, minus the ":"'.
  139. See http://www.w3.org/TR/REC-xml-names/#NT-NCName
  140. """
  141. return self.NAME_RE.sub('_', name)
  142. class AttrListExtension(Extension):
  143. def extendMarkdown(self, md, md_globals):
  144. md.treeprocessors.add('attr_list', AttrListTreeprocessor(md), '>prettify')
  145. def makeExtension(configs={}):
  146. return AttrListExtension(configs=configs)