From 8ecccb8b241cf8d20f1952dce7eadc0161c8bfbb Mon Sep 17 00:00:00 2001 From: Lyz Date: Sun, 26 Jan 2025 12:03:47 +0100 Subject: [PATCH] feat: add refile heading --- org_rw/org_rw.py | 62 ++++++++++++++++- tests/test_org.py | 165 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 1 deletion(-) diff --git a/org_rw/org_rw.py b/org_rw/org_rw.py index 31b904c..558e7a2 100644 --- a/org_rw/org_rw.py +++ b/org_rw/org_rw.py @@ -329,7 +329,7 @@ class Headline: closed: Optional[Time] = None, ): self.start_line = start_line - self.depth = depth + self._depth = depth self.orig = orig self.properties = properties self.keywords = keywords @@ -762,6 +762,16 @@ class Headline: pass self._state = new_state + @property + def depth(self): + return self._depth + + @depth.setter + def depth(self, value): + self._depth = value + for child in self.children: + child.depth = value + 1 + @property def clock(self): times = [] @@ -1066,6 +1076,38 @@ class Headline: self.children.append(headline) return headline + def refile( + self, destination: Union["Headline", OrgDoc], top: bool = False + ) -> Union["Headline", OrgDoc]: + """Refile this headline to a new destination. + + Args: + destination: The headline to which this headline will be moved + top: Whether to append to bottom or insert at top of destination's children + + Returns: + The destination headline + """ + # Remove from the parent + if self.parent: + if isinstance(self.parent, Headline): + self.parent.children.remove(self) + else: + self.parent.headlines.remove(self) + + # Add ourselves to the destination + if top: + destination.children.insert(0, self) + else: + destination.children.append(self) + + # Adjust the depth + self.depth = destination.depth + 1 + + # Adjust our parent + self.parent = destination + return destination + RawLine = collections.namedtuple("RawLine", ("linenum", "line")) Keyword = collections.namedtuple( @@ -2366,6 +2408,24 @@ class OrgDoc: def shallow_tags(self) -> list[str]: return self.tags + @property + def depth(self): + """ + Attribute to be compatible with the signature of the Headlines. + + Useful when doing operations across the headline hierarchy + """ + return 0 + + @property + def children(self): + """ + Attribute to be compatible with the signature of the Headlines. + + Useful when doing operations across the headline hierarchy + """ + return self.headlines + ## Querying def get_links(self): for headline in self.headlines: diff --git a/tests/test_org.py b/tests/test_org.py index a1fdff1..59441d5 100644 --- a/tests/test_org.py +++ b/tests/test_org.py @@ -1024,6 +1024,171 @@ class TestSerde(unittest.TestCase): self.assertEqual(dumps(doc), "* TODO First entry") + def test_refile_headline_down_to_bottom(self) -> None: + orig = """* Source Headline +** Child of Source +* Destination Headline +** Existing Child""" + doc = loads(orig) + + source_headline = doc.headlines[0] + destination_headline = doc.headlines[1] + + result = source_headline.refile(destination_headline) + + assert result == destination_headline + assert source_headline.parent == destination_headline + assert source_headline in destination_headline.children + assert destination_headline.children[-1] == source_headline + assert ( + dumps(doc) + == """* Destination Headline +** Existing Child +** Source Headline +*** Child of Source""" + ) + + def test_refile_headline_down_to_top(self) -> None: + orig = """* Source Headline +** Child of Source +* Destination Headline +** Existing Child""" + doc = loads(orig) + + source_headline = doc.headlines[0] + destination_headline = doc.headlines[1] + + result = source_headline.refile(destination_headline, top=True) + + assert result == destination_headline + assert source_headline.parent == destination_headline + assert source_headline in destination_headline.children + assert destination_headline.children[0] == source_headline + assert ( + dumps(doc) + == """* Destination Headline +** Source Headline +*** Child of Source +** Existing Child""" + ) + + def test_refile_headline_down_to_existing_child(self) -> None: + orig = """* Source Headline +** Child of Source +* Destination Parent +** Destination Headline""" + doc = loads(orig) + + source_headline = doc.headlines[0] + destination_headline = doc.headlines[1] + destination_child = destination_headline.children[0] + + result = source_headline.refile(destination_child) + + assert result == destination_child + assert source_headline.parent == destination_child + assert source_headline in destination_child.children + assert destination_child.children[-1] == source_headline + assert ( + dumps(doc) + == """* Destination Parent +** Destination Headline +*** Source Headline +**** Child of Source""" + ) + + def test_refile_headline_from_child_to_parent_bottom(self) -> None: + orig = """* Destination Headline +** Existing Child +*** Source Headline +**** Source Child""" + doc = loads(orig) + source_headline = doc.headlines[0].children[0].children[0] + destination_headline = doc.headlines[0] + + result = source_headline.refile(destination_headline) + + assert result == destination_headline + assert source_headline.parent == destination_headline + assert source_headline in destination_headline.children + assert destination_headline.children[-1] == source_headline + assert ( + dumps(doc) + == """* Destination Headline +** Existing Child +** Source Headline +*** Source Child""" + ) + + def test_refile_headline_from_child_to_parent_top(self) -> None: + orig = """* Destination Headline +** Existing Child +*** Source Headline +**** Source Child""" + doc = loads(orig) + source_headline = doc.headlines[0].children[0].children[0] + destination_headline = doc.headlines[0] + + result = source_headline.refile(destination_headline, top=True) + + assert result == destination_headline + assert source_headline.parent == destination_headline + assert source_headline in destination_headline.children + assert destination_headline.children[0] == source_headline + assert ( + dumps(doc) + == """* Destination Headline +** Source Headline +*** Source Child +** Existing Child""" + ) + + def test_refile_headline_from_child_to_first_level_at_bottom(self) -> None: + orig = """* Destination Headline +** Existing Child +*** Source Headline +**** Source Child""" + doc = loads(orig) + source_headline = doc.headlines[0].children[0].children[0] + destination_headline = doc.headlines[0].parent + + result = source_headline.refile(destination_headline) + + assert result == destination_headline + assert source_headline.parent == destination_headline + assert source_headline in destination_headline.children + assert destination_headline.children[-1] == source_headline + assert ( + dumps(doc) + == """* Destination Headline +** Existing Child +* Source Headline +** Source Child""" + ) + + def test_refile_headline_from_child_to_first_level_at_top(self) -> None: + orig = """* Destination Headline +** Existing Child +*** Source Headline +**** Source Child""" + doc = loads(orig) + source_headline = doc.headlines[0].children[0].children[0] + destination_headline = doc.headlines[0].parent + + result = source_headline.refile(destination_headline, top=True) + + assert result == destination_headline + assert source_headline.parent == destination_headline + assert source_headline in destination_headline.children + assert destination_headline.children[0] == source_headline + assert ( + dumps(doc) + == """* Source Headline +** Source Child +* Destination Headline +** Existing Child""" + ) + def print_tree(tree, indentation=0, headline=None): for element in tree: