@@ -1,5 +1,9 @@ | |||||
v0.5 (unreleased): | 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 | - Made Template.remove(keep_field=True) behave more reasonably when the | ||||
parameter is already empty. | parameter is already empty. | ||||
- Added the keep_template_params argument to Wikicode.strip_code(). If True, | - Added the keep_template_params argument to Wikicode.strip_code(). If True, | ||||
@@ -7,6 +7,11 @@ v0.5 | |||||
Unreleased | Unreleased | ||||
(`changes <https://github.com/earwig/mwparserfromhell/compare/v0.4.4...develop>`__): | (`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 | - Made :meth:`Template.remove(keep_field=True) <.Template.remove>` behave more | ||||
reasonably when the parameter is already empty. | reasonably when the parameter is already empty. | ||||
- Added the *keep_template_params* argument to :meth:`.Wikicode.strip_code`. | - Added the *keep_template_params* argument to :meth:`.Wikicode.strip_code`. | ||||
@@ -275,6 +275,21 @@ class Wikicode(StringMixIn): | |||||
else: | else: | ||||
self.nodes.pop(index) | 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): | def index(self, obj, recursive=False): | ||||
"""Return the index of *obj* in the list of nodes. | """Return the index of *obj* in the list of nodes. | ||||
@@ -294,6 +309,52 @@ class Wikicode(StringMixIn): | |||||
return i | return i | ||||
raise ValueError(obj) | 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): | def insert(self, index, value): | ||||
"""Insert *value* at *index* in the list of nodes. | """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, 3, "{{baz}}") | ||||
self.assertRaises(IndexError, code.set, -4, "{{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): | def test_index(self): | ||||
"""test Wikicode.index()""" | """test Wikicode.index()""" | ||||
code = parse("Have a {{template}} and a [[page|link]]") | code = parse("Have a {{template}} and a [[page|link]]") | ||||
@@ -102,6 +113,22 @@ class TestWikicode(TreeEqualityTestCase): | |||||
self.assertRaises(ValueError, code.index, | self.assertRaises(ValueError, code.index, | ||||
code.get(1).get(1).value, recursive=False) | 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): | def test_insert(self): | ||||
"""test Wikicode.insert()""" | """test Wikicode.insert()""" | ||||
code = parse("Have a {{template}} and a [[page|link]]") | code = parse("Have a {{template}} and a [[page|link]]") | ||||