@@ -1,5 +1,9 @@ | |||
v0.5 (unreleased): | |||
- Added Wikicode.contains() to determine whether a Node or Wikicode object is | |||
contained within another Wikicode object. | |||
- Added Wikicode.get_ancestors() and Wikicode.get_parent() to find all | |||
ancestors and the direct parent of a Node, respectively. | |||
- Made Template.remove(keep_field=True) behave more reasonably when the | |||
parameter is already empty. | |||
- Added the keep_template_params argument to Wikicode.strip_code(). If True, | |||
@@ -7,6 +7,11 @@ v0.5 | |||
Unreleased | |||
(`changes <https://github.com/earwig/mwparserfromhell/compare/v0.4.4...develop>`__): | |||
- Added :meth:`.Wikicode.contains` to determine whether a :class:`.Node` or | |||
:class:`.Wikicode` object is contained within another :class:`.Wikicode` | |||
object. | |||
- Added :meth:`.Wikicode.get_ancestors` and :meth:`.Wikicode.get_parent` to | |||
find all ancestors and the direct parent of a :class:`.Node`, respectively. | |||
- Made :meth:`Template.remove(keep_field=True) <.Template.remove>` behave more | |||
reasonably when the parameter is already empty. | |||
- Added the *keep_template_params* argument to :meth:`.Wikicode.strip_code`. | |||
@@ -275,6 +275,21 @@ class Wikicode(StringMixIn): | |||
else: | |||
self.nodes.pop(index) | |||
def contains(self, obj): | |||
"""Return whether this Wikicode object contains *obj*. | |||
If *obj* is a :class:`.Node` or :class:`.Wikicode` object, then we | |||
search for it exactly among all of our children, recursively. | |||
Otherwise, this method just uses :meth:`.__contains__` on the string. | |||
""" | |||
if not isinstance(obj, (Node, Wikicode)): | |||
return obj in self | |||
try: | |||
self._do_strong_search(obj, recursive=True) | |||
except ValueError: | |||
return False | |||
return True | |||
def index(self, obj, recursive=False): | |||
"""Return the index of *obj* in the list of nodes. | |||
@@ -294,6 +309,52 @@ class Wikicode(StringMixIn): | |||
return i | |||
raise ValueError(obj) | |||
def get_ancestors(self, obj): | |||
"""Return a list of all ancestor nodes of the :class:`.Node` *obj*. | |||
The list is ordered from the most shallow ancestor (greatest great- | |||
grandparent) to the direct parent. The node itself is not included in | |||
the list. For example:: | |||
>>> text = "{{a|{{b|{{c|{{d}}}}}}}}" | |||
>>> code = mwparserfromhell.parse(text) | |||
>>> node = code.filter_templates(matches=lambda n: n == "{{d}}")[0] | |||
>>> code.get_ancestors(node) | |||
['{{a|{{b|{{c|{{d}}}}}}}}', '{{b|{{c|{{d}}}}}}', '{{c|{{d}}}}'] | |||
Will return an empty list if *obj* is at the top level of this Wikicode | |||
object. Will raise :exc:`ValueError` if it wasn't found. | |||
""" | |||
def _get_ancestors(code, needle): | |||
for node in code.nodes: | |||
if node is needle: | |||
return [] | |||
for code in node.__children__(): | |||
ancestors = _get_ancestors(code, needle) | |||
if ancestors is not None: | |||
return [node] + ancestors | |||
if isinstance(obj, Wikicode): | |||
obj = obj.get(0) | |||
elif not isinstance(obj, Node): | |||
raise ValueError(obj) | |||
ancestors = _get_ancestors(self, obj) | |||
if ancestors is None: | |||
raise ValueError(obj) | |||
return ancestors | |||
def get_parent(self, obj): | |||
"""Return the direct parent node of the :class:`.Node` *obj*. | |||
This function is equivalent to calling :meth:`.get_ancestors` and | |||
taking the last element of the resulting list. Will return None if | |||
the node exists but does not have a parent; i.e., it is at the top | |||
level of the Wikicode object. | |||
""" | |||
ancestors = self.get_ancestors(obj) | |||
return ancestors[-1] if ancestors else None | |||
def insert(self, index, value): | |||
"""Insert *value* at *index* in the list of nodes. | |||
@@ -85,6 +85,17 @@ class TestWikicode(TreeEqualityTestCase): | |||
self.assertRaises(IndexError, code.set, 3, "{{baz}}") | |||
self.assertRaises(IndexError, code.set, -4, "{{baz}}") | |||
def test_contains(self): | |||
"""test Wikicode.contains()""" | |||
code = parse("Here is {{aaa|{{bbb|xyz{{ccc}}}}}} and a [[page|link]]") | |||
tmpl1, tmpl2, tmpl3 = code.filter_templates() | |||
tmpl4 = parse("{{ccc}}").filter_templates()[0] | |||
self.assertTrue(code.contains(tmpl1)) | |||
self.assertTrue(code.contains(tmpl3)) | |||
self.assertFalse(code.contains(tmpl4)) | |||
self.assertTrue(code.contains(str(tmpl4))) | |||
self.assertTrue(code.contains(tmpl2.params[0].value)) | |||
def test_index(self): | |||
"""test Wikicode.index()""" | |||
code = parse("Have a {{template}} and a [[page|link]]") | |||
@@ -102,6 +113,22 @@ class TestWikicode(TreeEqualityTestCase): | |||
self.assertRaises(ValueError, code.index, | |||
code.get(1).get(1).value, recursive=False) | |||
def test_get_ancestors_parent(self): | |||
"""test Wikicode.get_ancestors() and Wikicode.get_parent()""" | |||
code = parse("{{a|{{b|{{d|{{e}}{{f}}}}{{g}}}}}}{{c}}") | |||
tmpl = code.filter_templates(matches=lambda n: n.name == "f")[0] | |||
parent1 = code.filter_templates(matches=lambda n: n.name == "d")[0] | |||
parent2 = code.filter_templates(matches=lambda n: n.name == "b")[0] | |||
parent3 = code.filter_templates(matches=lambda n: n.name == "a")[0] | |||
fake = parse("{{f}}").get(0) | |||
self.assertEqual([parent3, parent2, parent1], code.get_ancestors(tmpl)) | |||
self.assertIs(parent1, code.get_parent(tmpl)) | |||
self.assertEqual([], code.get_ancestors(parent3)) | |||
self.assertIs(None, code.get_parent(parent3)) | |||
self.assertRaises(ValueError, code.get_ancestors, fake) | |||
self.assertRaises(ValueError, code.get_parent, fake) | |||
def test_insert(self): | |||
"""test Wikicode.insert()""" | |||
code = parse("Have a {{template}} and a [[page|link]]") | |||