180 lines
6.4 KiB
Python
180 lines
6.4 KiB
Python
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"]))]
|
|
)
|