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 10b4d9f..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.10b0
+ 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,17 +34,19 @@ repos:
'flake8-printf-formatting',
'flake8-type-checking',
]
+ args:
+ - '--allow-star-arg-any'
- repo: https://github.com/asottile/pyupgrade
- rev: v2.29.0
+ rev: v3.2.2
hooks:
- id: pyupgrade
args: [ "--py36-plus", "--py37-plus",'--keep-runtime-typing' ]
- repo: https://github.com/pycqa/isort
- rev: 5.10.0
+ rev: 5.10.1
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 b026c61..fd610a8 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,9 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-# Sanity HTML Renderer for Python
+[](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
This package generates HTML from [Portable Text](https://github.com/portabletext/portabletext).
@@ -20,19 +12,19 @@ For the most part, it mirrors [Sanity's](https://www.sanity.io/) own [block-cont
## Installation
```
-pip install sanity-html
+pip install portabletext-html
```
## Usage
-Instantiate the `SanityBlockRenderer` class with your content and call the `render` method.
+Instantiate the `PortableTextRenderer` class with your content and call the `render` method.
The following content
```python
-from sanity_html import SanityBlockRenderer
+from portabletext_html import PortableTextRenderer
-renderer = SanityBlockRenderer({
+renderer = PortableTextRenderer({
"_key": "R5FvMrjo",
"_type": "block",
"children": [
@@ -64,26 +56,26 @@ would like to.
To illustrate, if you passed this data to the renderer class:
```python
-from sanity_html import SanityBlockRenderer
-
-renderer = SanityBlockRenderer({
- "_type": "block",
- "_key": "foo",
- "style": "normal",
- "children": [
- {
- "_type": "span",
- "text": "Press, "
- },
- {
- "_type": "button",
- "text": "here"
- },
- {
- "_type": "span",
- "text": ", now!"
- }
- ]
+from portabletext_html import PortableTextRenderer
+
+renderer = PortableTextRenderer({
+ "_type": "block",
+ "_key": "foo",
+ "style": "normal",
+ "children": [
+ {
+ "_type": "span",
+ "text": "Press, "
+ },
+ {
+ "_type": "button",
+ "text": "here"
+ },
+ {
+ "_type": "span",
+ "text": ", now!"
+ }
+ ]
})
renderer.render()
```
@@ -94,12 +86,14 @@ does not have a corresponding built-in type serializer by default.
To render this text you must provide your own serializer, like this:
```python
-from sanity_html import SanityBlockRenderer
+from portabletext_html import PortableTextRenderer
+
def button_serializer(node: dict, context: Optional[Block], list_item: bool):
return f''
-renderer = SanityBlockRenderer(
+
+renderer = PortableTextRenderer(
...,
custom_serializers={'button': button_serializer}
)
@@ -136,9 +130,9 @@ Like with custom type serializers, additional serializers for
marker definitions and styles can be passed in like this:
```python
-from sanity_html import SanityBlockRenderer
+from portabletext_html import PortableTextRenderer
-renderer = SanityBlockRenderer(
+renderer = PortableTextRenderer(
...,
custom_marker_definitions={'em': ComicSansEmphasis}
)
@@ -152,7 +146,7 @@ Here's an example of a custom style, adding an extra font
to the built-in equivalent serializer:
```python
-from sanity_html.marker_definitions import MarkerDefinition
+from portabletext_html.marker_definitions import MarkerDefinition
class ComicSansEmphasis(MarkerDefinition):
@@ -166,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)
@@ -178,8 +177,9 @@ Since the `render_suffix` and `render` methods here are actually identical to th
they do not need to be specified, and the whole example can be reduced to:
```python
-from sanity_html.marker_definitions import MarkerDefinition # base
-from sanity_html import SanityBlockRenderer
+from portabletext_html.marker_definitions import MarkerDefinition # base
+from portabletext_html import PortableTextRenderer
+
class ComicSansEmphasis(MarkerDefinition):
tag = 'em'
@@ -189,7 +189,7 @@ class ComicSansEmphasis(MarkerDefinition):
return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">'
-renderer = SanityBlockRenderer(
+renderer = PortableTextRenderer(
...,
custom_marker_definitions={'em': ComicSansEmphasis}
)
@@ -220,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 15e5029..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.1"
+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 (>=4.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"
@@ -88,14 +88,14 @@ python-versions = "*"
[[package]]
name = "packaging"
-version = "21.2"
+version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
-pyparsing = ">=2.0.2,<3"
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "pluggy"
@@ -114,11 +114,11 @@ testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
-version = "1.10.0"
+version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pycodestyle"
@@ -138,11 +138,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyparsing"
-version = "2.4.7"
-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 = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pytest"
@@ -180,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"
@@ -192,23 +195,23 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "typing-extensions"
-version = "3.10.0.2"
-description = "Backported and Experimental Type Hints for Python 3.5+"
+version = "4.4.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
category = "dev"
optional = false
-python-versions = "*"
+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"
@@ -217,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"},
@@ -287,8 +289,8 @@ flake8 = [
{file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
]
importlib-metadata = [
- {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
- {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
+ {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"},
@@ -299,16 +301,16 @@ mccabe = [
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
packaging = [
- {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"},
- {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"},
+ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
- {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
- {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pycodestyle = [
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
@@ -319,8 +321,8 @@ pyflakes = [
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
]
pyparsing = [
- {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
- {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+ {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"},
@@ -335,11 +337,10 @@ toml = [
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typing-extensions = [
- {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
- {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
- {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
+ {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/__init__.py b/portabletext_html/__init__.py
new file mode 100644
index 0000000..bd2f0d5
--- /dev/null
+++ b/portabletext_html/__init__.py
@@ -0,0 +1,3 @@
+from portabletext_html.renderer import PortableTextRenderer, render
+
+__all__ = ['PortableTextRenderer', 'render']
diff --git a/sanity_html/constants.py b/portabletext_html/constants.py
similarity index 88%
rename from sanity_html/constants.py
rename to portabletext_html/constants.py
index bc73e2d..f5ead44 100644
--- a/sanity_html/constants.py
+++ b/portabletext_html/constants.py
@@ -2,7 +2,7 @@
from typing import TYPE_CHECKING
-from sanity_html.marker_definitions import (
+from portabletext_html.marker_definitions import (
CodeMarkerDefinition,
CommentMarkerDefinition,
EmphasisMarkerDefinition,
@@ -15,7 +15,7 @@
if TYPE_CHECKING:
from typing import Dict, Type
- from sanity_html.marker_definitions import MarkerDefinition
+ from portabletext_html.marker_definitions import MarkerDefinition
STYLE_MAP = {
'h1': 'h1',
diff --git a/sanity_html/logger.py b/portabletext_html/logger.py
similarity index 85%
rename from sanity_html/logger.py
rename to portabletext_html/logger.py
index 7d9ea67..18122b3 100644
--- a/sanity_html/logger.py
+++ b/portabletext_html/logger.py
@@ -6,7 +6,7 @@
"""
import logging
-logger = logging.getLogger('sanity_html')
+logger = logging.getLogger('portabletext_html')
if not logger.handlers: # pragma: no cover
logger.setLevel(logging.WARNING)
diff --git a/sanity_html/marker_definitions.py b/portabletext_html/marker_definitions.py
similarity index 90%
rename from sanity_html/marker_definitions.py
rename to portabletext_html/marker_definitions.py
index 7a7bb05..396f4e1 100644
--- a/sanity_html/marker_definitions.py
+++ b/portabletext_html/marker_definitions.py
@@ -2,12 +2,12 @@
from typing import TYPE_CHECKING
-from sanity_html.logger import logger
+from portabletext_html.logger import logger
if TYPE_CHECKING:
from typing import Type
- from sanity_html.types import Block, Span
+ from portabletext_html.types import Block, Span
class MarkerDefinition:
@@ -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/sanity_html/py.typed b/portabletext_html/py.typed
similarity index 100%
rename from sanity_html/py.typed
rename to portabletext_html/py.typed
diff --git a/sanity_html/renderer.py b/portabletext_html/renderer.py
similarity index 84%
rename from sanity_html/renderer.py
rename to portabletext_html/renderer.py
index 67c2577..1ca2ce3 100644
--- a/sanity_html/renderer.py
+++ b/portabletext_html/renderer.py
@@ -3,16 +3,16 @@
import html
from typing import TYPE_CHECKING, cast
-from sanity_html.constants import STYLE_MAP
-from sanity_html.logger import logger
-from sanity_html.marker_definitions import DefaultMarkerDefinition
-from sanity_html.types import Block, Span
-from sanity_html.utils import get_list_tags, is_block, is_list, is_span
+from portabletext_html.constants import STYLE_MAP
+from portabletext_html.logger import logger
+from portabletext_html.marker_definitions import DefaultMarkerDefinition
+from portabletext_html.types import Block, Span
+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 sanity_html.marker_definitions import MarkerDefinition
+ from portabletext_html.marker_definitions import MarkerDefinition
class UnhandledNodeError(Exception):
@@ -32,14 +32,14 @@ class MissingSerializerError(UnhandledNodeError):
pass
-class SanityBlockRenderer:
- """HTML renderer for Sanity block content."""
+class PortableTextRenderer:
+ """HTML renderer for Sanity's portable text format."""
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 = SanityBlockRenderer(blocks, *args, **kwargs)
+ renderer = PortableTextRenderer(blocks, *args, **kwargs)
return renderer.render()
diff --git a/sanity_html/types.py b/portabletext_html/types.py
similarity index 80%
rename from sanity_html/types.py
rename to portabletext_html/types.py
index c24f165..898d61e 100644
--- a/sanity_html/types.py
+++ b/portabletext_html/types.py
@@ -1,14 +1,14 @@
from __future__ import annotations
from dataclasses import dataclass, field
-from typing import TYPE_CHECKING, cast
+from typing import TYPE_CHECKING
-from sanity_html.utils import get_default_marker_definitions
+from portabletext_html.utils import get_default_marker_definitions
if TYPE_CHECKING:
from typing import Literal, Optional, Tuple, Type, Union
- from sanity_html.marker_definitions import MarkerDefinition
+ from portabletext_html.marker_definitions import MarkerDefinition
@dataclass(frozen=True)
@@ -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/sanity_html/utils.py b/portabletext_html/utils.py
similarity index 90%
rename from sanity_html/utils.py
rename to portabletext_html/utils.py
index d0afec7..3977d81 100644
--- a/sanity_html/utils.py
+++ b/portabletext_html/utils.py
@@ -2,12 +2,12 @@
from typing import TYPE_CHECKING
-from sanity_html.constants import ANNOTATION_MARKER_DEFINITIONS, DECORATOR_MARKER_DEFINITIONS
+from portabletext_html.constants import ANNOTATION_MARKER_DEFINITIONS, DECORATOR_MARKER_DEFINITIONS
if TYPE_CHECKING:
from typing import Type
- from sanity_html.marker_definitions import MarkerDefinition
+ from portabletext_html.marker_definitions import MarkerDefinition
def get_default_marker_definitions(mark_defs: list[dict]) -> dict[str, Type[MarkerDefinition]]:
diff --git a/pyproject.toml b/pyproject.toml
index 15152a2..7dc039a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
-name = 'sanity-html'
-version = '1.0.0'
+name = 'portabletext-html'
+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'
@@ -10,7 +10,7 @@ license = 'Apache2'
readme = 'README.md'
keywords = ['sanity', 'portable', 'text', 'html', 'parsing']
include = ['CHANGELOG.md']
-packages = [{ include = 'sanity_html' }]
+packages = [{ include = 'portabletext_html' }]
classifiers = [
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
@@ -21,8 +21,11 @@ classifiers = [
'Topic :: Text Processing :: Markup',
'Topic :: Text Processing :: Markup :: HTML',
'Programming Language :: Python',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
'Typing :: Typed',
]
@@ -51,11 +54,11 @@ include_trailing_comma = true
line_length = 120
[tool.pytest.ini_options]
-addopts = ['--cov=sanity_html','--cov-report', 'term-missing']
+addopts = ['--cov=portabletext_html','--cov-report', 'term-missing']
markers = ['unsupported']
[tool.coverage.run]
-source = ['sanity_html/*']
+source = ['portabletext_html/*']
omit = []
branch = true
diff --git a/sanity_html/__init__.py b/sanity_html/__init__.py
deleted file mode 100644
index 879ae73..0000000
--- a/sanity_html/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Python Sanity HTML Renderer."""
-
-from sanity_html.renderer import SanityBlockRenderer, render
-
-__all__ = ['SanityBlockRenderer', 'render']
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 0a2577c..2677218 100644
--- a/tests/test_marker_definitions.py
+++ b/tests/test_marker_definitions.py
@@ -1,5 +1,8 @@
-from sanity_html import SanityBlockRenderer
-from sanity_html.marker_definitions import (
+# pylint: skip-file
+from typing import Type
+
+from portabletext_html import PortableTextRenderer
+from portabletext_html.marker_definitions import (
CommentMarkerDefinition,
EmphasisMarkerDefinition,
LinkMarkerDefinition,
@@ -7,7 +10,7 @@
StrongMarkerDefinition,
UnderlineMarkerDefinition,
)
-from sanity_html.types import Block, Span
+from portabletext_html.types import Block, Span
sample_texts = ['test', None, 1, 2.2, '!"#$%&/()']
@@ -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}'
@@ -60,21 +68,34 @@ def test_render_comment_marker_success():
def test_custom_marker_definition():
- from sanity_html.marker_definitions import MarkerDefinition
+ 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 = SanityBlockRenderer(
- {
+ 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_module_loading.py b/tests/test_module_loading.py index d1a6523..03fe074 100644 --- a/tests/test_module_loading.py +++ b/tests/test_module_loading.py @@ -6,6 +6,6 @@ def test_module_should_be_importable(): This catches any compilation issue we might have. """ - from sanity_html import SanityBlockRenderer + from portabletext_html import PortableTextRenderer - assert SanityBlockRenderer + assert PortableTextRenderer diff --git a/tests/test_rendering.py b/tests/test_rendering.py index a9eaf8d..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 sanity_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