diff --git a/sanity_html/constants.py b/sanity_html/constants.py index bc73e2d..3bc245e 100644 --- a/sanity_html/constants.py +++ b/sanity_html/constants.py @@ -2,20 +2,20 @@ from typing import TYPE_CHECKING -from sanity_html.marker_definitions import ( - CodeMarkerDefinition, - CommentMarkerDefinition, - EmphasisMarkerDefinition, - LinkMarkerDefinition, - StrikeThroughMarkerDefinition, - StrongMarkerDefinition, - UnderlineMarkerDefinition, +from sanity_html.marker_serializers import ( + CodeSerializer, + CommentSerializer, + EmphasisSerializer, + LinkSerializer, + StrikeThroughSerializer, + StrongSerializer, + UnderlineSerializer, ) if TYPE_CHECKING: from typing import Dict, Type - from sanity_html.marker_definitions import MarkerDefinition + from sanity_html.marker_serializers import MarkerSerializer STYLE_MAP = { 'h1': 'h1', @@ -28,15 +28,15 @@ 'normal': 'p', } -DECORATOR_MARKER_DEFINITIONS: Dict[str, Type[MarkerDefinition]] = { - 'em': EmphasisMarkerDefinition, - 'strong': StrongMarkerDefinition, - 'code': CodeMarkerDefinition, - 'underline': UnderlineMarkerDefinition, - 'strike-through': StrikeThroughMarkerDefinition, +DECORATOR_MARKER_SERIALIZERS: Dict[str, Type[MarkerSerializer]] = { + 'em': EmphasisSerializer, + 'strong': StrongSerializer, + 'code': CodeSerializer, + 'underline': UnderlineSerializer, + 'strike-through': StrikeThroughSerializer, } -ANNOTATION_MARKER_DEFINITIONS: Dict[str, Type[MarkerDefinition]] = { - 'link': LinkMarkerDefinition, - 'comment': CommentMarkerDefinition, +ANNOTATION_MARKER_SERIALIZERS: Dict[str, Type[MarkerSerializer]] = { + 'link': LinkSerializer, + 'comment': CommentSerializer, } diff --git a/sanity_html/marker_definitions.py b/sanity_html/marker_serializers.py similarity index 73% rename from sanity_html/marker_definitions.py rename to sanity_html/marker_serializers.py index 9bd3153..04d9940 100644 --- a/sanity_html/marker_definitions.py +++ b/sanity_html/marker_serializers.py @@ -3,18 +3,19 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Type + from typing import Optional, Type from sanity_html.types import Block, Span -class MarkerDefinition: +class MarkerSerializer: """Base class for marker definition handlers.""" tag: str + type: Optional[str] @classmethod - def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: + def render_prefix(cls: Type[MarkerSerializer], span: Span, marker: str, context: Block) -> str: """Render the prefix for the marked span. Usually this this the opening of the HTML tag. @@ -22,7 +23,7 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: return f'<{cls.tag}>' @classmethod - def render_suffix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: + def render_suffix(cls: Type[MarkerSerializer], span: Span, marker: str, context: Block) -> str: """Render the suffix for the marked span. Usually this this the closing of the HTML tag. @@ -30,7 +31,7 @@ def render_suffix(cls: Type[MarkerDefinition], span: Span, marker: str, context: return f'' @classmethod - def render(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: + def render(cls: Type[MarkerSerializer], span: Span, marker: str, context: Block) -> str: """Render the marked span directly with prefix and suffix.""" result = cls.render_prefix(span, marker, context) result += str(span.text) @@ -41,42 +42,42 @@ def render(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) # Decorators -class DefaultMarkerDefinition(MarkerDefinition): +class DefaultMarkerSerializer(MarkerSerializer): """Marker used for unknown definitions.""" tag = 'span' -class EmphasisMarkerDefinition(MarkerDefinition): +class EmphasisSerializer(MarkerSerializer): """Marker definition for rendering.""" tag = 'em' -class StrongMarkerDefinition(MarkerDefinition): +class StrongSerializer(MarkerSerializer): """Marker definition for rendering.""" tag = 'strong' -class CodeMarkerDefinition(MarkerDefinition): +class CodeSerializer(MarkerSerializer): """Marker definition for rendering.""" tag = 'code' -class UnderlineMarkerDefinition(MarkerDefinition): +class UnderlineSerializer(MarkerSerializer): """Marker definition for rendering.""" tag = 'span' @classmethod - def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: + def render_prefix(cls: Type[MarkerSerializer], span: Span, marker: str, context: Block) -> str: """Render the span with the appropriate style for underline.""" return '' -class StrikeThroughMarkerDefinition(MarkerDefinition): +class StrikeThroughSerializer(MarkerSerializer): """Marker definition for rendering.""" tag = 'del' @@ -85,13 +86,13 @@ class StrikeThroughMarkerDefinition(MarkerDefinition): # Annotations -class LinkMarkerDefinition(MarkerDefinition): +class LinkSerializer(MarkerSerializer): """Marker definition for link rendering.""" tag = 'a' @classmethod - def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: + def render_prefix(cls: Type[MarkerSerializer], span: Span, marker: str, context: Block) -> str: """Render the opening anchor tag with the href attribute set. The href attribute is fetched from the provided block context using @@ -104,17 +105,17 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: return f'' -class CommentMarkerDefinition(MarkerDefinition): +class CommentSerializer(MarkerSerializer): """Marker definition for HTML comment rendering.""" tag = '!--' @classmethod - def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str: + def render_prefix(cls: Type[MarkerSerializer], span: Span, marker: str, context: Block) -> str: """Render the opening of the HTML comment block.""" return '' diff --git a/sanity_html/renderer.py b/sanity_html/renderer.py index 2bd9805..3f7ed59 100644 --- a/sanity_html/renderer.py +++ b/sanity_html/renderer.py @@ -1,17 +1,18 @@ from __future__ import annotations -import html +from collections import deque from typing import TYPE_CHECKING from sanity_html.constants import STYLE_MAP -from sanity_html.marker_definitions import DefaultMarkerDefinition +from sanity_html.serializers.lists import ListSerializer +from sanity_html.serializers.spans import SpanSerializer from sanity_html.types import Block, Span -from sanity_html.utils import get_list_tags, is_block, is_list, is_span +from sanity_html.utils import is_block, is_list, is_span if TYPE_CHECKING: from typing import Callable, Dict, List, Optional, Type, Union - from sanity_html.marker_definitions import MarkerDefinition + from sanity_html.marker_serializers import MarkerSerializer class SanityBlockRenderer: @@ -20,7 +21,7 @@ class SanityBlockRenderer: def __init__( self, blocks: Union[list[dict], dict], - custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None, + custom_marker_definitions: dict[str, Type[MarkerSerializer]] = None, custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None, ) -> None: self._wrapper_element: Optional[str] = None @@ -39,24 +40,10 @@ def render(self) -> str: return '' result = '' - list_nodes: List[Dict] = [] - - for node in self._blocks: - - if list_nodes and not is_list(node): - tree = self._normalize_list_tree(list_nodes) - result += ''.join([self._render_node(n, Block(**node), list_item=True) for n in tree]) - list_nodes = [] # reset list_nodes - - if is_list(node): - list_nodes.append(node) - continue # handle all elements ^ when the list ends - - result += self._render_node(node) # render non-list nodes immediately - - if list_nodes: - tree = self._normalize_list_tree(list_nodes) - result += ''.join(self._render_node(n, Block(**node), list_item=True) for n in tree) + blocks = deque(self._blocks) + while blocks: + node = blocks.popleft() + result += self._render_node(blocks, node) # render non-list nodes immediately result = result.strip() @@ -64,157 +51,51 @@ def render(self) -> str: return f'<{self._wrapper_element}>{result}' return result - def _render_node(self, node: dict, context: Optional[Block] = None, list_item: bool = False) -> str: + def _render_node( + self, + blocks: deque[dict], + node: dict, + context: Optional[Block] = None, + inline: bool = False, + child_idx: Optional[int] = None, + ) -> str: """ Call the correct render method depending on the node type. :param node: Block content node - can be block, span, or list (block). :param context: Optional context. Spans are passed with a Block instance as context for mark lookups. - :param list_item: Whether we are handling a list upstream (impacts block handling). + :param inline: Whether the node should be wrapped. + :param child_idx: 0-based index of this node in the parent node children array. """ if is_list(node): - block = Block(**node, marker_definitions=self._custom_marker_definitions) - return self._render_list(block, context) + return ListSerializer(self)(node, blocks) elif is_block(node): block = Block(**node, marker_definitions=self._custom_marker_definitions) - return self._render_block(block, list_item=list_item) - + return self._render_block(block, inline=inline) elif is_span(node): - if isinstance(node, str): - # TODO: Remove if we there's no coverage for this after we've fixed tests - # not convinced this code path is possible - put it in because the sanity lib checks for it - span = Span(**{'text': node}) - else: - span = Span(**node) - assert context # this should be a cast - return self._render_span(span, block=context) # context is span's outer block + return SpanSerializer(self)(node, context, child_idx) elif self._custom_serializers.get(node.get('_type', '')): - return self._custom_serializers.get(node.get('_type', ''))(node, context, list_item) # type: ignore + return self._custom_serializers.get(node.get('_type', ''))(node, context, inline) # type: ignore else: print('Unexpected code path 👺') # noqa: T001 # TODO: Remove after thorough testing return '' - def _render_block(self, block: Block, list_item: bool = False) -> str: + def _render_block(self, block: Block, inline: bool = False) -> str: text, tag = '', STYLE_MAP[block.style] - if not list_item or tag != 'p': + if not inline or tag != 'p': text += f'<{tag}>' - for child_node in block.children: - text += self._render_node(child_node, context=block) + children = deque(block.children) + for idx, child_node in enumerate(block.children): + text += self._render_node(children, child_node, context=block, child_idx=idx) - if not list_item or tag != 'p': + if not inline or tag != 'p': text += f'' return text - def _render_span(self, span: Span, block: Block) -> str: - result: str = '' - prev_node, next_node = block.get_node_siblings(span) - prev_marks = prev_node.get('marks', []) if prev_node else [] - next_marks = next_node.get('marks', []) if next_node else [] - - sorted_marks = sorted(span.marks, key=lambda x: -block.marker_frequencies[x]) - for mark in sorted_marks: - if mark in prev_marks: - continue - marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)() - result += marker_callable.render_prefix(span, mark, block) - - result += html.escape(span.text).replace('\n', '
') - - for mark in reversed(sorted_marks): - if mark in next_marks: - continue - - marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)() - result += marker_callable.render_suffix(span, mark, block) - - return result - - def _render_list(self, node: Block, context: Optional[Block]) -> str: - assert node.listItem - head, tail = get_list_tags(node.listItem) - result = head - for child in node.children: - result += f'
  • {self._render_block(Block(**child), True)}
  • ' - result += tail - return result - - def _normalize_list_tree(self, nodes: list) -> list[dict]: - tree = [] - - current_list = None - for node in nodes: - if not is_block(node): - tree.append(node) - current_list = None - continue - - if current_list is None: - current_list = self._list_from_block(node) - tree.append(current_list) - continue - - if node.get('level') == current_list['level'] and node.get('listItem') == current_list['listItem']: - current_list['children'].append(node) - continue - - if node.get('level') > current_list['level']: - new_list = self._list_from_block(node) - current_list['children'][-1]['children'].append(new_list) - current_list = new_list - continue - - if node.get('level') < current_list['level']: - parent = self._find_list(tree[-1], level=node.get('level'), list_item=node.get('listItem')) - if parent: - current_list = parent - current_list['children'].append(node) - continue - current_list = self._list_from_block(node) - tree.append(current_list) - continue - - if node.get('listItem') != current_list['listItem']: - match = self._find_list(tree[-1], level=node.get('level')) - if match and match['listItem'] == node.get('listItem'): - current_list = match - current_list['children'].append(node) - continue - current_list = self._list_from_block(node) - tree.append(current_list) - continue - # TODO: Warn - tree.append(node) - - return tree - - def _find_list(self, root_node: dict, level: int, list_item: Optional[str] = None) -> Optional[dict]: - filter_on_type = isinstance(list_item, str) - if ( - root_node.get('_type') == 'list' - and root_node.get('level') == level - and (filter_on_type and root_node.get('listItem') == list_item) - ): - return root_node - - children = root_node.get('children') - if children: - return self._find_list(children[-1], level, list_item) - - return None - - def _list_from_block(self, block: dict) -> dict: - return { - '_type': 'list', - '_key': f'${block["_key"]}-parent', - 'level': block.get('level'), - 'listItem': block['listItem'], - 'children': [block], - } - def render(blocks: List[Dict], *args, **kwargs) -> str: """Shortcut function inspired by Sanity's own blocksToHtml.h callable.""" diff --git a/sanity_html/serializers/__init__.py b/sanity_html/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sanity_html/serializers/lists.py b/sanity_html/serializers/lists.py new file mode 100644 index 0000000..2b6eb89 --- /dev/null +++ b/sanity_html/serializers/lists.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from sanity_html.types import Block +from sanity_html.utils import is_block, is_list + +if TYPE_CHECKING: + from collections import deque + from typing import Optional + + from sanity_html import SanityBlockRenderer + + +class ListSerializer: + def __init__(self, sanity_renderer: SanityBlockRenderer) -> None: + self.sanity_renderer = sanity_renderer + + def __call__(self, node: dict, blocks: deque[dict]) -> str: + result = '' + list_items: list[dict] = [node] + while blocks and is_list(blocks[0]): + list_items.append(blocks.popleft()) + list_roots = self._normalize_list_tree(list_items) + for list_root in list_roots: + head, tail = self.get_list_tags(list_root['listItem']) + result += head + for child in list_root['children']: + result += f'
  • {self.sanity_renderer._render_block(Block(**child), True)}
  • ' + result += tail + return result + + def get_list_tags(self, list_item: str) -> tuple[str, str]: + """Return the appropriate list tags for a given list item.""" + # TODO: Make it possible for users to pass their own maps, perhaps by adding this to the class + # and checking optional class context variables defined on initialization. + return { + 'bullet': ('
      ', '
    '), + 'square': ('
      ', '
    '), + 'number': ('
      ', '
    '), + }[list_item] + + def _normalize_list_tree(self, nodes: list) -> list[dict]: + tree = [] + + current_list = None + for node in nodes: + if not is_block(node): + tree.append(node) + current_list = None + continue + + if current_list is None: + current_list = self._list_from_block(node) + tree.append(current_list) + continue + + if node.get('level') == current_list['level'] and node.get('listItem') == current_list['listItem']: + current_list['children'].append(node) + continue + + if node.get('level') > current_list['level']: + new_list = self._list_from_block(node) + current_list['children'][-1]['children'].append(new_list) + current_list = new_list + continue + + if node.get('level') < current_list['level']: + parent = self._find_list(tree[-1], level=node.get('level'), list_item=node.get('listItem')) + if parent: + current_list = parent + current_list['children'].append(node) + continue + current_list = self._list_from_block(node) + tree.append(current_list) + continue + + if node.get('listItem') != current_list['listItem']: + match = self._find_list(tree[-1], level=node.get('level')) + if match and match['listItem'] == node.get('listItem'): + current_list = match + current_list['children'].append(node) + continue + current_list = self._list_from_block(node) + tree.append(current_list) + continue + # TODO: Warn + tree.append(node) + + return tree + + def _find_list(self, root_node: dict, level: int, list_item: Optional[str] = None) -> Optional[dict]: + filter_on_type = isinstance(list_item, str) + if ( + root_node.get('_type') == 'list' + and root_node.get('level') == level + and (filter_on_type and root_node.get('listItem') == list_item) + ): + return root_node + + children = root_node.get('children') + if children: + return self._find_list(children[-1], level, list_item) + + return None + + def _list_from_block(self, block: dict) -> dict: + return { + '_type': 'list', + '_key': f'${block["_key"]}-parent', + 'level': block.get('level'), + 'listItem': block['listItem'], + 'children': [block], + } diff --git a/sanity_html/serializers/spans.py b/sanity_html/serializers/spans.py new file mode 100644 index 0000000..7ad8d5e --- /dev/null +++ b/sanity_html/serializers/spans.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +import html + +from sanity_html.constants import DECORATOR_MARKER_SERIALIZERS +from sanity_html.marker_serializers import DefaultMarkerSerializer +from sanity_html.types import Span + +if TYPE_CHECKING: + from typing import Optional, Union + from sanity_html.renderer import SanityBlockRenderer + from sanity_html.types import Block + + +class SpanSerializer: + def __init__(self, sanity_renderer: SanityBlockRenderer) -> None: + self.sanity_renderer = sanity_renderer + + def __call__(self, span: Union[dict, str], block: Block, child_idx: Optional[int] = None): + if isinstance(span, str): + span = Span(**{'text': span}) + else: + span = Span(**span) + + result: str = '' + prev_node, next_node = block.get_node_siblings(span, child_idx=child_idx) + prev_marks = prev_node.get('marks', []) if prev_node else [] + next_marks = next_node.get('marks', []) if next_node else [] + + sorted_marks = sorted( + span.marks, key=lambda x: (int(x in DECORATOR_MARKER_SERIALIZERS), -block.marker_frequencies[x]) + ) + for mark in sorted_marks: + if mark in prev_marks: + continue + marker_callable = block.marker_definitions.get(mark)() + result += marker_callable.render_prefix(span, mark, block) + + result += html.escape(span.text).replace('\n', '
    ') + + for mark in reversed(sorted_marks): + print(child_idx, mark, 'in', next_marks, next_node) + if mark in next_marks: + continue + + marker_callable = block.marker_definitions.get(mark, DefaultMarkerSerializer)() + result += marker_callable.render_suffix(span, mark, block) + + return result diff --git a/sanity_html/types.py b/sanity_html/types.py index 929594e..738adbf 100644 --- a/sanity_html/types.py +++ b/sanity_html/types.py @@ -3,12 +3,12 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, cast -from sanity_html.utils import get_default_marker_definitions +from sanity_html.constants import ANNOTATION_MARKER_SERIALIZERS, DECORATOR_MARKER_SERIALIZERS if TYPE_CHECKING: from typing import Literal, Optional, Tuple, Type, Union - from sanity_html.marker_definitions import MarkerDefinition + from sanity_html.marker_serializers import MarkerSerializer @dataclass(frozen=True) @@ -43,7 +43,7 @@ class Block: listItem: Optional[Literal['bullet', 'number', 'square']] = None children: list[dict] = field(default_factory=list) markDefs: list[dict] = field(default_factory=list) - marker_definitions: dict[str, Type[MarkerDefinition]] = field(default_factory=dict) + marker_definitions: dict[str, Type[MarkerSerializer]] = field(default_factory=dict) marker_frequencies: dict[str, int] = field(init=False) def __post_init__(self) -> None: @@ -53,7 +53,15 @@ def __post_init__(self) -> None: To make handling of span `marks` simpler, we define marker_definitions as a dict, from which we can directly look up both annotation marks or decorator marks. """ - marker_definitions = get_default_marker_definitions(self.markDefs) + marker_definitions = DECORATOR_MARKER_SERIALIZERS.copy() + for definition in self.markDefs: + if definition['_type'] in ANNOTATION_MARKER_SERIALIZERS: + marker = ANNOTATION_MARKER_SERIALIZERS[definition['_type']] + marker_definitions[definition['_key']] = marker + if definition['_type'] in self.marker_definitions: + marker = self.marker_definitions[definition['_type']] + marker_definitions[definition['_key']] = marker + marker_definitions.update(self.marker_definitions) self.marker_definitions = marker_definitions self.marker_frequencies = self._compute_marker_frequencies() @@ -68,28 +76,35 @@ def _compute_marker_frequencies(self) -> dict[str, int]: counts[mark] = 0 return counts - def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Optional[dict]]: + def get_node_siblings( + self, node: Union[dict, Span], child_idx: Optional[int] = None + ) -> Tuple[Optional[dict], Optional[dict]]: """Return the sibling nodes (prev, next) to the given node.""" if not self.children: return None, None - try: - if not isinstance(node, (dict, Span)): - raise ValueError(f'Expected dict or Span but received {type(node)}') - elif type(node) == dict: - node = cast(dict, node) - node_idx = self.children.index(node) - elif type(node) == Span: - node = cast(Span, node) - node_idx = self.children.index(next((c for c in self.children if c.get('_key') == node._key), {})) - except ValueError: - return None, None + if child_idx is not None: + node_idx = child_idx + else: + print('fallback') + try: + if not isinstance(node, (dict, Span)): + raise ValueError(f'Expected dict or Span but received {type(node)}') + elif type(node) == dict: + node = cast(dict, node) + node_idx = self.children.index(node) + elif type(node) == Span: + node = cast(Span, node) + node_idx = self.children.index(next((c for c in self.children if c.get('_key') == node._key), {})) + except ValueError: + return None, None prev_node = None next_node = None - + print(node_idx, self.children) + print(node_idx, len(self.children), node_idx < len(self.children) - 2) if node_idx >= 1: prev_node = self.children[node_idx - 1] - if node_idx < len(self.children) - 2: + if node_idx <= len(self.children) - 2: next_node = self.children[node_idx + 1] return prev_node, next_node diff --git a/sanity_html/utils.py b/sanity_html/utils.py index d0afec7..b3c77e9 100644 --- a/sanity_html/utils.py +++ b/sanity_html/utils.py @@ -2,15 +2,15 @@ from typing import TYPE_CHECKING -from sanity_html.constants import ANNOTATION_MARKER_DEFINITIONS, DECORATOR_MARKER_DEFINITIONS +from sanity_html.constants import ANNOTATION_MARKER_SERIALIZERS, DECORATOR_MARKER_SERIALIZERS if TYPE_CHECKING: from typing import Type - from sanity_html.marker_definitions import MarkerDefinition + from sanity_html.marker_serializers import MarkerSerializer -def get_default_marker_definitions(mark_defs: list[dict]) -> dict[str, Type[MarkerDefinition]]: +def get_default_marker_definitions(mark_defs: list[dict]) -> dict[str, Type[MarkerSerializer]]: """ Convert JSON definitions to a map of marker definition renderers. @@ -20,11 +20,11 @@ def get_default_marker_definitions(mark_defs: list[dict]) -> dict[str, Type[Mark marker_definitions = {} for definition in mark_defs: - if definition['_type'] in ANNOTATION_MARKER_DEFINITIONS: - marker = ANNOTATION_MARKER_DEFINITIONS[definition['_type']] + if definition['_type'] in ANNOTATION_MARKER_SERIALIZERS: + marker = ANNOTATION_MARKER_SERIALIZERS[definition['_type']] marker_definitions[definition['_key']] = marker - return {**marker_definitions, **DECORATOR_MARKER_DEFINITIONS} + return {**marker_definitions, **DECORATOR_MARKER_SERIALIZERS} def is_list(node: dict) -> bool: @@ -40,14 +40,3 @@ def is_span(node: dict) -> bool: def is_block(node: dict) -> bool: """Check whether a node is a block node.""" return node.get('_type') == 'block' - - -def get_list_tags(list_item: str) -> tuple[str, str]: - """Return the appropriate list tags for a given list item.""" - # TODO: Make it possible for users to pass their own maps, perhaps by adding this to the class - # and checking optional class context variables defined on initialization. - return { - 'bullet': ('
      ', '
    '), - 'square': ('
      ', '
    '), - 'number': ('
      ', '
    '), - }[list_item] diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index cbbccf3..6b43d03 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -1,10 +1,10 @@ -from sanity_html.marker_definitions import ( - CommentMarkerDefinition, - EmphasisMarkerDefinition, - LinkMarkerDefinition, - StrikeThroughMarkerDefinition, - StrongMarkerDefinition, - UnderlineMarkerDefinition, +from sanity_html.marker_serializers import ( + CommentSerializer, + EmphasisSerializer, + LinkSerializer, + StrikeThroughSerializer, + StrongSerializer, + UnderlineSerializer, ) from sanity_html.types import Block, Span @@ -15,31 +15,28 @@ def test_render_emphasis_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) - assert EmphasisMarkerDefinition.render(node, 'em', block) == f'{text}' + assert EmphasisSerializer.render(node, 'em', block) == f'{text}' def test_render_strong_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) - assert StrongMarkerDefinition.render(node, 'strong', block) == f'{text}' + assert StrongSerializer.render(node, 'strong', block) == f'{text}' def test_render_underline_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) - assert ( - UnderlineMarkerDefinition.render(node, 'u', block) - == f'{text}' - ) + assert UnderlineSerializer.render(node, 'u', block) == f'{text}' def test_render_strikethrough_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) - assert StrikeThroughMarkerDefinition.render(node, 'strike', block) == f'{text}' + assert StrikeThroughSerializer.render(node, 'strike', block) == f'{text}' def test_render_link_marker_success(): @@ -48,11 +45,11 @@ def test_render_link_marker_success(): block = Block( _type='block', children=[node.__dict__], markDefs=[{'_type': 'link', '_key': 'linkId', 'href': text}] ) - assert LinkMarkerDefinition.render(node, 'linkId', block) == f'
    {text}' + assert LinkSerializer.render(node, 'linkId', block) == f'{text}' def test_render_comment_marker_success(): for text in sample_texts: node = Span(_type='span', text=text) block = Block(_type='block', children=[node.__dict__]) - assert CommentMarkerDefinition.render(node, 'comment', block) == f'' + assert CommentSerializer.render(node, 'comment', block) == f'' diff --git a/tests/test_upstream_suite.py b/tests/test_upstream_suite.py index 340e9e3..5e10982 100644 --- a/tests/test_upstream_suite.py +++ b/tests/test_upstream_suite.py @@ -6,7 +6,7 @@ import pytest from sanity_html import render -from sanity_html.marker_definitions import LinkMarkerDefinition, MarkerDefinition +from sanity_html.marker_serializers import LinkSerializer, MarkerSerializer from sanity_html.renderer import SanityBlockRenderer from sanity_html.types import Block, Span @@ -47,6 +47,23 @@ def fake_image_serializer(node: dict, context: Optional[Block], list_item: bool) return f'
    {image}
    ' +def fake_codeserializer(node: dict, context: Optional[Block], list_item: bool): + """ + { + "input": [ + { + "_type": "code", + "_key": "9a15ea2ed8a2", + "language": "javascript", + "code": "const foo = require('foo')\n\nfoo('hi there', (err, thing) => {\n console.log(err)\n})\n" + } + ], + "output": "
    const foo = require('foo')\n\nfoo('hi there', (err, thing) => {\n  console.log(err)\n})\n
    " + """ + assert node['_type'] == 'code' + import html + return f'
    {html.escape(node["code"])}
    ' + def get_fixture(rel_path) -> dict: """Load and return fixture data as dict.""" return json.loads((Path(__file__).parent / rel_path).read_text()) @@ -198,7 +215,18 @@ def test_018_marks_all_the_way_dow(): fixture_data = get_fixture('fixtures/upstream/018-marks-all-the-way-down.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + class Highlight(MarkerSerializer): + tag = 'span' + type = 'highlight' + @classmethod + def render_prefix(cls: Type[MarkerSerializer], span: Span, marker: str, context: Block) -> str: + marker_definition = next((md for md in context.markDefs if md['_key'] == marker), None) + if not marker_definition: + raise ValueError(f'Marker definition for key: {marker} not found in parent block context') + thickness = marker_definition.get('thickness', '') + return f'' + sbr = SanityBlockRenderer(input_blocks, custom_marker_definitions={'highlight': Highlight}) + output = sbr.render() assert output == expected_output @@ -277,12 +305,12 @@ def test_027_styled_list_item(): assert output == expected_output -@pytest.mark.unsupported def test_050_custom_block_type(): fixture_data = get_fixture('fixtures/upstream/050-custom-block-type.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - output = render(input_blocks) + sbr = SanityBlockRenderer(input_blocks, custom_serializers={'code': fake_codeserializer}) + output = sbr.render() assert output == expected_output @@ -295,13 +323,12 @@ def test_051_override_default(): assert output == expected_output -@pytest.mark.unsupported def test_052_custom_mark(): fixture_data = get_fixture('fixtures/upstream/052-custom-marks.json') input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - class CustomMarkerSerializer(MarkerDefinition): + class CustomMarkerSerializer(MarkerSerializer): tag = 'span' @classmethod @@ -317,7 +344,7 @@ def test_053_override_default_mark(): input_blocks = fixture_data['input'] expected_output = fixture_data['output'] - class CustomLinkMark(LinkMarkerDefinition): + class CustomLinkMark(LinkSerializer): @classmethod def render_prefix(cls, span, marker, context) -> str: result = super().render_prefix(span, marker, context)