diff --git a/.github/scripts/get_python_versions.py b/.github/scripts/get_python_versions.py
deleted file mode 100644
index 8a39145..0000000
--- a/.github/scripts/get_python_versions.py
+++ /dev/null
@@ -1,22 +0,0 @@
-if __name__ == '__main__':
- import json
-
- import requests
- from packaging import version as semver
-
- stable_versions = requests.get(
- 'https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json'
- ).json()
-
- min_version = semver.parse('3.7')
- versions = {}
-
- for version_object in stable_versions:
-
- version = version_object['version']
- major_and_minor_version = semver.parse('.'.join(version.split('.')[:2]))
-
- if major_and_minor_version not in versions and major_and_minor_version >= min_version:
- versions[major_and_minor_version] = version
-
- print(json.dumps(list(versions.values()))) # noqa
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 082066e..22b34ce 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,7 +18,7 @@ jobs:
id: cache-venv
with:
path: .venv
- key: venv-0
+ key: venv-1
- run: |
python -m venv .venv --upgrade-deps
source .venv/bin/activate
@@ -28,7 +28,7 @@ jobs:
id: pre-commit-cache
with:
path: ~/.cache/pre-commit
- key: key-0
+ key: key-1
- run: |
source .venv/bin/activate
pre-commit run --all-files
@@ -38,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: [ "3.7", "3.8", "3.9", "3.10" ]
+ python-version: [ "3.7.14", "3.8.14", "3.9.15", "3.10.8", "3.11.0", "3.12.0-alpha.1" ]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
@@ -48,20 +48,19 @@ jobs:
id: poetry-cache
with:
path: ~/.local
- key: key-0
+ key: key-2
- uses: snok/install-poetry@v1
with:
virtualenvs-create: false
- version: 1.2.0a2
- uses: actions/cache@v2
id: cache-venv
with:
path: .venv
- key: ${{ hashFiles('**/poetry.lock') }}-0
+ key: ${{ hashFiles('**/poetry.lock') }}-1
- run: |
python -m venv .venv
source .venv/bin/activate
- pip install -U pip
+ pip install -U pip wheel
poetry install --no-interaction --no-root
if: steps.cache-venv.outputs.cache-hit != 'true'
- name: Run tests
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3062b26..6493166 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,11 +1,11 @@
repos:
- repo: https://github.com/ambv/black
- rev: 21.11b1
+ rev: 22.10.0
hooks:
- id: black
args: [ "--quiet" ]
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.0.1
+ rev: v4.3.0
hooks:
- id: check-ast
- id: check-merge-conflict
@@ -18,8 +18,8 @@ repos:
- id: trailing-whitespace
- id: mixed-line-ending
- id: trailing-whitespace
- - repo: https://gitlab.com/pycqa/flake8
- rev: 3.9.2
+ - repo: https://github.com/pycqa/flake8
+ rev: 5.0.4
hooks:
- id: flake8
additional_dependencies: [
@@ -34,8 +34,10 @@ repos:
'flake8-printf-formatting',
'flake8-type-checking',
]
+ args:
+ - '--allow-star-arg-any'
- repo: https://github.com/asottile/pyupgrade
- rev: v2.29.1
+ rev: v3.2.2
hooks:
- id: pyupgrade
args: [ "--py36-plus", "--py37-plus",'--keep-runtime-typing' ]
@@ -44,7 +46,7 @@ repos:
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.910-1
+ rev: v0.991
hooks:
- id: mypy
additional_dependencies:
diff --git a/README.md b/README.md
index dc43d25..fd610a8 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
+[](https://pypi.org/project/portabletext-html/)
+[](https://github.com/otovo/python-portabletext-html/actions/workflows/test.yml)
+[](https://codecov.io/gh/otovo/python-portabletext-html)
+[](https://pypi.org/project/python-portabletext-html/)
# Portable Text HTML Renderer for Python
@@ -168,6 +160,11 @@ class ComicSansEmphasis(MarkerDefinition):
def render_suffix(cls, span: Span, marker: str, context: Block) -> str:
return f'{cls.tag}>'
+ @classmethod
+ def render_text(cls, span: Span, marker: str, context: Block) -> str:
+ # custom rendering logic can be placed here
+ return str(span.text)
+
@classmethod
def render(cls, span: Span, marker: str, context: Block) -> str:
result = cls.render_prefix(span, marker, context)
@@ -223,4 +220,4 @@ In the meantime, users should be able to serialize image types by passing a cust
Contributions are always appreciated 👏
-For details, see the [CONTRIBUTING.md](https://github.com/otovo/python-sanity-html/blob/main/CONTRIBUTING.md).
+For details, see the [CONTRIBUTING.md](https://github.com/otovo/python-portabletext-html/blob/main/CONTRIBUTING.md).
diff --git a/poetry.lock b/poetry.lock
index 268a8e6..b05bfd9 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,6 +1,6 @@
[[package]]
name = "atomicwrites"
-version = "1.4.0"
+version = "1.4.1"
description = "Atomic file writes."
category = "dev"
optional = false
@@ -8,25 +8,25 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
-version = "21.2.0"
+version = "22.1.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.5"
[package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
-docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
+dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
+docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
+tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
+tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
[[package]]
name = "colorama"
-version = "0.4.4"
+version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
[[package]]
name = "coverage"
@@ -55,20 +55,20 @@ pyflakes = ">=2.3.0,<2.4.0"
[[package]]
name = "importlib-metadata"
-version = "4.8.2"
+version = "5.0.0"
description = "Read metadata from Python packages"
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
perf = ["ipython"]
-testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
[[package]]
name = "iniconfig"
@@ -138,11 +138,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyparsing"
-version = "3.0.6"
-description = "Python parsing module"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.6.8"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
@@ -183,7 +183,7 @@ pytest = ">=4.6"
toml = "*"
[package.extras]
-testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]]
name = "toml"
@@ -195,23 +195,23 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "typing-extensions"
-version = "4.0.0"
-description = "Backported and Experimental Type Hints for Python 3.6+"
+version = "4.4.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[[package]]
name = "zipp"
-version = "3.6.0"
+version = "3.10.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
+testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "1.1"
@@ -220,16 +220,15 @@ content-hash = "c641d950bccb6ffac52cf3fcd3571b51f5e31d4864c03e763fe2748919bf855b
[metadata.files]
atomicwrites = [
- {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
- {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+ {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
]
attrs = [
- {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
- {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
+ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
+ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
]
colorama = [
- {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
- {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
coverage = [
{file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"},
@@ -290,8 +289,8 @@ flake8 = [
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
]
importlib-metadata = [
- {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"},
- {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"},
+ {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"},
+ {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@@ -322,8 +321,8 @@ pyflakes = [
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
]
pyparsing = [
- {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"},
- {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"},
+ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
@@ -338,10 +337,10 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typing-extensions = [
- {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"},
- {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"},
+ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
+ {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
]
zipp = [
- {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
- {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
+ {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"},
+ {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"},
]
diff --git a/portabletext_html/marker_definitions.py b/portabletext_html/marker_definitions.py
index 72cc2d2..396f4e1 100644
--- a/portabletext_html/marker_definitions.py
+++ b/portabletext_html/marker_definitions.py
@@ -37,10 +37,15 @@ def render_suffix(cls: Type[MarkerDefinition], span: Span, marker: str, context:
def render(cls: Type[MarkerDefinition], 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)
+ result += cls.render_text(span, marker, context)
result += cls.render_suffix(span, marker, context)
return result
+ @classmethod
+ def render_text(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str:
+ """Render the content part for a marked span."""
+ return str(span.text)
+
# Decorators
diff --git a/portabletext_html/renderer.py b/portabletext_html/renderer.py
index 614b177..1ca2ce3 100644
--- a/portabletext_html/renderer.py
+++ b/portabletext_html/renderer.py
@@ -10,7 +10,7 @@
from portabletext_html.utils import get_list_tags, is_block, is_list, is_span
if TYPE_CHECKING:
- from typing import Callable, Dict, List, Optional, Type, Union
+ from typing import Any, Callable, Dict, List, Optional, Type, Union
from portabletext_html.marker_definitions import MarkerDefinition
@@ -38,8 +38,8 @@ class PortableTextRenderer:
def __init__(
self,
blocks: Union[list[dict], dict],
- custom_marker_definitions: dict[str, Type[MarkerDefinition]] = None,
- custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] = None,
+ custom_marker_definitions: dict[str, Type[MarkerDefinition]] | None = None,
+ custom_serializers: dict[str, Callable[[dict, Optional[Block], bool], str]] | None = None,
) -> None:
logger.debug('Initializing block renderer')
self._wrapper_element: Optional[str] = None
@@ -66,7 +66,7 @@ def render(self) -> str:
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])
+ result += ''.join([self._render_node(n, list_item=True) for n in tree])
list_nodes = [] # reset list_nodes
if is_list(node):
@@ -106,14 +106,14 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b
elif is_span(node):
logger.debug('Rendering node as span')
span = Span(**node)
- context = cast(Block, context) # context should always be a Block here
+ context = cast('Block', context) # context should always be a Block here
return self._render_span(span, block=context)
elif self._custom_serializers.get(node.get('_type', '')):
return self._custom_serializers.get(node.get('_type', ''))(node, context, list_item) # type: ignore
else:
- if hasattr(node, '_type'):
+ if '_type' in node:
raise MissingSerializerError(
f'Found unhandled node type: {node["_type"]}. ' 'Most likely this requires a custom serializer.'
)
@@ -150,7 +150,19 @@ def _render_span(self, span: Span, block: Block) -> str:
marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)()
result += marker_callable.render_prefix(span, mark, block)
- result += html.escape(span.text).replace('\n', '
')
+ # to avoid rendering the text multiple times,
+ # only the first custom mark will be used
+ custom_mark_text_rendered = False
+ if sorted_marks:
+ for mark in sorted_marks:
+ if custom_mark_text_rendered or mark in prev_marks:
+ continue
+ marker_callable = block.marker_definitions.get(mark, DefaultMarkerDefinition)()
+ result += marker_callable.render_text(span, mark, block)
+ custom_mark_text_rendered = True
+
+ if not custom_mark_text_rendered:
+ result += html.escape(span.text).replace('\n', '
')
for mark in reversed(sorted_marks):
if mark in next_marks:
@@ -244,7 +256,7 @@ def _list_from_block(self, block: dict) -> dict:
}
-def render(blocks: List[Dict], *args, **kwargs) -> str:
+def render(blocks: List[Dict], *args: Any, **kwargs: Any) -> str:
"""Shortcut function inspired by Sanity's own blocksToHtml.h callable."""
renderer = PortableTextRenderer(blocks, *args, **kwargs)
return renderer.render()
diff --git a/portabletext_html/types.py b/portabletext_html/types.py
index 68638cd..898d61e 100644
--- a/portabletext_html/types.py
+++ b/portabletext_html/types.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
-from typing import TYPE_CHECKING, cast
+from typing import TYPE_CHECKING
from portabletext_html.utils import get_default_marker_definitions
@@ -53,9 +53,7 @@ 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.update(self.marker_definitions)
- self.marker_definitions = marker_definitions
+ self.marker_definitions = self._add_custom_marker_definitions()
self.marker_frequencies = self._compute_marker_frequencies()
def _compute_marker_frequencies(self) -> dict[str, int]:
@@ -68,16 +66,24 @@ def _compute_marker_frequencies(self) -> dict[str, int]:
counts[mark] = 0
return counts
+ def _add_custom_marker_definitions(self) -> dict[str, Type[MarkerDefinition]]:
+ marker_definitions = get_default_marker_definitions(self.markDefs)
+ marker_definitions.update(self.marker_definitions)
+ for definition in self.markDefs:
+ if definition['_type'] in self.marker_definitions:
+ marker = self.marker_definitions[definition['_type']]
+ marker_definitions[definition['_key']] = marker
+ # del marker_definitions[definition['_type']]
+ return marker_definitions
+
def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Optional[dict]]:
"""Return the sibling nodes (prev, next) to the given node."""
if not self.children:
return None, None
try:
if type(node) == dict:
- node = cast(dict, node)
node_idx = self.children.index(node)
elif type(node) == Span:
- node = cast(Span, node)
for index, item in enumerate(self.children):
if 'text' in item and node.text == item['text']:
# Is it possible to handle several identical texts?
@@ -88,11 +94,9 @@ def get_node_siblings(self, node: Union[dict, Span]) -> Tuple[Optional[dict], Op
except ValueError:
return None, None
- prev_node = None
next_node = None
- if node_idx != 0:
- prev_node = self.children[node_idx - 1]
+ prev_node = self.children[node_idx - 1] if node_idx != 0 else None
if node_idx != len(self.children) - 1:
next_node = self.children[node_idx + 1]
diff --git a/pyproject.toml b/pyproject.toml
index cc7ffeb..7dc039a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = 'portabletext-html'
-version = '1.0.0b1'
+version = '1.1.3'
description = "HTML renderer for Sanity's Portable Text format"
homepage = 'https://github.com/otovo/python-sanity-html'
repository = 'https://github.com/otovo/python-sanity-html'
@@ -25,6 +25,7 @@ classifiers = [
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
'Typing :: Typed',
]
diff --git a/setup.cfg b/setup.cfg
index 72028f4..44e5250 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -47,3 +47,21 @@ exclude =
max-complexity = 15
max-line-length = 120
+
+[mypy]
+show_error_codes = True
+warn_unused_ignores = True
+strict_optional = True
+incremental = True
+ignore_missing_imports = True
+warn_redundant_casts = True
+warn_unused_configs = True
+disallow_untyped_defs = True
+disallow_untyped_calls = True
+local_partial_types = True
+show_traceback = True
+exclude =
+ .venv/
+
+[mypy-tests.*]
+ignore_errors = True
diff --git a/tests/fixtures/custom_serializer_node_after_list.json b/tests/fixtures/custom_serializer_node_after_list.json
new file mode 100644
index 0000000..39386cf
--- /dev/null
+++ b/tests/fixtures/custom_serializer_node_after_list.json
@@ -0,0 +1,20 @@
+[
+ {
+ "_key": "e5b6e416e6e9",
+ "_type": "block",
+ "children": [
+ { "_key": "3bbbff0f158b", "_type": "span", "marks": [], "text": "resers" }
+ ],
+ "level": 1,
+ "listItem": "bullet",
+ "markDefs": [],
+ "style": "normal"
+ },
+ {
+ "_key": "73405dda68e0",
+ "_type": "extraInfoBlock",
+ "extraInfo": "This informations is not supported by Block",
+ "markDefs": [],
+ "style": "normal"
+ }
+]
diff --git a/tests/fixtures/invalid_node.json b/tests/fixtures/invalid_node.json
new file mode 100644
index 0000000..74f09bf
--- /dev/null
+++ b/tests/fixtures/invalid_node.json
@@ -0,0 +1,13 @@
+{
+ "_key": "73405dda68e7",
+ "children": [
+ {
+ "_key": "25a09c61d80a",
+ "_type": "span",
+ "marks": [],
+ "text": "Otovo guarantee is good"
+ }
+ ],
+ "markDefs": [],
+ "style": "normal"
+}
diff --git a/tests/fixtures/invalid_type.json b/tests/fixtures/invalid_type.json
new file mode 100644
index 0000000..745ac66
--- /dev/null
+++ b/tests/fixtures/invalid_type.json
@@ -0,0 +1,14 @@
+{
+ "_key": "73405dda68e7",
+ "_type": "invalid_type",
+ "children": [
+ {
+ "_key": "25a09c61d80a",
+ "_type": "span",
+ "marks": [],
+ "text": "Otovo guarantee is good"
+ }
+ ],
+ "markDefs": [],
+ "style": "normal"
+}
diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py
index 6011426..2677218 100644
--- a/tests/test_marker_definitions.py
+++ b/tests/test_marker_definitions.py
@@ -1,3 +1,6 @@
+# pylint: skip-file
+from typing import Type
+
from portabletext_html import PortableTextRenderer
from portabletext_html.marker_definitions import (
CommentMarkerDefinition,
@@ -16,6 +19,7 @@ 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_text(node, 'em', block) == f'{text}'
assert EmphasisMarkerDefinition.render(node, 'em', block) == f'{text}'
@@ -23,6 +27,7 @@ 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_text(node, 'strong', block) == f'{text}'
assert StrongMarkerDefinition.render(node, 'strong', block) == f'{text}'
@@ -30,6 +35,7 @@ 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_text(node, 'u', block) == f'{text}'
assert (
UnderlineMarkerDefinition.render(node, 'u', block)
== f'{text}'
@@ -40,6 +46,7 @@ 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_text(node, 'strike', block) == f'{text}'
assert StrikeThroughMarkerDefinition.render(node, 'strike', block) == f'{text}'
@@ -49,6 +56,7 @@ def test_render_link_marker_success():
block = Block(
_type='block', children=[node.__dict__], markDefs=[{'_type': 'link', '_key': 'linkId', 'href': text}]
)
+ assert LinkMarkerDefinition.render_text(node, 'linkId', block) == f'{text}'
assert LinkMarkerDefinition.render(node, 'linkId', block) == f'{text}'
@@ -62,19 +70,32 @@ def test_render_comment_marker_success():
def test_custom_marker_definition():
from portabletext_html.marker_definitions import MarkerDefinition
- class ComicSansEmphasis(MarkerDefinition):
+ class ConditionalMarkerDefinition(MarkerDefinition):
tag = 'em'
@classmethod
- def render_prefix(cls, span, marker, context):
- return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">'
+ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str:
+ marker_definition = next((md for md in context.markDefs if md['_key'] == marker), None)
+ condition = marker_definition.get('cloudCondition', '')
+ if not condition:
+ style = 'display: none'
+ return f'<{cls.tag} style=\"{style}\">'
+ else:
+ return super().render_prefix(span, marker, context)
+
+ @classmethod
+ def render_text(cls: Type[MarkerDefinition], span: Span, marker: str, context: Block) -> str:
+ marker_definition = next((md for md in context.markDefs if md['_key'] == marker), None)
+ condition = marker_definition.get('cloudCondition', '')
+ return span.text if not condition else ''
renderer = PortableTextRenderer(
- {
+ blocks={
'_type': 'block',
- 'children': [{'_key': 'a1ph4', '_type': 'span', 'marks': ['em'], 'text': 'Sanity'}],
- 'markDefs': [],
+ 'children': [{'_key': 'a1ph4', '_type': 'span', 'marks': ['some_id'], 'text': 'Sanity'}],
+ 'markDefs': [{'_key': 'some_id', '_type': 'contractConditional', 'cloudCondition': False}],
},
- custom_marker_definitions={'em': ComicSansEmphasis},
+ custom_marker_definitions={'contractConditional': ConditionalMarkerDefinition},
)
- assert renderer.render() == '
Sanity
' + result = renderer.render() + assert result == 'Sanity
' diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 3894862..1c82309 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -1,8 +1,18 @@ import html import json from pathlib import Path +from typing import Optional -from portabletext_html.renderer import render +import pytest + +from portabletext_html.renderer import MissingSerializerError, UnhandledNodeError, render +from portabletext_html.types import Block + + +def extraInfoSerializer(node: dict, context: Optional[Block], list_item: bool) -> str: + extraInfo = node.get('extraInfo') + + return f'{extraInfo}
' def load_fixture(fixture_name) -> dict: @@ -45,3 +55,22 @@ def test_nested_marks(): fixture = load_fixture('nested_marks.json') output = render(fixture) assert output == 'A word of warning; Sanity is addictive.
' + + +def test_missing_serializer(): + fixture = load_fixture('invalid_type.json') + with pytest.raises(MissingSerializerError): + render(fixture) + + +def test_invalid_node(): + fixture = load_fixture('invalid_node.json') + with pytest.raises(UnhandledNodeError): + render(fixture) + + +def test_custom_serializer_node_after_list(): + fixture = load_fixture('custom_serializer_node_after_list.json') + output = render(fixture, custom_serializers={'extraInfoBlock': extraInfoSerializer}) + + assert output == 'This informations is not supported by Block