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.
 
 
 
 

231 lines
8.5 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. import re
  23. from mwparserfromhell.nodes import HTMLEntity, Node, Template, Text
  24. from mwparserfromhell.string_mixin import StringMixIn
  25. from mwparserfromhell.utils import parse_anything
  26. __all__ = ["Wikicode"]
  27. FLAGS = re.IGNORECASE | re.DOTALL | re.UNICODE
  28. class Wikicode(StringMixIn):
  29. def __init__(self, nodes):
  30. self._nodes = nodes
  31. def __unicode__(self):
  32. return "".join([unicode(node) for node in self.nodes])
  33. def _iterate_over_children(self, node):
  34. yield (None, node)
  35. if isinstance(node, Template):
  36. for child in self._get_all_nodes(node.name):
  37. yield (node.name, child)
  38. for param in node.params:
  39. if param.showkey:
  40. for child in self._get_all_nodes(param.name):
  41. yield (param.name, child)
  42. for child in self._get_all_nodes(param.value):
  43. yield (param.value, child)
  44. def _get_children(self, node):
  45. for context, child in self._iterate_over_children(node):
  46. yield child
  47. def _get_context(self, node, obj):
  48. for context, child in self._iterate_over_children(node):
  49. if child is obj:
  50. return context
  51. raise ValueError(obj)
  52. def _get_all_nodes(self, code):
  53. for node in code.nodes:
  54. for child in self._get_children(node):
  55. yield child
  56. def _is_equivalent(self, obj, node):
  57. if isinstance(obj, Node):
  58. if node is obj:
  59. return True
  60. else:
  61. if node == obj:
  62. return True
  63. return False
  64. def _contains(self, nodes, obj):
  65. if isinstance(obj, Node):
  66. for node in nodes:
  67. if node is obj:
  68. return True
  69. else:
  70. if obj in nodes:
  71. return True
  72. return False
  73. def _do_search(self, obj, recursive, callback, context, *args, **kwargs):
  74. if recursive:
  75. for i, node in enumerate(context.nodes):
  76. if self._is_equivalent(obj, node):
  77. return callback(context, i, *args, **kwargs)
  78. if self._contains(self._get_children(node), obj):
  79. context = self._get_context(node, obj)
  80. return self._do_search(obj, recursive, callback, context,
  81. *args, **kwargs)
  82. raise ValueError(obj)
  83. callback(context, self.index(obj, recursive=False), *args, **kwargs)
  84. def _get_tree(self, code, lines, marker=None, indent=0):
  85. def write(*args):
  86. if lines and lines[-1] is marker: # Continue from the last line
  87. lines.pop() # Remove the marker
  88. last = lines.pop()
  89. lines.append(last + " ".join(args))
  90. else:
  91. lines.append(" " * indent + " ".join(args))
  92. for node in code.nodes:
  93. if isinstance(node, Template):
  94. write("{{", )
  95. self._get_tree(node.name, lines, marker, indent + 1)
  96. for param in node.params:
  97. write(" | ")
  98. lines.append(marker) # Continue from this line
  99. self._get_tree(param.name, lines, marker, indent + 1)
  100. write(" = ")
  101. lines.append(marker) # Continue from this line
  102. self._get_tree(param.value, lines, marker, indent + 1)
  103. write("}}")
  104. else:
  105. write(unicode(node))
  106. return lines
  107. @property
  108. def nodes(self):
  109. return self._nodes
  110. def get(self, index):
  111. return self.nodes[index]
  112. def set(self, index, value):
  113. nodes = parse_anything(value).nodes
  114. if len(nodes) > 1:
  115. raise ValueError("Cannot coerce multiple nodes into one index")
  116. if index >= len(self.nodes) or -1 * index > len(self.nodes):
  117. raise IndexError("List assignment index out of range")
  118. self.nodes.pop(index)
  119. if nodes:
  120. self.nodes[index] = nodes[0]
  121. def index(self, obj, recursive=False):
  122. if recursive:
  123. for i, node in enumerate(self.nodes):
  124. if self._contains(self._get_children(node), obj):
  125. return i
  126. raise ValueError(obj)
  127. for i, node in enumerate(self.nodes):
  128. if self._is_equivalent(obj, node):
  129. return i
  130. raise ValueError(obj)
  131. def insert(self, index, value):
  132. nodes = parse_anything(value).nodes
  133. for node in reversed(nodes):
  134. self.nodes.insert(index, node)
  135. def insert_before(self, obj, value, recursive=True):
  136. callback = lambda self, i, value: self.insert(i, value)
  137. self._do_search(obj, recursive, callback, self, value)
  138. def insert_after(self, obj, value, recursive=True):
  139. callback = lambda self, i, value: self.insert(i + 1, value)
  140. self._do_search(obj, recursive, callback, self, value)
  141. def replace(self, obj, value, recursive=True):
  142. def callback(self, i, value):
  143. self.nodes.pop(i)
  144. self.insert(i, value)
  145. self._do_search(obj, recursive, callback, self, value)
  146. def append(self, value):
  147. nodes = parse_anything(value).nodes
  148. for node in nodes:
  149. self.nodes.append(node)
  150. def remove(self, obj, recursive=True):
  151. callback = lambda self, i: self.nodes.pop(i)
  152. self._do_search(obj, recursive, callback, self)
  153. def ifilter(self, recursive=False, matches=None, flags=FLAGS,
  154. forcetype=None):
  155. if recursive:
  156. nodes = self._get_all_nodes(self)
  157. else:
  158. nodes = self.nodes
  159. for node in nodes:
  160. if not forcetype or isinstance(node, forcetype):
  161. if not matches or re.search(matches, unicode(node), flags):
  162. yield node
  163. def ifilter_templates(self, recursive=False, matches=None, flags=FLAGS):
  164. return self.filter(recursive, matches, flags, forcetype=Template)
  165. def ifilter_text(self, recursive=False, matches=None, flags=FLAGS):
  166. return self.filter(recursive, matches, flags, forcetype=Text)
  167. def filter(self, recursive=False, matches=None, flags=FLAGS,
  168. forcetype=None):
  169. return list(self.ifilter(recursive, matches, flags, forcetype))
  170. def filter_templates(self, recursive=False, matches=None, flags=FLAGS):
  171. return list(self.ifilter_templates(recursive, matches, flags))
  172. def filter_text(self, recursive=False, matches=None, flags=FLAGS):
  173. return list(self.ifilter_text(recursive, matches, flags))
  174. def strip_code(self, normalize=True, collapse=True):
  175. nodes = []
  176. for node in self.nodes:
  177. if isinstance(node, Text):
  178. nodes.append(node)
  179. elif isinstance(node, HTMLEntity):
  180. if normalize:
  181. nodes.append(node.normalize())
  182. else:
  183. nodes.append(node)
  184. if collapse:
  185. stripped = u"".join(nodes).strip("\n")
  186. while "\n\n\n" in stripped:
  187. stripped = stripped.replace("\n\n\n", "\n\n")
  188. return stripped
  189. else:
  190. return u"".join(nodes)
  191. def get_tree(self):
  192. marker = object() # Random object we can find with certainty in a list
  193. return "\n".join(self._get_tree(self, [], marker))