diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
deleted file mode 100644
index 2cf3ca0..0000000
--- a/.github/workflows/codecov.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: coverage
-
-on:
- pull_request:
- push:
- branches:
- - main
-
-jobs:
- codecov:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
- with:
- python-version: 3.9
- - name: Install poetry
- uses: snok/install-poetry@v1.1.2
- with:
- virtualenvs-create: true
- virtualenvs-in-project: true
- - uses: actions/cache@v2
- id: cache-venv
- with:
- path: .venv
- key: ${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}-0
- - run: poetry install --no-interaction --no-root
- if: steps.cache-venv.outputs.cache-hit != 'true'
- - run: poetry install --no-interaction
- - run: poetry run pytest -m "not unsupported" --cov-report=xml
- - uses: codecov/codecov-action@v1
- with:
- file: ./coverage.xml
- fail_ci_if_error: true
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 7bf7c2f..fd2ef12 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -13,7 +13,7 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: 3.9
- - uses: snok/install-poetry@v1.1.2
+ - uses: snok/install-poetry@v1.1.6
- name: Publish to test-pypi
run: |
poetry config repositories.test https://test.pypi.org/legacy/
@@ -28,7 +28,7 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: 3.9
- - uses: snok/install-poetry@v1.1.2
+ - uses: snok/install-poetry@v1.1.6
- name: Publish to pypi
run: |
poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 397a32d..22b34ce 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,49 +13,63 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
- python-version: 3.9
+ python-version: "3.10"
- uses: actions/cache@v2
+ id: cache-venv
with:
- path: |
- ~/.cache/pip
- ~/.cache/pre-commit
- key: ${{ runner.os }}-pip-2
- restore-keys: |
- ${{ runner.os }}-pip-
- ${{ runner.os }}-
- - run: python -m pip install pre-commit
- - run: pre-commit run --all-files
+ path: .venv
+ key: venv-1
+ - run: |
+ python -m venv .venv --upgrade-deps
+ source .venv/bin/activate
+ pip install pre-commit
+ if: steps.cache-venv.outputs.cache-hit != 'true'
+ - uses: actions/cache@v2
+ id: pre-commit-cache
+ with:
+ path: ~/.cache/pre-commit
+ key: key-1
+ - run: |
+ source .venv/bin/activate
+ pre-commit run --all-files
+
test:
- needs: linting
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
- python-version: ["3.9"]
+ python-version: [ "3.7.14", "3.8.14", "3.9.15", "3.10.8", "3.11.0", "3.12.0-alpha.1" ]
steps:
- - name: Check out repository
- uses: actions/checkout@v2
- - name: Set up python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
- with:
- python-version: ${{ matrix.python-version }}
- - name: Install poetry
- uses: snok/install-poetry@v1.1.2
- with:
- virtualenvs-create: true
- virtualenvs-in-project: true
- - name: Load cached venv
- uses: actions/cache@v2
+ - uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: "${{ matrix.python-version }}"
+ - uses: actions/cache@v2
+ id: poetry-cache
+ with:
+ path: ~/.local
+ key: key-2
+ - uses: snok/install-poetry@v1
+ with:
+ virtualenvs-create: false
+ - uses: actions/cache@v2
id: cache-venv
with:
path: .venv
- key: ${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}-3
- - name: Install dependencies
- run: poetry install --no-interaction --no-root
+ key: ${{ hashFiles('**/poetry.lock') }}-1
+ - run: |
+ python -m venv .venv
+ source .venv/bin/activate
+ pip install -U pip wheel
+ poetry install --no-interaction --no-root
if: steps.cache-venv.outputs.cache-hit != 'true'
- - name: Install package
- run: poetry install --no-interaction
- name: Run tests
run: |
source .venv/bin/activate
- poetry run pytest -m "not unsupported"
+ pytest -m "not unsupported" --cov-report=xml
+ coverage report
+ - uses: codecov/codecov-action@v2
+ with:
+ file: ./coverage.xml
+ fail_ci_if_error: true
+ if: matrix.python-version == '3.10'
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 83ec612..6493166 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,11 +1,11 @@
repos:
- repo: https://github.com/ambv/black
- rev: 20.8b1
+ rev: 22.10.0
hooks:
- id: black
args: [ "--quiet" ]
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v3.4.0
+ 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.0
+ - repo: https://github.com/pycqa/flake8
+ rev: 5.0.4
hooks:
- id: flake8
additional_dependencies: [
@@ -34,17 +34,20 @@ repos:
'flake8-printf-formatting',
'flake8-type-checking',
]
+ args:
+ - '--allow-star-arg-any'
- repo: https://github.com/asottile/pyupgrade
- rev: v2.12.0
+ rev: v3.2.2
hooks:
- id: pyupgrade
args: [ "--py36-plus", "--py37-plus",'--keep-runtime-typing' ]
- repo: https://github.com/pycqa/isort
- rev: 5.8.0
+ rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.812
+ rev: v0.991
hooks:
- id: mypy
- additional_dependencies: [ pytest ]
+ additional_dependencies:
+ - types-requests
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index e69de29..0000000
diff --git a/LICENSE.txt b/LICENSE
similarity index 100%
rename from LICENSE.txt
rename to LICENSE
diff --git a/README.md b/README.md
index bb9e9a7..fd610a8 100644
--- a/README.md
+++ b/README.md
@@ -1,52 +1,223 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+[](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/)
-# Python Sanity HTML Renderer
+# Portable Text HTML Renderer for Python
-> Repo is currently a work in progress. Not ready to be used.
+This package generates HTML from [Portable Text](https://github.com/portabletext/portabletext).
-HTML renderer for [Sanity's](https://www.sanity.io/) [Portable Text](https://github.com/portabletext/portabletext) format.
+For the most part, it mirrors [Sanity's](https://www.sanity.io/) own [block-content-to-html](https://www.npmjs.com/package/%40sanity/block-content-to-html) NPM library.
-Written as a python alternative to [Sanity's](https://www.sanity.io/) [block-content-to-html](https://www.npmjs.com/package/%40sanity/block-content-to-html) npm package,
-for when you don't have access to a JavaScript runtime.
+## Installation
+
+```
+pip install portabletext-html
+```
-### TODO
+## Usage
-- [ ] Add support for image type
+Instantiate the `PortableTextRenderer` class with your content and call the `render` method.
-## Installation
+The following content
+```python
+from portabletext_html import PortableTextRenderer
+
+renderer = PortableTextRenderer({
+ "_key": "R5FvMrjo",
+ "_type": "block",
+ "children": [
+ {"_key": "cZUQGmh4", "_type": "span", "marks": ["strong"], "text": "A word of"},
+ {"_key": "toaiCqIK", "_type": "span", "marks": ["strong"], "text": " warning;"},
+ {"_key": "gaZingsA", "_type": "span", "marks": [], "text": " Sanity is addictive."}
+ ],
+ "markDefs": [],
+ "style": "normal"
+})
+renderer.render()
```
-pip install python-sanity-html
+
+Generates this HTML
+```html
+
A word of warning; Sanity is addictive.
``` -## Usage +### Supported types + +The `block` and `span` types are supported out of the box. + +### Custom types + +Beyond the built-in types, you have the freedom to provide +your own serializers to render any custom `_type` the way you +would like to. + +To illustrate, if you passed this data to the renderer class: + +```python +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() +``` + +The renderer would actually throw an error here, since `button` +does not have a corresponding built-in type serializer by default. -To parse your block content as HTML, simply instantiate the parser like this +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(block_content) +renderer = PortableTextRenderer( + ..., + custom_serializers={'button': button_serializer} +) output = renderer.render() ``` +With the custom serializer provided, the renderer would now successfully +output the following HTML: + +```html +Press , now!
+``` + +### Supported mark definitions + +The package provides several built-in marker definitions and styles: + +**decorator marker definitions** + +- `em` +- `strong` +- `code` +- `underline` +- `strike-through` + +**annotation marker definitions** + +- `link` +- `comment` + +### Custom mark definitions + +Like with custom type serializers, additional serializers for +marker definitions and styles can be passed in like this: + +```python +from portabletext_html import PortableTextRenderer + +renderer = PortableTextRenderer( + ..., + custom_marker_definitions={'em': ComicSansEmphasis} +) +renderer.render() +``` + +The primary difference between a type serializer and a mark definition serializer +is that the latter uses a class structure, and has three required methods. + +Here's an example of a custom style, adding an extra font +to the built-in equivalent serializer: + +```python +from portabletext_html.marker_definitions import MarkerDefinition + + +class ComicSansEmphasis(MarkerDefinition): + tag = 'em' + + @classmethod + def render_prefix(cls, span: Span, marker: str, context: Block) -> str: + return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">' + + @classmethod + 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) + result += str(span.text) + result += cls.render_suffix(span, marker, context) + return result +``` + +Since the `render_suffix` and `render` methods here are actually identical to the base class, +they do not need to be specified, and the whole example can be reduced to: + +```python +from portabletext_html.marker_definitions import MarkerDefinition # base +from portabletext_html import PortableTextRenderer + + +class ComicSansEmphasis(MarkerDefinition): + tag = 'em' + + @classmethod + def render_prefix(cls, span: Span, marker: str, context: Block) -> str: + return f'<{cls.tag} style="font-family: "Comic Sans MS", "Comic Sans", cursive;">' + + +renderer = PortableTextRenderer( + ..., + custom_marker_definitions={'em': ComicSansEmphasis} +) +renderer.render() +``` + + +### Supported styles + +Blocks can optionally define a `style` tag. These styles are supported: + +- `h1` +- `h2` +- `h3` +- `h4` +- `h5` +- `h6` +- `blockquote` +- `normal` + +## Missing features + +For anyone interested, we would be happy to see a +default built-in serializer for the `image` type added. +In the meantime, users should be able to serialize image types by passing a custom serializer. + ## Contributing 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 53f8891..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 = "20.3.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.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] +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" @@ -41,17 +41,35 @@ toml = ["toml"] [[package]] name = "flake8" -version = "3.9.0" +version = "3.9.2" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" +[[package]] +name = "importlib-metadata" +version = "5.0.0" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +perf = ["ipython"] +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" version = "1.1.1" @@ -70,33 +88,37 @@ python-versions = "*" [[package]] name = "packaging" -version = "20.9" +version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pluggy" -version = "0.13.1" +version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +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" @@ -116,15 +138,18 @@ 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" -version = "6.2.3" +version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -134,9 +159,10 @@ python-versions = ">=3.6" atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0.0a1" +pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" @@ -145,7 +171,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-cov" -version = "2.11.1" +version = "2.12.1" description = "Pytest plugin for measuring coverage." category = "dev" optional = false @@ -154,9 +180,10 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] coverage = ">=5.2.1" pytest = ">=4.6" +toml = "*" [package.extras] -testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "toml" @@ -166,23 +193,42 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "zipp" +version = "3.10.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +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" -python-versions = "^3.9" -content-hash = "031b6c95c5355805b7c59e9aef1a159db3e8339edd40c50292ae2ae81e7bf191" +python-versions = '^3.7' +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-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, - {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, + {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"}, @@ -239,8 +285,12 @@ coverage = [ {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] flake8 = [ - {file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"}, - {file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"}, + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +importlib-metadata = [ + {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"}, @@ -251,16 +301,16 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {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"}, @@ -271,18 +321,26 @@ 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.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"}, - {file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"}, + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-cov = [ - {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, - {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +typing-extensions = [ + {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.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/portabletext_html/logger.py b/portabletext_html/logger.py new file mode 100644 index 0000000..18122b3 --- /dev/null +++ b/portabletext_html/logger.py @@ -0,0 +1,13 @@ +""" +Logging setup. + +The rest of the code gets the logger through this module rather than +`logging.getLogger` to make sure that it is configured. +""" +import logging + +logger = logging.getLogger('portabletext_html') + +if not logger.handlers: # pragma: no cover + logger.setLevel(logging.WARNING) + logger.addHandler(logging.NullHandler()) diff --git a/sanity_html/marker_definitions.py b/portabletext_html/marker_definitions.py similarity index 83% rename from sanity_html/marker_definitions.py rename to portabletext_html/marker_definitions.py index 5947160..396f4e1 100644 --- a/sanity_html/marker_definitions.py +++ b/portabletext_html/marker_definitions.py @@ -2,10 +2,12 @@ from typing import TYPE_CHECKING +from portabletext_html.logger import logger + if TYPE_CHECKING: from typing import Type - from sanity_html.dataclasses import Block, Span + from portabletext_html.types import Block, Span class MarkerDefinition: @@ -19,6 +21,7 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Usually this this the opening of the HTML tag. """ + logger.debug('Rendering %s prefix', cls.tag) return f'<{cls.tag}>' @classmethod @@ -27,16 +30,22 @@ def render_suffix(cls: Type[MarkerDefinition], span: Span, marker: str, context: Usually this this the closing of the HTML tag. """ + logger.debug('Rendering %s suffix', cls.tag) return f'{cls.tag}>' @classmethod 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 @@ -97,10 +106,10 @@ def render_prefix(cls: Type[MarkerDefinition], span: Span, marker: str, context: The href attribute is fetched from the provided block context using the provided marker key. """ - marker_defintion = next((md for md in context.markDefs if md['_key'] == marker), None) - if not marker_defintion: + 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') - href = marker_defintion.get('href', '') + href = marker_definition.get('href', '') return f'' 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 65% rename from sanity_html/renderer.py rename to portabletext_html/renderer.py index bfdfd8e..1ca2ce3 100644 --- a/sanity_html/renderer.py +++ b/portabletext_html/renderer.py @@ -1,32 +1,47 @@ from __future__ import annotations import html -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast -from sanity_html.constants import STYLE_MAP -from sanity_html.dataclasses import Block, Span -from sanity_html.marker_definitions import DefaultMarkerDefinition -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 -# TODO: Let user pass custom code block definitions/plugins -# to represent custom types (see children definition in portable text spec) +class UnhandledNodeError(Exception): + """Raised when we receive a node that we cannot parse.""" + pass -class SanityBlockRenderer: - """HTML renderer for Sanity block content.""" + +class MissingSerializerError(UnhandledNodeError): + """ + Raised when an unrecognized node _type value is found. + + This usually means that you need to pass a custom serializer + to handle the custom type. + """ + + pass + + +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 self._custom_marker_definitions = custom_marker_definitions or {} self._custom_serializers = custom_serializers or {} @@ -39,16 +54,19 @@ def __init__( def render(self) -> str: """Render HTML from self._blocks.""" + logger.debug('Rendering HTML') + if not self._blocks: 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, Block(**node)) - result += ''.join([self._render_node(n, Block(**node), list_item=True) for n in tree]) + tree = self._normalize_list_tree(list_nodes) + result += ''.join([self._render_node(n, list_item=True) for n in tree]) list_nodes = [] # reset list_nodes if is_list(node): @@ -58,11 +76,8 @@ def render(self) -> str: result += self._render_node(node) # render non-list nodes immediately if list_nodes: - tree = self._normalize_list_tree(list_nodes, Block(**node)) - result += ''.join( - self._render_node(n, Block(**node), list_item=True) for n in tree - ) - + tree = self._normalize_list_tree(list_nodes) + result += ''.join(self._render_node(n, Block(**node), list_item=True) for n in tree) result = result.strip() @@ -79,45 +94,51 @@ def _render_node(self, node: dict, context: Optional[Block] = None, list_item: b :param list_item: Whether we are handling a list upstream (impacts block handling). """ if is_list(node): + logger.debug('Rendering node as list') block = Block(**node, marker_definitions=self._custom_marker_definitions) return self._render_list(block, context) + elif is_block(node): + logger.debug('Rendering node as block') block = Block(**node, marker_definitions=self._custom_marker_definitions) return self._render_block(block, list_item=list_item) 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) + logger.debug('Rendering node as span') + span = Span(**node) + 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 - assert context # this should be a cast - return self._render_span(span, block=context) # context is span's outer block - elif custom_serializer := self._custom_serializers.get(node.get('_type', '')): - return custom_serializer(node, context, list_item) else: - print('Unexpected code path 👺') # noqa: T001 # TODO: Remove after thorough testing - return '' + if '_type' in node: + raise MissingSerializerError( + f'Found unhandled node type: {node["_type"]}. ' 'Most likely this requires a custom serializer.' + ) + else: + raise UnhandledNodeError(f'Received node that we cannot handle: {node}') def _render_block(self, block: Block, list_item: bool = False) -> str: - text = '' - if not list_item: - tag = STYLE_MAP[block.style] + text, tag = '', STYLE_MAP[block.style] + + if not list_item or tag != 'p': text += f'<{tag}>' - for child_node in block.children: - text += self._render_node(child_node, context=block) + for child_node in block.children: + text += self._render_node(child_node, context=block) + + if not list_item or tag != 'p': text += f'{tag}>' - else: - for child_node in block.children: - text += self._render_node(child_node, context=block) + return text def _render_span(self, span: Span, block: Block) -> str: + logger.debug('Rendering span') 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 [] @@ -125,14 +146,28 @@ def _render_span(self, span: Span, block: Block) -> str: 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', 'A word of warning; Sanity is addictive.
"} +{ + "input": { + "_key": "R5FvMrjo", + "_type": "block", + "children": [ + { + "_key": "cZUQGmh4", + "_type": "span", + "marks": [ + "strong" + ], + "text": "A word of" + }, + { + "_key": "toaiCqIK", + "_type": "span", + "marks": [ + "strong" + ], + "text": " warning;" + }, + { + "_key": "gaZingA", + "_type": "span", + "marks": [], + "text": " Sanity is addictive." + } + ], + "markDefs": [], + "style": "normal" + }, + "output": "A word of warning; Sanity is addictive.
" +} diff --git a/tests/fixtures/upstream/012-image-support.json b/tests/fixtures/upstream/012-image-support.json index 6197ecd..5d2d958 100644 --- a/tests/fixtures/upstream/012-image-support.json +++ b/tests/fixtures/upstream/012-image-support.json @@ -1 +1,26 @@ -{"input":[{"style":"normal","_type":"block","_key":"bd73ec5f61a1","markDefs":[],"children":[{"_type":"span","text":"Also, images are pretty common.","marks":[]}]},{"_type":"image","_key":"d234a4fa317a","asset":{"_type":"reference","_ref":"image-YiOKD0O6AdjKPaK24WtbOEv0-3456x2304-jpg"}}],"output":"Also, images are pretty common.
Also, images are pretty common.
The collection may be examined by arranging a research appointment in advance by contacting the ACT archivist by email or phone. ACT generally does not accept walk-in research patrons, although requests may be made in person at the Archivist’s office (E15-222). ACT recommends arranging appointments at least three weeks in advance in order to ensure availability. ACT reserves the right to cancel research appointments at any time. Appointment scheduling is subject to institute holidays and closings.
The collection space is located at:
20 Ames Street
Building E15-235
Cambridge, Massachusetts 02139
For patrons who are unable to arrange for an on-campus visit to the Archives and Special Collections, reference questions may be directed to the Archivist remotely by email or phone. Generally, emails and phone calls will receive a response within 72 hours of receipt. Requests are typically filled in the order they are received.
Patrons requesting collection materials in person or remotely may be asked to provide certain information to the Archivist, such as contact information and topic(s) of research. This information is only used to track requests for statistical evaluations of collection use and will not be disclosed to outside organizations for any purpose. ACT will endeavor to protect the privacy of all patrons accessing collections.
ACT reserves the right to charge an hourly rate for requests that require more than three hours of research on behalf of a patron (remote requests). Collection materials may be scanned and made available upon request, but digitization of certain materials may incur costs. Additionally, requests to publish, exhibit, or otherwise reproduce and display collection materials may incur use fees.
Permission to examine collection materials in person or remotely (by receiving transfers of digitized materials) does not imply or grant permission to publish or exhibit those materials. Permission to publish, exhibit, or otherwise use collection materials is granted on a case by case basis in accordance with MIT policy, restrictions that may have been placed on materials by donors or depositors, and copyright law. To request permission to publish, exhibit, or otherwise use collection materials, contact the Archivist. When permission is granted by MIT, patrons must comply with all guidelines provided by ACT for citations, credits, and copyright statements. Exclusive rights to examine or publish material will not be granted.
The collection may be examined by arranging a research appointment in advance by contacting the ACT archivist by email or phone. ACT generally does not accept walk-in research patrons, although requests may be made in person at the Archivist’s office (E15-222). ACT recommends arranging appointments at least three weeks in advance in order to ensure availability. ACT reserves the right to cancel research appointments at any time. Appointment scheduling is subject to institute holidays and closings.
The collection space is located at:
20 Ames Street
Building E15-235
Cambridge, Massachusetts 02139
For patrons who are unable to arrange for an on-campus visit to the Archives and Special Collections, reference questions may be directed to the Archivist remotely by email or phone. Generally, emails and phone calls will receive a response within 72 hours of receipt. Requests are typically filled in the order they are received.
Patrons requesting collection materials in person or remotely may be asked to provide certain information to the Archivist, such as contact information and topic(s) of research. This information is only used to track requests for statistical evaluations of collection use and will not be disclosed to outside organizations for any purpose. ACT will endeavor to protect the privacy of all patrons accessing collections.
ACT reserves the right to charge an hourly rate for requests that require more than three hours of research on behalf of a patron (remote requests). Collection materials may be scanned and made available upon request, but digitization of certain materials may incur costs. Additionally, requests to publish, exhibit, or otherwise reproduce and display collection materials may incur use fees.
Permission to examine collection materials in person or remotely (by receiving transfers of digitized materials) does not imply or grant permission to publish or exhibit those materials. Permission to publish, exhibit, or otherwise use collection materials is granted on a case by case basis in accordance with MIT policy, restrictions that may have been placed on materials by donors or depositors, and copyright law. To request permission to publish, exhibit, or otherwise use collection materials, contact the Archivist. When permission is granted by MIT, patrons must comply with all guidelines provided by ACT for citations, credits, and copyright statements. Exclusive rights to examine or publish material will not be granted.
Men, , da!
"} +{ + "input": [ + { + "_type": "block", + "_key": "foo", + "style": "normal", + "children": [ + { + "_type": "span", + "text": "Men, " + }, + { + "_type": "button", + "text": "bli med du også" + }, + { + "_type": "span", + "text": ", da!" + } + ] + } + ], + "output": "Men, , da!
" +} diff --git a/tests/fixtures/upstream/027-styled-list-items.json b/tests/fixtures/upstream/027-styled-list-items.json index 2ffdfc6..7b8d93b 100644 --- a/tests/fixtures/upstream/027-styled-list-items.json +++ b/tests/fixtures/upstream/027-styled-list-items.json @@ -1 +1,63 @@ -{"input":[{"style":"normal","_type":"block","_key":"f94596b05b41","markDefs":[],"children":[{"_type":"span","text":"Let's test some of these lists!","marks":[]}]},{"listItem":"bullet","style":"normal","level":1,"_type":"block","_key":"937effb1cd06","markDefs":[],"children":[{"_type":"span","text":"Bullet 1","marks":[]}]},{"listItem":"bullet","style":"h1","level":1,"_type":"block","_key":"bd2d22278b88","markDefs":[],"children":[{"_type":"span","text":"Bullet 2","marks":[]}]},{"listItem":"bullet","style":"normal","level":1,"_type":"block","_key":"a97d32e9f747","markDefs":[],"children":[{"_type":"span","text":"Bullet 3","marks":[]}]}],"output":"Let's test some of these lists!
Let's test some of these lists!
Sanity
"} +{ + "input": { + "_type": "block", + "children": [ + { + "_key": "a1ph4", + "_type": "span", + "marks": [ + "mark1" + ], + "text": "Sanity" + } + ], + "markDefs": [ + { + "_key": "mark1", + "_type": "highlight", + "thickness": 5 + } + ] + }, + "output": "Sanity
" +} diff --git a/tests/test_marker_definitions.py b/tests/test_marker_definitions.py index 572d003..2677218 100644 --- a/tests/test_marker_definitions.py +++ b/tests/test_marker_definitions.py @@ -1,5 +1,8 @@ -from sanity_html.dataclasses import Block, Span -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,6 +10,7 @@ StrongMarkerDefinition, UnderlineMarkerDefinition, ) +from portabletext_html.types import Block, Span sample_texts = ['test', None, 1, 2.2, '!"#$%&/()'] @@ -15,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}' @@ -22,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}' @@ -29,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}' @@ -39,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'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