angrybeanie_wagtail/env/lib/python3.12/site-packages/draftjs_exporter/html.py

181 lines
6.4 KiB
Python
Raw Normal View History

2025-07-25 21:32:16 +10:00
from itertools import groupby
from operator import attrgetter
from typing import List, Optional, Tuple
from draftjs_exporter.command import Command
from draftjs_exporter.composite_decorators import (
render_decorators,
should_render_decorators,
)
from draftjs_exporter.defaults import BLOCK_MAP, STYLE_MAP
from draftjs_exporter.dom import DOM
from draftjs_exporter.entity_state import EntityState
from draftjs_exporter.options import Options
from draftjs_exporter.style_state import StyleState
from draftjs_exporter.types import (
Block,
Config,
ContentState,
Element,
EntityMap,
)
from draftjs_exporter.wrapper_state import WrapperState
class HTML:
"""
Entry point of the exporter. Combines entity, wrapper and style state
to generate the right HTML nodes.
"""
__slots__ = (
"composite_decorators",
"entity_options",
"block_options",
"style_options",
)
def __init__(self, config: Optional[Config] = None) -> None:
if config is None:
config = {}
self.composite_decorators = config.get("composite_decorators", [])
self.entity_options = Options.map_entities(config.get("entity_decorators", {}))
self.block_options = Options.map_blocks(config.get("block_map", BLOCK_MAP))
self.style_options = Options.map_styles(config.get("style_map", STYLE_MAP))
DOM.use(config.get("engine", DOM.STRING))
def render(self, content_state: Optional[ContentState] = None) -> str:
"""
Starts the export process on a given piece of content state.
"""
if content_state is None:
content_state = {}
blocks = content_state.get("blocks", [])
wrapper_state = WrapperState(self.block_options, blocks)
document = DOM.create_element()
entity_map = content_state.get("entityMap", {})
min_depth = 0
for block in blocks:
# Assume a depth of 0 if it's not specified, like Draft.js would.
depth = block["depth"] if "depth" in block else 0
elt = self.render_block(block, entity_map, wrapper_state)
if depth > min_depth:
min_depth = depth
# At level 0, append the element to the document.
if depth == 0:
DOM.append_child(document, elt)
# If there is no block at depth 0, we need to add the wrapper that contains the whole tree to the document.
if min_depth > 0 and wrapper_state.stack.length() != 0:
DOM.append_child(document, wrapper_state.stack.tail().elt)
return DOM.render(document)
def render_block(
self, block: Block, entity_map: EntityMap, wrapper_state: WrapperState
) -> Element:
has_styles = "inlineStyleRanges" in block and block["inlineStyleRanges"]
has_entities = "entityRanges" in block and block["entityRanges"]
has_decorators = should_render_decorators(
self.composite_decorators, block["text"]
)
if has_styles or has_entities:
content = DOM.create_element()
entity_state = EntityState(self.entity_options, entity_map)
style_state = StyleState(self.style_options) if has_styles else None
for text, commands in self.build_command_groups(block):
for command in commands:
entity_state.apply(command)
if style_state:
style_state.apply(command)
# Decorators are not rendered inside entities.
if has_decorators and entity_state.has_no_entity():
decorated_node = render_decorators(
self.composite_decorators,
text,
block,
wrapper_state.blocks,
)
else:
decorated_node = text
if style_state:
styled_node = style_state.render_styles(
decorated_node, block, wrapper_state.blocks
)
else:
styled_node = decorated_node
entity_node = entity_state.render_entities(
styled_node, block, wrapper_state.blocks
)
if entity_node is not None:
DOM.append_child(content, entity_node)
# Check whether there actually are two different nodes, confirming we are not inserting an upcoming entity.
if styled_node != entity_node and entity_state.has_no_entity():
DOM.append_child(content, styled_node)
# Fast track for blocks which do not contain styles nor entities, which is very common.
elif has_decorators:
content = render_decorators(
self.composite_decorators,
block["text"],
block,
wrapper_state.blocks,
)
else:
content = block["text"]
return wrapper_state.element_for(block, content)
def build_command_groups(self, block: Block) -> List[Tuple[str, List[Command]]]:
"""
Creates block modification commands, grouped by start index,
with the text to apply them on.
"""
text = block["text"]
commands = self.build_commands(block)
grouped = groupby(commands, attrgetter("index"))
listed = list(groupby(commands, attrgetter("index")))
sliced = []
i = 0
for start_index, comms in grouped:
if i < len(listed) - 1:
stop_index = listed[i + 1][0]
sliced.append((text[start_index:stop_index], list(comms)))
else:
sliced.append(("", list(comms)))
i += 1
return sliced
def build_commands(self, block: Block) -> List[Command]:
"""
Build all of the manipulation commands for a given block.
- One pair to set the text.
- Multiple pairs for styles.
- Multiple pairs for entities.
"""
style_commands = Command.from_style_ranges(block)
entity_commands = Command.from_entity_ranges(block)
styles_and_entities = style_commands + entity_commands
styles_and_entities.sort(key=attrgetter("index"))
return (
[Command("start_text", 0)]
+ styles_and_entities
+ [Command("stop_text", len(block["text"]))]
)