collective.html2blocks 1.0.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- collective.html2blocks-1.0.0a1/.gitignore +18 -0
- collective.html2blocks-1.0.0a1/PKG-INFO +47 -0
- collective.html2blocks-1.0.0a1/README.md +20 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/__init__.py +2 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/_types.py +119 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/blocks/__init__.py +56 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/blocks/iframe/__init__.py +31 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/blocks/iframe/soundcloud.py +25 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/blocks/iframe/youtube.py +32 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/blocks/image.py +112 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/blocks/slate/__init__.py +69 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/blocks/slate/parser.py +530 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/blocks/table.py +136 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/blocks/video.py +50 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/cli.py +62 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/commands/__init__.py +4 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/commands/convert.py +95 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/commands/info.py +53 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/commands/server.py +45 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/converter.py +93 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/logger.py +20 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/registry.py +322 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/services/__init__.py +63 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/services/healthcheck.py +33 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/services/html.py +103 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/services/info.py +42 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/utils/__init__.py +1 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/utils/blocks.py +63 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/utils/generator.py +59 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/utils/inline.py +52 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/utils/markup.py +490 -0
- collective.html2blocks-1.0.0a1/collective/html2blocks/utils/slate.py +405 -0
- collective.html2blocks-1.0.0a1/pyproject.toml +169 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: collective.html2blocks
|
|
3
|
+
Version: 1.0.0a1
|
|
4
|
+
Summary: Convert html to blocks to be used by Volto
|
|
5
|
+
Project-URL: Homepage, https://github.com/collective/collective.html2blocks#readme
|
|
6
|
+
Project-URL: Changelog, https://github.com/collective/collective.html2blocks/blob/main/CHANGELOG.md
|
|
7
|
+
Project-URL: Issues, https://github.com/collective/collective.html2blocks/issues
|
|
8
|
+
Project-URL: Source, https://github.com/collective/collective.html2blocks
|
|
9
|
+
Project-URL: Repository, https://github.com/collective/collective.html2blocks
|
|
10
|
+
Author-email: Érico Andrei <ericof@plone.org>
|
|
11
|
+
Keywords: Plone,Python,Volto
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Framework :: Plone
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: beautifulsoup4>=4.13.3
|
|
23
|
+
Requires-Dist: fastapi[standard]>=0.116.1
|
|
24
|
+
Requires-Dist: python-slugify>=8.0.4
|
|
25
|
+
Requires-Dist: typer>=0.15.1
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
<h1 align="center">collective.html2blocks</h1>
|
|
29
|
+
|
|
30
|
+
<div align="center">
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
33
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
34
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
35
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
36
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
[](https://github.com/collective/collective.html2blocks/actions/workflows/qa.yml)
|
|
40
|
+
[](https://github.com/collective/collective.html2blocks/actions/workflows/test.yml)
|
|
41
|
+
|
|
42
|
+
[](https://github.com/collective/collective.html2blocks)
|
|
43
|
+
[](https://github.com/collective/collective.html2blocks)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
**collective.html2blocks** helps with migrations from old Plone versions -- and other CMS -- to Volto.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<h1 align="center">collective.html2blocks</h1>
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
6
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
7
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
8
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
9
|
+
[](https://pypi.org/project/collective.html2blocks/)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
[](https://github.com/collective/collective.html2blocks/actions/workflows/qa.yml)
|
|
13
|
+
[](https://github.com/collective/collective.html2blocks/actions/workflows/test.yml)
|
|
14
|
+
|
|
15
|
+
[](https://github.com/collective/collective.html2blocks)
|
|
16
|
+
[](https://github.com/collective/collective.html2blocks)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
**collective.html2blocks** helps with migrations from old Plone versions -- and other CMS -- to Volto.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from bs4 import Tag
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import NotRequired
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
from typing import TypedDict
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
BaseVoltoBlock = TypedDict("BaseVoltoBlock", {"@type": str})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VoltoBlock(BaseVoltoBlock, total=False):
|
|
17
|
+
"""Volto Block type with optional fields."""
|
|
18
|
+
|
|
19
|
+
value: NotRequired[list]
|
|
20
|
+
align: NotRequired[str]
|
|
21
|
+
size: NotRequired[str]
|
|
22
|
+
url: NotRequired[str]
|
|
23
|
+
plaintext: NotRequired[str]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SlateBlockItem(TypedDict, total=False):
|
|
27
|
+
"""A child in an Slate Block."""
|
|
28
|
+
|
|
29
|
+
type: NotRequired[str]
|
|
30
|
+
text: NotRequired[str]
|
|
31
|
+
key: NotRequired[str]
|
|
32
|
+
cells: NotRequired[Sequence["SlateBlockItem"]]
|
|
33
|
+
children: NotRequired[list["SlateBlockItem"]]
|
|
34
|
+
value: NotRequired["SlateBlockItem | str"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BlocksLayout(TypedDict):
|
|
38
|
+
"""Blocks layout information."""
|
|
39
|
+
|
|
40
|
+
items: Sequence[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class VoltoBlocksInfo(TypedDict):
|
|
44
|
+
"""Volto Blocks information."""
|
|
45
|
+
|
|
46
|
+
blocks: dict[str, VoltoBlock]
|
|
47
|
+
blocks_layout: BlocksLayout
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BlockConverter(Protocol):
|
|
51
|
+
"""Protocol for block converters."""
|
|
52
|
+
|
|
53
|
+
__name__: str
|
|
54
|
+
__orig_mod__: str
|
|
55
|
+
|
|
56
|
+
def __call__(self, element: Tag) -> Generator[VoltoBlock, None, None]: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ElementConverterFunc(Protocol):
|
|
60
|
+
"""Protocol for element converters."""
|
|
61
|
+
|
|
62
|
+
__name__: str
|
|
63
|
+
__module__: str
|
|
64
|
+
|
|
65
|
+
def __call__(
|
|
66
|
+
self, element: Tag, tag_name: str
|
|
67
|
+
) -> Generator[VoltoBlock, None, SlateBlockItem]: ...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ElementConverter(ElementConverterFunc):
|
|
71
|
+
"""Protocol for element converters."""
|
|
72
|
+
|
|
73
|
+
__name__: str
|
|
74
|
+
__orig_mod__: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
SlateItemsGenerator = Generator[VoltoBlock | None, None, list[SlateBlockItem]]
|
|
78
|
+
|
|
79
|
+
SlateItemGenerator = Generator[VoltoBlock | None, None, SlateBlockItem | None]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class IFrameConverter:
|
|
84
|
+
url_pattern: str
|
|
85
|
+
provider: str
|
|
86
|
+
converter: Callable
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Registry:
|
|
91
|
+
block_converters: dict[str, BlockConverter]
|
|
92
|
+
element_converters: dict[str, ElementConverter]
|
|
93
|
+
iframe_converters: dict[re.Pattern | str, IFrameConverter]
|
|
94
|
+
default: BlockConverter | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class EmbedInfo:
|
|
99
|
+
url: str
|
|
100
|
+
provider_id: str
|
|
101
|
+
converter: Callable
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ReportRegistrations(TypedDict):
|
|
105
|
+
"""Report registrations type."""
|
|
106
|
+
|
|
107
|
+
block: dict[str, str]
|
|
108
|
+
element: dict[str, str]
|
|
109
|
+
iframe: dict[str, str]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
__all__ = [
|
|
113
|
+
"EmbedInfo",
|
|
114
|
+
"Registry",
|
|
115
|
+
"ReportRegistrations",
|
|
116
|
+
"Tag",
|
|
117
|
+
"VoltoBlock",
|
|
118
|
+
"VoltoBlocksInfo",
|
|
119
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Block converters for collective.html2blocks.
|
|
3
|
+
|
|
4
|
+
This module imports and exposes the main block converter functions for HTML elements
|
|
5
|
+
such as iframe, image, slate (rich text), table, and video. These converters are
|
|
6
|
+
registered with the html2blocks registry and are used to transform HTML elements
|
|
7
|
+
into Volto blocks for use in Plone/Volto content management systems.
|
|
8
|
+
|
|
9
|
+
Each converter implements a specific strategy for handling its corresponding HTML tag:
|
|
10
|
+
|
|
11
|
+
- `iframe_block`: Converts <iframe> elements to Volto embed blocks,
|
|
12
|
+
handling provider-specific logic.
|
|
13
|
+
- `image_block`: Converts <img> elements to Volto image blocks,
|
|
14
|
+
extracting src, alt, and other attributes.
|
|
15
|
+
- `slate_block`: Converts rich text elements (paragraphs, headings,
|
|
16
|
+
lists, etc.) to Volto Slate blocks, supporting nested and inline content.
|
|
17
|
+
- `table_block`: Converts <table> elements to Volto table blocks,
|
|
18
|
+
handling headers, rows, and cell types.
|
|
19
|
+
- `video_block`: Converts <video> elements to Volto video blocks,
|
|
20
|
+
extracting sources and metadata.
|
|
21
|
+
|
|
22
|
+
These converters are registered using decorators and can be extended or referenced
|
|
23
|
+
by other developers to implement custom block conversion logic. See the implementation
|
|
24
|
+
of each block in its respective module for details on parsing, normalization, and
|
|
25
|
+
Volto block structure.
|
|
26
|
+
|
|
27
|
+
Example usage::
|
|
28
|
+
|
|
29
|
+
from collective.html2blocks.blocks import slate_block
|
|
30
|
+
blocks = slate_block(element)
|
|
31
|
+
|
|
32
|
+
For custom converters, use the registry decorators:
|
|
33
|
+
|
|
34
|
+
from collective.html2blocks.registry import block_converter
|
|
35
|
+
|
|
36
|
+
@block_converter('custom')
|
|
37
|
+
def custom_block(element):
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
__all__ lists the available block converters for import and reference.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from .iframe import iframe_block
|
|
44
|
+
from .image import image_block
|
|
45
|
+
from .slate import slate_block
|
|
46
|
+
from .table import table_block
|
|
47
|
+
from .video import video_block
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"iframe_block",
|
|
52
|
+
"image_block",
|
|
53
|
+
"slate_block",
|
|
54
|
+
"table_block",
|
|
55
|
+
"video_block",
|
|
56
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .soundcloud import soundcloud_block
|
|
2
|
+
from .youtube import youtube_block
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from collective.html2blocks import _types as t
|
|
5
|
+
from collective.html2blocks import registry
|
|
6
|
+
from collective.html2blocks.utils.markup import url_from_iframe
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"iframe_block",
|
|
11
|
+
"iframe_default_block",
|
|
12
|
+
"soundcloud_block",
|
|
13
|
+
"youtube_block",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@registry.iframe_converter("iframe")
|
|
18
|
+
def iframe_default_block(
|
|
19
|
+
element: t.Tag, src: str, provider_id: str
|
|
20
|
+
) -> Generator[t.VoltoBlock, None, None]:
|
|
21
|
+
"""Implemented by @kitconcept/volto-iframe-block."""
|
|
22
|
+
height = element.get("height", "200px")
|
|
23
|
+
yield {"@type": "iframe", "src": src, "width": "full", "height": height}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@registry.block_converter("iframe")
|
|
27
|
+
def iframe_block(element: t.Tag) -> Generator[t.VoltoBlock, None, None]:
|
|
28
|
+
"""Variations of the iframe block."""
|
|
29
|
+
src = url_from_iframe(element)
|
|
30
|
+
embed_info = registry.get_iframe_converter(src)
|
|
31
|
+
yield from embed_info.converter(element, embed_info.url, embed_info.provider_id)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from collective.html2blocks import _types as t
|
|
2
|
+
from collective.html2blocks import registry
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SOUNDCLOUD_REGEX = re.compile(
|
|
8
|
+
r"https://w\.soundcloud.com/player/\?url=https(%3A|:)//api.soundcloud.com/tracks/(?P<provider_id>\d+)(.*)$"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@registry.iframe_converter(
|
|
13
|
+
"soundcloud",
|
|
14
|
+
src_pattern=SOUNDCLOUD_REGEX,
|
|
15
|
+
url_pattern=r"https://api.soundcloud.com/tracks/\g<provider_id>\2",
|
|
16
|
+
)
|
|
17
|
+
def soundcloud_block(element: t.Tag, src: str, provider_id: str) -> list[t.VoltoBlock]:
|
|
18
|
+
"""Implemented by @kitconcept/volto-social-blocks."""
|
|
19
|
+
block: t.VoltoBlock = {
|
|
20
|
+
"@type": "soundcloudBlock",
|
|
21
|
+
"soundcloudId": provider_id,
|
|
22
|
+
"align": "center",
|
|
23
|
+
"size": "l",
|
|
24
|
+
}
|
|
25
|
+
return [block]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from collective.html2blocks import _types as t
|
|
2
|
+
from collective.html2blocks import registry
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
YOUTUBE_REGEX = re.compile(
|
|
8
|
+
r"^(http:|https:|)\/\/(m.|www.)?(youtu(be\.com|\.be|be\.googleapis\.com|be-nocookie\.com))\/(embed\/|watch\?v=|v\/)?(?P<provider_id>[A-Za-z0-9._%-]*)(\?\S+|\&\S+)?$"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_youtube_video_id(url: str) -> str:
|
|
13
|
+
video_id = ""
|
|
14
|
+
if match := re.match(YOUTUBE_REGEX, url):
|
|
15
|
+
video_id = match.group("provider_id")
|
|
16
|
+
return video_id
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _youtube_block(src: str) -> list[t.VoltoBlock]:
|
|
20
|
+
"""Return a block to display youtube videos."""
|
|
21
|
+
block: t.VoltoBlock = {"@type": "video", "url": src}
|
|
22
|
+
return [block]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@registry.iframe_converter(
|
|
26
|
+
"youtube",
|
|
27
|
+
src_pattern=YOUTUBE_REGEX,
|
|
28
|
+
url_pattern=r"https://youtu.be/\g<provider_id>",
|
|
29
|
+
)
|
|
30
|
+
def youtube_block(element: t.Tag, src: str, provider_id: str) -> list[t.VoltoBlock]:
|
|
31
|
+
"""Implemented by @plone/volto."""
|
|
32
|
+
return _youtube_block(src)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image block converter for collective.html2blocks.
|
|
3
|
+
|
|
4
|
+
This module provides the block converter for <img> elements, transforming them
|
|
5
|
+
into Volto image blocks. It extracts image attributes, alignment, size, and
|
|
6
|
+
custom data attributes, supporting Plone/Volto conventions for image handling.
|
|
7
|
+
|
|
8
|
+
Implementation details:
|
|
9
|
+
- Alignment and size are inferred from CSS classes and image src patterns.
|
|
10
|
+
- Custom data attributes are added to the block for extensibility.
|
|
11
|
+
- The converter yields a Volto image block with all relevant metadata.
|
|
12
|
+
|
|
13
|
+
Example usage::
|
|
14
|
+
|
|
15
|
+
from collective.html2blocks.blocks.image import image_block
|
|
16
|
+
blocks = list(image_block(element))
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from collections.abc import Generator
|
|
20
|
+
from collective.html2blocks import _types as t
|
|
21
|
+
from collective.html2blocks import registry
|
|
22
|
+
from collective.html2blocks.logger import logger
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _align_from_classes(css_classes: list[str]) -> str:
|
|
28
|
+
align = "center"
|
|
29
|
+
if "image-left" in css_classes:
|
|
30
|
+
align = "left"
|
|
31
|
+
elif "image-right" in css_classes:
|
|
32
|
+
align = "right"
|
|
33
|
+
elif "image-inline" in css_classes:
|
|
34
|
+
align = "center"
|
|
35
|
+
return align
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _add_align_to_block(block: t.VoltoBlock, css_classes: list[str]) -> None:
|
|
39
|
+
size: str = "l"
|
|
40
|
+
match align := _align_from_classes(css_classes):
|
|
41
|
+
case "left":
|
|
42
|
+
size = "m"
|
|
43
|
+
case "right":
|
|
44
|
+
size = "m"
|
|
45
|
+
block["align"] = align
|
|
46
|
+
block["size"] = size
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _add_size_to_block(block: t.VoltoBlock, src: str) -> None:
|
|
50
|
+
size: str = block.get("size", "m")
|
|
51
|
+
match _scale_from_src(src):
|
|
52
|
+
case "large":
|
|
53
|
+
size = "l"
|
|
54
|
+
case "preview":
|
|
55
|
+
size = "l"
|
|
56
|
+
case "thumb":
|
|
57
|
+
size = "s"
|
|
58
|
+
case "tile":
|
|
59
|
+
size = "s"
|
|
60
|
+
block["size"] = size
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _add_data_attrs_to_block(block: t.VoltoBlock, attrs: dict) -> None:
|
|
64
|
+
data_attrs: dict = {k: v for k, v in attrs.items() if k.startswith("data-")}
|
|
65
|
+
for raw_key, value in data_attrs.items():
|
|
66
|
+
key = raw_key.replace("data-", "")
|
|
67
|
+
block[key] = value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _scale_from_src(src: str) -> str:
|
|
71
|
+
scale = ""
|
|
72
|
+
if (match := re.search("/@@images/(?P<field>[^/]*)/(?P<scale>.+)", src)) or (
|
|
73
|
+
match := re.search("/image_(?P<scale>.+)", src)
|
|
74
|
+
):
|
|
75
|
+
scale = match.group("scale")
|
|
76
|
+
elif match := re.search("/image$", src):
|
|
77
|
+
scale = "original"
|
|
78
|
+
return scale
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@registry.block_converter("img")
|
|
82
|
+
def image_block(element: t.Tag) -> Generator[t.VoltoBlock, None, None]:
|
|
83
|
+
"""
|
|
84
|
+
Convert an <img> element to a Volto image block.
|
|
85
|
+
|
|
86
|
+
This converter extracts src, alt, title, alignment, size, and custom data
|
|
87
|
+
attributes from the image element and yields a Volto image block.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
element (Tag): The <img> element to convert.
|
|
91
|
+
|
|
92
|
+
Yields:
|
|
93
|
+
VoltoBlock: The converted image block.
|
|
94
|
+
|
|
95
|
+
Example::
|
|
96
|
+
|
|
97
|
+
blocks = list(image_block(element))
|
|
98
|
+
# [{'@type': 'image', 'url': ..., 'alt': ..., ...}]
|
|
99
|
+
"""
|
|
100
|
+
src: str = element.get("src")
|
|
101
|
+
if src is None:
|
|
102
|
+
logger.debug(f"Dropping element {element}")
|
|
103
|
+
return None
|
|
104
|
+
url: str = src.split("/@@images")[0]
|
|
105
|
+
css_classes: list[str] = element.get("class", [])
|
|
106
|
+
alt: str = element.get("alt", "")
|
|
107
|
+
title: str = element.get("title", "")
|
|
108
|
+
block: t.VoltoBlock = {"@type": "image", "url": url, "alt": alt, "title": title}
|
|
109
|
+
_add_align_to_block(block, css_classes)
|
|
110
|
+
_add_size_to_block(block, src)
|
|
111
|
+
_add_data_attrs_to_block(block, element.attrs)
|
|
112
|
+
yield block
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slate block converter for collective.html2blocks.
|
|
3
|
+
|
|
4
|
+
This module provides the default block converter for rich text elements,
|
|
5
|
+
transforming paragraphs, headings, lists, and other inline/nested content into
|
|
6
|
+
Volto Slate blocks. It uses the parser to deserialize HTML elements and
|
|
7
|
+
normalizes the result for Volto consumption.
|
|
8
|
+
|
|
9
|
+
Implementation details:
|
|
10
|
+
- Handles plain text extraction and normalization.
|
|
11
|
+
- Supports nested and inline content, grouping and flattening as needed.
|
|
12
|
+
- Yields Volto Slate blocks and any additional blocks found during parsing.
|
|
13
|
+
|
|
14
|
+
Example usage::
|
|
15
|
+
|
|
16
|
+
from collective.html2blocks.blocks.slate import slate_block
|
|
17
|
+
blocks = list(slate_block(element))
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from . import parser
|
|
21
|
+
from collections.abc import Generator
|
|
22
|
+
from collective.html2blocks import _types as t
|
|
23
|
+
from collective.html2blocks import registry
|
|
24
|
+
from collective.html2blocks.utils import markup
|
|
25
|
+
from collective.html2blocks.utils import slate as utils
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@registry.default_converter
|
|
29
|
+
def slate_block(
|
|
30
|
+
element: t.Tag,
|
|
31
|
+
) -> Generator[t.VoltoBlock, None, t.VoltoBlock | None]:
|
|
32
|
+
"""
|
|
33
|
+
Convert rich text HTML elements to Volto Slate blocks.
|
|
34
|
+
|
|
35
|
+
This converter deserializes paragraphs, headings, lists, and other content
|
|
36
|
+
into Volto Slate blocks, handling nested and inline elements, and normalizing
|
|
37
|
+
the result for Volto. Additional blocks found during parsing are also yielded.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
element (Tag): The HTML element to convert.
|
|
41
|
+
|
|
42
|
+
Yields:
|
|
43
|
+
VoltoBlock: The converted Slate block and any additional blocks.
|
|
44
|
+
|
|
45
|
+
Example::
|
|
46
|
+
|
|
47
|
+
blocks = list(slate_block(element))
|
|
48
|
+
# [{'@type': 'slate', 'plaintext': ..., 'value': [...]}]
|
|
49
|
+
"""
|
|
50
|
+
plaintext = markup.extract_plaintext(element)
|
|
51
|
+
value = yield from parser.deserialize(element)
|
|
52
|
+
blocks: list[t.VoltoBlock] = []
|
|
53
|
+
additional_blocks: list[t.VoltoBlock] = []
|
|
54
|
+
if value is None:
|
|
55
|
+
value = []
|
|
56
|
+
elif isinstance(value, list):
|
|
57
|
+
value = yield from parser.extract_blocks(value)
|
|
58
|
+
elif isinstance(value, dict) and (children := value.get("children", [])):
|
|
59
|
+
children = yield from parser.extract_blocks(children)
|
|
60
|
+
value["children"] = children
|
|
61
|
+
if value and not blocks:
|
|
62
|
+
value = [value] if not isinstance(value, list) else value
|
|
63
|
+
value = utils.process_top_level_items(value)
|
|
64
|
+
block = {"@type": "slate", "plaintext": plaintext, "value": value}
|
|
65
|
+
block = yield from parser.finalize_slate(block)
|
|
66
|
+
blocks = [block]
|
|
67
|
+
yield from blocks
|
|
68
|
+
yield from additional_blocks
|
|
69
|
+
return None
|