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}{self._wrapper_element}>'
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'{tag}>'
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)