A Python parser for MediaWiki wikicode https://mwparserfromhell.readthedocs.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

339 lines
13 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2012 Ben Kurtovic <ben.kurtovic@verizon.net>
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a copy
  6. # of this software and associated documentation files (the "Software"), to deal
  7. # in the Software without restriction, including without limitation the rights
  8. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. # copies of the Software, and to permit persons to whom the Software is
  10. # furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in
  13. # all copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. # SOFTWARE.
  22. from __future__ import unicode_literals
  23. import re
  24. from .compat import maxsize, str
  25. from .nodes import Heading, Node, Tag, Template, Text
  26. from .string_mixin import StringMixIn
  27. from .utils import parse_anything
  28. __all__ = ["Wikicode"]
  29. FLAGS = re.IGNORECASE | re.DOTALL | re.UNICODE
  30. class Wikicode(StringMixIn):
  31. """A ``Wikicode`` is a container for nodes that functions like a string.
  32. """
  33. def __init__(self, nodes):
  34. super(Wikicode, self).__init__()
  35. self._nodes = nodes
  36. def __unicode__(self):
  37. return "".join([str(node) for node in self.nodes])
  38. def _get_children(self, node):
  39. """Iterate over all descendants of a given *node*, including itself.
  40. This is implemented by the ``__iternodes__()`` generator of ``Node``
  41. classes, which by default yields itself and nothing more.
  42. """
  43. for context, child in node.__iternodes__(self._get_all_nodes):
  44. yield child
  45. def _get_context(self, node, obj):
  46. """Return a ``Wikicode`` that contains *obj* in its descendants.
  47. The closest (shortest distance from *node*) suitable ``Wikicode`` will
  48. be returned, or ``None`` if the *obj* is the *node* itself.
  49. Raises ``ValueError`` if *obj* is not within *node*.
  50. """
  51. for context, child in node.__iternodes__(self._get_all_nodes):
  52. if child is obj:
  53. return context
  54. raise ValueError(obj)
  55. def _get_all_nodes(self, code):
  56. """Iterate over all of our descendant nodes.
  57. This is implemented by calling :py:meth:`_get_children` on every node
  58. in our node list (:py:attr:`self.nodes <nodes>`).
  59. """
  60. for node in code.nodes:
  61. for child in self._get_children(node):
  62. yield child
  63. def _is_equivalent(self, obj, node):
  64. """Return ``True`` if *obj* and *node* are equivalent, else ``False``.
  65. If *obj* is a ``Node``, the function will test whether they are the
  66. same object, otherwise it will compare them with ``==``.
  67. """
  68. if isinstance(obj, Node):
  69. if node is obj:
  70. return True
  71. else:
  72. if node == obj:
  73. return True
  74. return False
  75. def _contains(self, nodes, obj):
  76. """Return ``True`` if *obj* is inside of *nodes*, else ``False``.
  77. If *obj* is a ``Node``, we will only return ``True`` if *obj* is
  78. actually in the list (and not just a node that equals it). Otherwise,
  79. the test is simply ``obj in nodes``.
  80. """
  81. if isinstance(obj, Node):
  82. for node in nodes:
  83. if node is obj:
  84. return True
  85. return False
  86. return obj in nodes
  87. def _do_search(self, obj, recursive, callback, context, *args, **kwargs):
  88. """Look within *context* for *obj*, executing *callback* if found.
  89. If *recursive* is ``True``, we'll look within context and its
  90. descendants, otherwise we'll just execute callback. We raise
  91. :py:exc:`ValueError` if *obj* isn't in our node list or context. If
  92. found, *callback* is passed the context, the index of the node within
  93. the context, and whatever were passed as ``*args`` and ``**kwargs``.
  94. """
  95. if recursive:
  96. for i, node in enumerate(context.nodes):
  97. if self._is_equivalent(obj, node):
  98. return callback(context, i, *args, **kwargs)
  99. if self._contains(self._get_children(node), obj):
  100. context = self._get_context(node, obj)
  101. return self._do_search(obj, recursive, callback, context,
  102. *args, **kwargs)
  103. raise ValueError(obj)
  104. callback(context, self.index(obj, recursive=False), *args, **kwargs)
  105. def _get_tree(self, code, lines, marker, indent):
  106. """Build a tree to illustrate the way the Wikicode object was parsed.
  107. The method that builds the actual tree is ``__showtree__`` of ``Node``
  108. objects. *code* is the ``Wikicode`` object to build a tree for. *lines*
  109. is the list to append the tree to, which is returned at the end of the
  110. method. *marker* is some object to be used to indicate that the builder
  111. should continue on from the last line instead of starting a new one; it
  112. should be any object that can be tested for with ``is``. *indent* is
  113. the starting indentation.
  114. """
  115. def write(*args):
  116. if lines and lines[-1] is marker: # Continue from the last line
  117. lines.pop() # Remove the marker
  118. last = lines.pop()
  119. lines.append(last + " ".join(args))
  120. else:
  121. lines.append(" " * 6 * indent + " ".join(args))
  122. get = lambda code: self._get_tree(code, lines, marker, indent + 1)
  123. mark = lambda: lines.append(marker)
  124. for node in code.nodes:
  125. node.__showtree__(write, get, mark)
  126. return lines
  127. @property
  128. def nodes(self):
  129. """A list of :py:class:`~mwparserfromhell.nodes.Node` objects.
  130. This is the internal data actually stored within a
  131. :py:class:`~mwparserfromhell.wikicode.Wikicode` object.
  132. """
  133. return self._nodes
  134. @nodes.setter
  135. def nodes(self, value):
  136. self._nodes = value
  137. def get(self, index):
  138. """Return the *index*\ th node within the list of nodes."""
  139. return self.nodes[index]
  140. def set(self, index, value):
  141. """Set the ``Node`` at *index* to *value*.
  142. Raises :py:exc:`IndexError` if *index* is out of range, or
  143. :py:exc:`ValueError` if *value* cannot be coerced into one
  144. :py:class:`~mwparserfromhell.nodes.Node`. To insert multiple nodes at
  145. an index, use :py:meth:`get` with either :py:meth:`remove` and
  146. :py:meth:`insert` or :py:meth:`replace`.
  147. """
  148. nodes = parse_anything(value).nodes
  149. if len(nodes) > 1:
  150. raise ValueError("Cannot coerce multiple nodes into one index")
  151. if index >= len(self.nodes) or -1 * index > len(self.nodes):
  152. raise IndexError("List assignment index out of range")
  153. self.nodes.pop(index)
  154. if nodes:
  155. self.nodes[index] = nodes[0]
  156. def index(self, obj, recursive=False):
  157. """Return the index of *obj* in the list of nodes.
  158. Raises :py:exc:`ValueError` if *obj* is not found. If *recursive* is
  159. ``True``, we will look in all nodes of ours and their descendants, and
  160. return the index of our direct descendant node within *our* list of
  161. nodes. Otherwise, the lookup is done only on direct descendants.
  162. """
  163. if recursive:
  164. for i, node in enumerate(self.nodes):
  165. if self._contains(self._get_children(node), obj):
  166. return i
  167. raise ValueError(obj)
  168. for i, node in enumerate(self.nodes):
  169. if self._is_equivalent(obj, node):
  170. return i
  171. raise ValueError(obj)
  172. def insert(self, index, value):
  173. """Insert *value* at *index* in the list of nodes.
  174. *value* can be anything parasable by
  175. :py:func:`mwparserfromhell.utils.parse_anything`, which includes
  176. strings or other :py:class:`~mwparserfromhell.wikicode.Wikicode` or
  177. :py:class:`~mwparserfromhell.nodes.Node` objects.
  178. """
  179. nodes = parse_anything(value).nodes
  180. for node in reversed(nodes):
  181. self.nodes.insert(index, node)
  182. def insert_before(self, obj, value, recursive=True):
  183. """Insert *value* immediately before *obj* in the list of nodes.
  184. *value* can be anything parasable by
  185. :py:func:`mwparserfromhell.utils.parse_anything`. If *recursive* is
  186. ``True``, we will try to find *obj* within our child nodes even if it
  187. is not a direct descendant of this
  188. :py:class:`~mwparserfromhell.wikicode.Wikicode` object. If *obj* is not
  189. in the node list, :py:exc:`ValueError` is raised.
  190. """
  191. callback = lambda self, i, value: self.insert(i, value)
  192. self._do_search(obj, recursive, callback, self, value)
  193. def insert_after(self, obj, value, recursive=True):
  194. """Insert *value* immediately after *obj* in the list of nodes.
  195. *value* can be anything parasable by
  196. :py:func:`mwparserfromhell.utils.parse_anything`. If *recursive* is
  197. ``True``, we will try to find *obj* within our child nodes even if it
  198. is not a direct descendant of this
  199. :py:class:`~mwparserfromhell.wikicode.Wikicode` object. If *obj* is not
  200. in the node list, :py:exc:`ValueError` is raised.
  201. """
  202. callback = lambda self, i, value: self.insert(i + 1, value)
  203. self._do_search(obj, recursive, callback, self, value)
  204. def replace(self, obj, value, recursive=True):
  205. def callback(self, i, value):
  206. self.nodes.pop(i)
  207. self.insert(i, value)
  208. self._do_search(obj, recursive, callback, self, value)
  209. def append(self, value):
  210. nodes = parse_anything(value).nodes
  211. for node in nodes:
  212. self.nodes.append(node)
  213. def remove(self, obj, recursive=True):
  214. callback = lambda self, i: self.nodes.pop(i)
  215. self._do_search(obj, recursive, callback, self)
  216. def ifilter(self, recursive=False, matches=None, flags=FLAGS,
  217. forcetype=None):
  218. if recursive:
  219. nodes = self._get_all_nodes(self)
  220. else:
  221. nodes = self.nodes
  222. for node in nodes:
  223. if not forcetype or isinstance(node, forcetype):
  224. if not matches or re.search(matches, str(node), flags):
  225. yield node
  226. def ifilter_templates(self, recursive=False, matches=None, flags=FLAGS):
  227. return self.filter(recursive, matches, flags, forcetype=Template)
  228. def ifilter_text(self, recursive=False, matches=None, flags=FLAGS):
  229. return self.filter(recursive, matches, flags, forcetype=Text)
  230. def ifilter_tags(self, recursive=False, matches=None, flags=FLAGS):
  231. return self.ifilter(recursive, matches, flags, forcetype=Tag)
  232. def filter(self, recursive=False, matches=None, flags=FLAGS,
  233. forcetype=None):
  234. return list(self.ifilter(recursive, matches, flags, forcetype))
  235. def filter_templates(self, recursive=False, matches=None, flags=FLAGS):
  236. return list(self.ifilter_templates(recursive, matches, flags))
  237. def filter_text(self, recursive=False, matches=None, flags=FLAGS):
  238. return list(self.ifilter_text(recursive, matches, flags))
  239. def filter_tags(self, recursive=False, matches=None, flags=FLAGS):
  240. return list(self.ifilter_tags(recursive, matches, flags))
  241. def get_sections(self, flat=True, matches=None, levels=None, flags=FLAGS,
  242. include_headings=True):
  243. if matches:
  244. matches = r"^(=+?)\s*" + matches + r"\s*\1$"
  245. headings = self.filter(recursive=True, matches=matches, flags=flags,
  246. forcetype=Heading)
  247. if levels:
  248. headings = [head for head in headings if head.level in levels]
  249. sections = []
  250. buffers = [[maxsize, 0]]
  251. i = 0
  252. while i < len(self.nodes):
  253. if self.nodes[i] in headings:
  254. this = self.nodes[i].level
  255. for (level, start) in buffers:
  256. if not flat or this <= level:
  257. buffers.remove([level, start])
  258. sections.append(self.nodes[start:i])
  259. buffers.append([this, i])
  260. if not include_headings:
  261. i += 1
  262. i += 1
  263. for (level, start) in buffers:
  264. if start != i:
  265. sections.append(self.nodes[start:i])
  266. return sections
  267. def strip_code(self, normalize=True, collapse=True):
  268. nodes = []
  269. for node in self.nodes:
  270. stripped = node.__strip__(normalize, collapse)
  271. if stripped:
  272. nodes.append(str(stripped))
  273. if collapse:
  274. stripped = "".join(nodes).strip("\n")
  275. while "\n\n\n" in stripped:
  276. stripped = stripped.replace("\n\n\n", "\n\n")
  277. return stripped
  278. else:
  279. return "".join(nodes)
  280. def get_tree(self):
  281. marker = object() # Random object we can find with certainty in a list
  282. return "\n".join(self._get_tree(self, [], marker, 0))