diff --git a/tests/test_parser.py b/tests/test_parser.py index e78c6d3..22abf71 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -10,7 +10,15 @@ from __future__ import annotations from itertools import chain from typing import Iterable, Sequence -from docutils.nodes import Element, Node, Text, container, emphasis, strong +from docutils.nodes import ( + Element, + Node, + Text, + container, + emphasis, + literal, + strong, +) from docutils.utils import new_document import pytest @@ -162,6 +170,18 @@ def compare_nodes( "the message is: [rot=13]uryyb[/rot] - the - [rot13]jbeyq", [Text("the message is: hello - the - world")], ), + ( + "a`[code]`b", + [ + Text("a"), + literal("", Text("[code]"), **{"class": "inline-code"}), + Text("b"), + ], + ), + ( + "a[code]`", + [Text("a"), container("", Text("`"), **{"class": "code"})], + ), ( "[spoiler=should open|should close]spoiler [b]content[/b]!", [ diff --git a/textoutpc/parser.py b/textoutpc/parser.py index 4bb56d6..4529572 100644 --- a/textoutpc/parser.py +++ b/textoutpc/parser.py @@ -34,6 +34,7 @@ from .lexer import ( Entity, NewlineEntity, OpenTagEntity, + SpecialEntity, TextEntity, iter_textout_entities, ) @@ -153,6 +154,37 @@ class StateMachine: return [Text(text)] + def open_tag(self, tag: Tag, /) -> None: + """Open a new stack level. + + :param tag: The tag with which to open the tag. + """ + # Add the text currently in the buffer to the top of the stack + # before inserting the new element. + text_nodes = self.flush_text() + if text_nodes: + prev: Sequence[Node] | Document + if self.stack: + prev = self.stack[0].children + else: + prev = self.document + + if len(prev) > 0 and isinstance(prev[-1], Text): + prev[-1] = Text(str(prev[-1]) + str(text_nodes[0])) + else: + prev.extend(text_nodes) + + # Insert the element. + self.stack.insert( + 0, + StackElement( + name=tag.name, + tag=tag, + children=[], + is_raw=tag.is_raw(), + ), + ) + def close_multiple(self, count: int, /) -> None: """Close multiple tags. @@ -225,31 +257,7 @@ class StateMachine: self.text += entity.raw return - # Add the text currently in the buffer to the top of the stack - # before inserting the new element. - text_nodes = self.flush_text() - if text_nodes: - prev: Sequence[Node] | Document - if self.stack: - prev = self.stack[0].children - else: - prev = self.document - - if len(prev) > 0 and isinstance(prev[-1], Text): - prev[-1] = Text(str(prev[-1]) + str(text_nodes[0])) - else: - prev.extend(text_nodes) - - # Insert the element. - self.stack.insert( - 0, - StackElement( - name=f"[{entity.name}]", - tag=tag, - children=[], - is_raw=tag.is_raw(), - ), - ) + self.open_tag(tag) return if isinstance(entity, CloseTagEntity): @@ -271,12 +279,44 @@ class StateMachine: if ent_name in ("[]", el.name): self.close_multiple(1 + i) return - else: - # The closing tag doesn't correspond to an existing tag, - # so we consider it as simple text. - self.text += entity.raw + + # The closing tag doesn't correspond to an existing tag, + # so we consider it as simple text. + self.text += entity.raw + return + + if isinstance(entity, SpecialEntity): + # This either opens or closes a tag. + if self.stack and self.stack[0].is_raw: + if self.stack[0].name == entity.value: + self.close_multiple(1) + else: + self.text += entity.value + return + # If the tag is opened, we want to close it. + for i, el in enumerate(self.stack): + if entity.value == el.name: + self.close_multiple(1 + i) + return + + tag_cls = self.tags.get(entity.value) + if tag_cls is None: + self.text += entity.value + return + + # Otherwise, we want to open the tag. + try: + tag = tag_cls(name=entity.value) + except TagValidationError: + # TODO: Add a warning. + self.text += entity.value + return + + self.open_tag(tag) + return + raise NotImplementedError( # pragma: no cover f"Unsupported element {entity!r}", )