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.
 
 
 
 

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