draftjs_exporter 5.2.0__py3-none-any.whl
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.
- draftjs_exporter/__init__.py +5 -0
- draftjs_exporter/command.py +56 -0
- draftjs_exporter/composite_decorators.py +93 -0
- draftjs_exporter/constants.py +61 -0
- draftjs_exporter/defaults.py +56 -0
- draftjs_exporter/dom.py +129 -0
- draftjs_exporter/engines/__init__.py +0 -0
- draftjs_exporter/engines/base.py +50 -0
- draftjs_exporter/engines/html5lib.py +45 -0
- draftjs_exporter/engines/lxml.py +49 -0
- draftjs_exporter/engines/string.py +116 -0
- draftjs_exporter/engines/string_compat.py +62 -0
- draftjs_exporter/entity_state.py +108 -0
- draftjs_exporter/error.py +6 -0
- draftjs_exporter/html.py +180 -0
- draftjs_exporter/options.py +105 -0
- draftjs_exporter/py.typed +0 -0
- draftjs_exporter/style_state.py +49 -0
- draftjs_exporter/types.py +35 -0
- draftjs_exporter/utils/__init__.py +0 -0
- draftjs_exporter/utils/module_loading.py +25 -0
- draftjs_exporter/wrapper_state.py +170 -0
- draftjs_exporter-5.2.0.dist-info/METADATA +376 -0
- draftjs_exporter-5.2.0.dist-info/RECORD +25 -0
- draftjs_exporter-5.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from html import escape
|
|
2
|
+
from typing import Sequence, Union
|
|
3
|
+
|
|
4
|
+
from draftjs_exporter.engines.base import Attr
|
|
5
|
+
from draftjs_exporter.engines.string import VOID_ELEMENTS, DOMString, Elt
|
|
6
|
+
from draftjs_exporter.types import HTML
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DOMStringCompat(DOMString):
|
|
10
|
+
"""
|
|
11
|
+
The same as DOMString, but with as much backwards-compatibility as possible.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def render_attrs(attr: Attr) -> str:
|
|
16
|
+
attrs = [f' {k}="{escape(v)}"' for k, v in attr.items()]
|
|
17
|
+
# Compat: reverts "Remove HTML attributes alphabetical sorting of default string engine ([#129](https://github.com/springload/draftjs_exporter/pull/129))"
|
|
18
|
+
attrs.sort()
|
|
19
|
+
return "".join(attrs)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def render_children(children: Sequence[Union[HTML, Elt]]) -> HTML:
|
|
23
|
+
return "".join(
|
|
24
|
+
[
|
|
25
|
+
DOMStringCompat.render(c)
|
|
26
|
+
if isinstance(c, Elt)
|
|
27
|
+
# Compat: reverts "Disable single and double quotes escaping outside of attributes for string engine ([#129](https://github.com/springload/draftjs_exporter/pull/129))"
|
|
28
|
+
else escape(c, quote=True)
|
|
29
|
+
for c in children
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def render(elt: Elt) -> HTML:
|
|
35
|
+
type_ = elt.type
|
|
36
|
+
attr = DOMStringCompat.render_attrs(elt.attr) if elt.attr else ""
|
|
37
|
+
children = DOMStringCompat.render_children(elt.children) if elt.children else ""
|
|
38
|
+
|
|
39
|
+
if type_ == "fragment":
|
|
40
|
+
return children
|
|
41
|
+
|
|
42
|
+
if type_ in VOID_ELEMENTS:
|
|
43
|
+
return f"<{type_}{attr}/>"
|
|
44
|
+
|
|
45
|
+
if type_ == "escaped_html":
|
|
46
|
+
return elt.markup
|
|
47
|
+
|
|
48
|
+
return f"<{type_}{attr}>{children}</{type_}>"
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def render_debug(elt: Elt) -> HTML:
|
|
52
|
+
type_ = elt.type
|
|
53
|
+
attr = DOMStringCompat.render_attrs(elt.attr) if elt.attr else ""
|
|
54
|
+
children = DOMStringCompat.render_children(elt.children) if elt.children else ""
|
|
55
|
+
|
|
56
|
+
if type_ in VOID_ELEMENTS:
|
|
57
|
+
return f"<{type_}{attr}/>"
|
|
58
|
+
|
|
59
|
+
if type_ == "escaped_html":
|
|
60
|
+
return elt.markup
|
|
61
|
+
|
|
62
|
+
return f"<{type_}{attr}>{children}</{type_}>"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from typing import List, Optional, Sequence
|
|
2
|
+
|
|
3
|
+
from draftjs_exporter.command import Command
|
|
4
|
+
from draftjs_exporter.constants import ENTITY_TYPES
|
|
5
|
+
from draftjs_exporter.dom import DOM
|
|
6
|
+
from draftjs_exporter.error import ExporterException
|
|
7
|
+
from draftjs_exporter.options import Options, OptionsMap
|
|
8
|
+
from draftjs_exporter.types import (
|
|
9
|
+
Block,
|
|
10
|
+
Element,
|
|
11
|
+
EntityDetails,
|
|
12
|
+
EntityKey,
|
|
13
|
+
EntityMap,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EntityException(ExporterException):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EntityState:
|
|
22
|
+
__slots__ = (
|
|
23
|
+
"entity_options",
|
|
24
|
+
"entity_map",
|
|
25
|
+
"entity_stack",
|
|
26
|
+
"completed_entity",
|
|
27
|
+
"element_stack",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def __init__(self, entity_options: OptionsMap, entity_map: EntityMap) -> None:
|
|
31
|
+
self.entity_options = entity_options
|
|
32
|
+
self.entity_map = entity_map
|
|
33
|
+
|
|
34
|
+
self.entity_stack: List[EntityKey] = []
|
|
35
|
+
self.completed_entity: Optional[EntityKey] = None
|
|
36
|
+
self.element_stack: List[Element] = []
|
|
37
|
+
|
|
38
|
+
def apply(self, command: Command) -> None:
|
|
39
|
+
if command.name == "start_entity":
|
|
40
|
+
self.entity_stack.append(command.data)
|
|
41
|
+
elif command.name == "stop_entity":
|
|
42
|
+
expected_entity = self.entity_stack[-1]
|
|
43
|
+
|
|
44
|
+
if command.data != expected_entity:
|
|
45
|
+
raise EntityException(f"Expected {expected_entity}, got {command.data}")
|
|
46
|
+
|
|
47
|
+
self.completed_entity = self.entity_stack.pop()
|
|
48
|
+
|
|
49
|
+
def has_entity(self) -> List[EntityKey]:
|
|
50
|
+
return self.entity_stack
|
|
51
|
+
|
|
52
|
+
def has_no_entity(self) -> bool:
|
|
53
|
+
return not self.entity_stack
|
|
54
|
+
|
|
55
|
+
def get_entity_details(self, entity_key: EntityKey) -> EntityDetails:
|
|
56
|
+
details = self.entity_map.get(entity_key)
|
|
57
|
+
|
|
58
|
+
if details is None:
|
|
59
|
+
raise EntityException(
|
|
60
|
+
f'Entity "{entity_key}" does not exist in the entityMap'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return details
|
|
64
|
+
|
|
65
|
+
def render_entities(
|
|
66
|
+
self, style_node: Element, block: Block, blocks: Sequence[Block]
|
|
67
|
+
) -> Element:
|
|
68
|
+
# We have a complete (start, stop) entity to render.
|
|
69
|
+
if self.completed_entity is not None:
|
|
70
|
+
entity_details = self.get_entity_details(self.completed_entity)
|
|
71
|
+
options = Options.get(
|
|
72
|
+
self.entity_options,
|
|
73
|
+
entity_details["type"],
|
|
74
|
+
ENTITY_TYPES.FALLBACK,
|
|
75
|
+
)
|
|
76
|
+
props = entity_details["data"].copy()
|
|
77
|
+
props["entity"] = {
|
|
78
|
+
"type": entity_details["type"],
|
|
79
|
+
"mutability": entity_details["mutability"]
|
|
80
|
+
if "mutability" in entity_details
|
|
81
|
+
else None,
|
|
82
|
+
"block": block,
|
|
83
|
+
"blocks": blocks,
|
|
84
|
+
"entity_range": {"key": self.completed_entity},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if len(self.element_stack) == 1:
|
|
88
|
+
children = self.element_stack[0]
|
|
89
|
+
else:
|
|
90
|
+
children = DOM.create_element()
|
|
91
|
+
|
|
92
|
+
for n in self.element_stack:
|
|
93
|
+
DOM.append_child(children, n)
|
|
94
|
+
|
|
95
|
+
self.completed_entity = None
|
|
96
|
+
self.element_stack = []
|
|
97
|
+
|
|
98
|
+
# Is there still another entity? (adjacent) if so add the current style_node for it.
|
|
99
|
+
if self.has_entity():
|
|
100
|
+
self.element_stack.append(style_node)
|
|
101
|
+
|
|
102
|
+
return DOM.create_element(options.element, props, children)
|
|
103
|
+
|
|
104
|
+
if self.has_entity():
|
|
105
|
+
self.element_stack.append(style_node)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
return style_node
|
draftjs_exporter/html.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from itertools import groupby
|
|
2
|
+
from operator import attrgetter
|
|
3
|
+
from typing import List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from draftjs_exporter.command import Command
|
|
6
|
+
from draftjs_exporter.composite_decorators import (
|
|
7
|
+
render_decorators,
|
|
8
|
+
should_render_decorators,
|
|
9
|
+
)
|
|
10
|
+
from draftjs_exporter.defaults import BLOCK_MAP, STYLE_MAP
|
|
11
|
+
from draftjs_exporter.dom import DOM
|
|
12
|
+
from draftjs_exporter.entity_state import EntityState
|
|
13
|
+
from draftjs_exporter.options import Options
|
|
14
|
+
from draftjs_exporter.style_state import StyleState
|
|
15
|
+
from draftjs_exporter.types import (
|
|
16
|
+
Block,
|
|
17
|
+
Config,
|
|
18
|
+
ContentState,
|
|
19
|
+
Element,
|
|
20
|
+
EntityMap,
|
|
21
|
+
)
|
|
22
|
+
from draftjs_exporter.wrapper_state import WrapperState
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HTML:
|
|
26
|
+
"""
|
|
27
|
+
Entry point of the exporter. Combines entity, wrapper and style state
|
|
28
|
+
to generate the right HTML nodes.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
__slots__ = (
|
|
32
|
+
"composite_decorators",
|
|
33
|
+
"entity_options",
|
|
34
|
+
"block_options",
|
|
35
|
+
"style_options",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def __init__(self, config: Optional[Config] = None) -> None:
|
|
39
|
+
if config is None:
|
|
40
|
+
config = {}
|
|
41
|
+
|
|
42
|
+
self.composite_decorators = config.get("composite_decorators", [])
|
|
43
|
+
|
|
44
|
+
self.entity_options = Options.map_entities(config.get("entity_decorators", {}))
|
|
45
|
+
self.block_options = Options.map_blocks(config.get("block_map", BLOCK_MAP))
|
|
46
|
+
self.style_options = Options.map_styles(config.get("style_map", STYLE_MAP))
|
|
47
|
+
|
|
48
|
+
DOM.use(config.get("engine", DOM.STRING))
|
|
49
|
+
|
|
50
|
+
def render(self, content_state: Optional[ContentState] = None) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Starts the export process on a given piece of content state.
|
|
53
|
+
"""
|
|
54
|
+
if content_state is None:
|
|
55
|
+
content_state = {}
|
|
56
|
+
|
|
57
|
+
blocks = content_state.get("blocks", [])
|
|
58
|
+
wrapper_state = WrapperState(self.block_options, blocks)
|
|
59
|
+
document = DOM.create_element()
|
|
60
|
+
entity_map = content_state.get("entityMap", {})
|
|
61
|
+
min_depth = 0
|
|
62
|
+
|
|
63
|
+
for block in blocks:
|
|
64
|
+
# Assume a depth of 0 if it's not specified, like Draft.js would.
|
|
65
|
+
depth = block["depth"] if "depth" in block else 0
|
|
66
|
+
elt = self.render_block(block, entity_map, wrapper_state)
|
|
67
|
+
|
|
68
|
+
if depth > min_depth:
|
|
69
|
+
min_depth = depth
|
|
70
|
+
|
|
71
|
+
# At level 0, append the element to the document.
|
|
72
|
+
if depth == 0:
|
|
73
|
+
DOM.append_child(document, elt)
|
|
74
|
+
|
|
75
|
+
# If there is no block at depth 0, we need to add the wrapper that contains the whole tree to the document.
|
|
76
|
+
if min_depth > 0 and wrapper_state.stack.length() != 0:
|
|
77
|
+
DOM.append_child(document, wrapper_state.stack.tail().elt)
|
|
78
|
+
|
|
79
|
+
return DOM.render(document)
|
|
80
|
+
|
|
81
|
+
def render_block(
|
|
82
|
+
self, block: Block, entity_map: EntityMap, wrapper_state: WrapperState
|
|
83
|
+
) -> Element:
|
|
84
|
+
has_styles = "inlineStyleRanges" in block and block["inlineStyleRanges"]
|
|
85
|
+
has_entities = "entityRanges" in block and block["entityRanges"]
|
|
86
|
+
has_decorators = should_render_decorators(
|
|
87
|
+
self.composite_decorators, block["text"]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if has_styles or has_entities:
|
|
91
|
+
content = DOM.create_element()
|
|
92
|
+
entity_state = EntityState(self.entity_options, entity_map)
|
|
93
|
+
style_state = StyleState(self.style_options) if has_styles else None
|
|
94
|
+
|
|
95
|
+
for text, commands in self.build_command_groups(block):
|
|
96
|
+
for command in commands:
|
|
97
|
+
entity_state.apply(command)
|
|
98
|
+
if style_state:
|
|
99
|
+
style_state.apply(command)
|
|
100
|
+
|
|
101
|
+
# Decorators are not rendered inside entities.
|
|
102
|
+
if has_decorators and entity_state.has_no_entity():
|
|
103
|
+
decorated_node = render_decorators(
|
|
104
|
+
self.composite_decorators,
|
|
105
|
+
text,
|
|
106
|
+
block,
|
|
107
|
+
wrapper_state.blocks,
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
decorated_node = text
|
|
111
|
+
|
|
112
|
+
if style_state:
|
|
113
|
+
styled_node = style_state.render_styles(
|
|
114
|
+
decorated_node, block, wrapper_state.blocks
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
styled_node = decorated_node
|
|
118
|
+
entity_node = entity_state.render_entities(
|
|
119
|
+
styled_node, block, wrapper_state.blocks
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if entity_node is not None:
|
|
123
|
+
DOM.append_child(content, entity_node)
|
|
124
|
+
|
|
125
|
+
# Check whether there actually are two different nodes, confirming we are not inserting an upcoming entity.
|
|
126
|
+
if styled_node != entity_node and entity_state.has_no_entity():
|
|
127
|
+
DOM.append_child(content, styled_node)
|
|
128
|
+
# Fast track for blocks which do not contain styles nor entities, which is very common.
|
|
129
|
+
elif has_decorators:
|
|
130
|
+
content = render_decorators(
|
|
131
|
+
self.composite_decorators,
|
|
132
|
+
block["text"],
|
|
133
|
+
block,
|
|
134
|
+
wrapper_state.blocks,
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
content = block["text"]
|
|
138
|
+
|
|
139
|
+
return wrapper_state.element_for(block, content)
|
|
140
|
+
|
|
141
|
+
def build_command_groups(self, block: Block) -> List[Tuple[str, List[Command]]]:
|
|
142
|
+
"""
|
|
143
|
+
Creates block modification commands, grouped by start index,
|
|
144
|
+
with the text to apply them on.
|
|
145
|
+
"""
|
|
146
|
+
text = block["text"]
|
|
147
|
+
|
|
148
|
+
commands = self.build_commands(block)
|
|
149
|
+
grouped = groupby(commands, attrgetter("index"))
|
|
150
|
+
listed = list(groupby(commands, attrgetter("index")))
|
|
151
|
+
sliced = []
|
|
152
|
+
|
|
153
|
+
i = 0
|
|
154
|
+
for start_index, comms in grouped:
|
|
155
|
+
if i < len(listed) - 1:
|
|
156
|
+
stop_index = listed[i + 1][0]
|
|
157
|
+
sliced.append((text[start_index:stop_index], list(comms)))
|
|
158
|
+
else:
|
|
159
|
+
sliced.append(("", list(comms)))
|
|
160
|
+
i += 1
|
|
161
|
+
|
|
162
|
+
return sliced
|
|
163
|
+
|
|
164
|
+
def build_commands(self, block: Block) -> List[Command]:
|
|
165
|
+
"""
|
|
166
|
+
Build all of the manipulation commands for a given block.
|
|
167
|
+
- One pair to set the text.
|
|
168
|
+
- Multiple pairs for styles.
|
|
169
|
+
- Multiple pairs for entities.
|
|
170
|
+
"""
|
|
171
|
+
style_commands = Command.from_style_ranges(block)
|
|
172
|
+
entity_commands = Command.from_entity_ranges(block)
|
|
173
|
+
styles_and_entities = style_commands + entity_commands
|
|
174
|
+
styles_and_entities.sort(key=attrgetter("index"))
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
[Command("start_text", 0)]
|
|
178
|
+
+ styles_and_entities
|
|
179
|
+
+ [Command("stop_text", len(block["text"]))]
|
|
180
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES, INLINE_STYLES
|
|
4
|
+
from draftjs_exporter.error import ConfigException
|
|
5
|
+
from draftjs_exporter.types import ConfigMap, Props, RenderableType
|
|
6
|
+
|
|
7
|
+
# Internal equivalent of a ConfigMap.
|
|
8
|
+
OptionsMap = Dict[str, "Options"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Options:
|
|
12
|
+
"""
|
|
13
|
+
Facilitates querying configuration from a config map.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__slots__ = ("type", "element", "props", "wrapper", "wrapper_props")
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
type_: str,
|
|
21
|
+
element: RenderableType,
|
|
22
|
+
props: Optional[Props] = None,
|
|
23
|
+
wrapper: RenderableType = None,
|
|
24
|
+
wrapper_props: Optional[Props] = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self.type = type_
|
|
27
|
+
self.element = element
|
|
28
|
+
self.props = props if props else {}
|
|
29
|
+
self.wrapper = wrapper
|
|
30
|
+
self.wrapper_props = wrapper_props
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
return f"<Options {self.type} {self.element} {self.props} {self.wrapper} {self.wrapper_props}>"
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return str(self)
|
|
37
|
+
|
|
38
|
+
def __eq__(self, other: Any) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Equality used in test code only, not to be relied on for the exporter.
|
|
41
|
+
"""
|
|
42
|
+
return str(self) == str(other)
|
|
43
|
+
|
|
44
|
+
def __ne__(self, other: Any) -> bool:
|
|
45
|
+
return not self == other
|
|
46
|
+
|
|
47
|
+
def __hash__(self) -> int:
|
|
48
|
+
return hash(str(self))
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def create(kind_map: ConfigMap, type_: str, fallback_key: str) -> "Options":
|
|
52
|
+
"""
|
|
53
|
+
Create an Options object from any mapping.
|
|
54
|
+
"""
|
|
55
|
+
if type_ not in kind_map:
|
|
56
|
+
if fallback_key not in kind_map:
|
|
57
|
+
raise ConfigException(
|
|
58
|
+
f'"{type_}" is not in the config and has no fallback'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
config = kind_map[fallback_key]
|
|
62
|
+
else:
|
|
63
|
+
config = kind_map[type_]
|
|
64
|
+
|
|
65
|
+
if isinstance(config, dict):
|
|
66
|
+
if "element" not in config:
|
|
67
|
+
raise ConfigException(f'"{type_}" does not define an element')
|
|
68
|
+
|
|
69
|
+
opts = Options(type_, **config)
|
|
70
|
+
else:
|
|
71
|
+
opts = Options(type_, config)
|
|
72
|
+
|
|
73
|
+
return opts
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def map(kind_map: ConfigMap, fallback_key: str) -> OptionsMap:
|
|
77
|
+
options = {}
|
|
78
|
+
for type_ in kind_map:
|
|
79
|
+
options[type_] = Options.create(kind_map, type_, fallback_key)
|
|
80
|
+
|
|
81
|
+
return options
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def map_blocks(block_map: ConfigMap) -> OptionsMap:
|
|
85
|
+
return Options.map(block_map, BLOCK_TYPES.FALLBACK)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def map_styles(style_map: ConfigMap) -> OptionsMap:
|
|
89
|
+
return Options.map(style_map, INLINE_STYLES.FALLBACK)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def map_entities(entity_map: ConfigMap) -> OptionsMap:
|
|
93
|
+
return Options.map(entity_map, ENTITY_TYPES.FALLBACK)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def get(options: OptionsMap, type_: str, fallback_key: str) -> "Options":
|
|
97
|
+
try:
|
|
98
|
+
return options[type_]
|
|
99
|
+
except KeyError:
|
|
100
|
+
try:
|
|
101
|
+
return options[fallback_key]
|
|
102
|
+
except KeyError:
|
|
103
|
+
raise ConfigException(
|
|
104
|
+
f'"{type_}" is not in the config and has no fallback'
|
|
105
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import List, Sequence
|
|
2
|
+
|
|
3
|
+
from draftjs_exporter.command import Command
|
|
4
|
+
from draftjs_exporter.constants import INLINE_STYLES
|
|
5
|
+
from draftjs_exporter.dom import DOM
|
|
6
|
+
from draftjs_exporter.options import Options, OptionsMap
|
|
7
|
+
from draftjs_exporter.types import Block, Element
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StyleState:
|
|
11
|
+
"""
|
|
12
|
+
Handles the creation of inline styles on elements.
|
|
13
|
+
Receives inline_style commands, and generates the element's `style`
|
|
14
|
+
attribute from those.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__slots__ = ("styles", "style_options")
|
|
18
|
+
|
|
19
|
+
def __init__(self, style_options: OptionsMap) -> None:
|
|
20
|
+
self.styles: List[str] = []
|
|
21
|
+
self.style_options = style_options
|
|
22
|
+
|
|
23
|
+
def apply(self, command: Command) -> None:
|
|
24
|
+
if command.name == "start_inline_style":
|
|
25
|
+
self.styles.append(command.data)
|
|
26
|
+
elif command.name == "stop_inline_style":
|
|
27
|
+
self.styles.remove(command.data)
|
|
28
|
+
|
|
29
|
+
def is_empty(self) -> bool:
|
|
30
|
+
return not self.styles
|
|
31
|
+
|
|
32
|
+
def render_styles(
|
|
33
|
+
self, decorated_node: Element, block: Block, blocks: Sequence[Block]
|
|
34
|
+
) -> Element:
|
|
35
|
+
node = decorated_node
|
|
36
|
+
if not self.is_empty():
|
|
37
|
+
# This will mutate self.styles, but it’s going to be reset after rendering anyway.
|
|
38
|
+
self.styles.sort(reverse=True)
|
|
39
|
+
|
|
40
|
+
# Nest the tags.
|
|
41
|
+
for style in self.styles:
|
|
42
|
+
options = Options.get(self.style_options, style, INLINE_STYLES.FALLBACK)
|
|
43
|
+
props = dict(options.props)
|
|
44
|
+
props["block"] = block
|
|
45
|
+
props["blocks"] = blocks
|
|
46
|
+
props["inline_style_range"] = {"style": style}
|
|
47
|
+
node = DOM.create_element(options.element, props, node)
|
|
48
|
+
|
|
49
|
+
return node
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, List, Mapping, Union
|
|
2
|
+
|
|
3
|
+
# Element represents an instance of a RenderableType. It’s engine-specific so very hard to type.
|
|
4
|
+
Element = Any
|
|
5
|
+
# Props are always a dictionary with string keys and arbitrary values.
|
|
6
|
+
# TODO Other types than string for keys.
|
|
7
|
+
Props = Dict[str, Any]
|
|
8
|
+
|
|
9
|
+
# A DOM tag name.
|
|
10
|
+
Tag = str
|
|
11
|
+
# A component function, taking props as a parameter and returning an Element by calling DOM.create_element.
|
|
12
|
+
Component = Callable[[Props], Element]
|
|
13
|
+
# What can be rendered: None, DOM tag name, Component.
|
|
14
|
+
RenderableType = Union[None, Tag, Component]
|
|
15
|
+
# The output of the exporter.
|
|
16
|
+
HTML = str
|
|
17
|
+
|
|
18
|
+
# The whole config object.
|
|
19
|
+
Config = Dict[str, Any]
|
|
20
|
+
# block_map, style_map, entity_decorators.
|
|
21
|
+
# The dict could be limited to a fixed set of keys, but this would require TypedDict.
|
|
22
|
+
ConfigMap = Mapping[str, Union[Dict[str, Any], RenderableType]]
|
|
23
|
+
# composite_decorators.
|
|
24
|
+
Decorator = Dict[str, Any]
|
|
25
|
+
CompositeDecorators = List[Decorator]
|
|
26
|
+
|
|
27
|
+
# The whole content state. blocks and entity_map.
|
|
28
|
+
ContentState = Mapping[str, Any]
|
|
29
|
+
# Blocks have a predetermined set of keys and values, but let’s be permissive without TypedDict.
|
|
30
|
+
Block = Mapping[str, Any]
|
|
31
|
+
# Entity key is int in blocks, str in Entity map.
|
|
32
|
+
EntityKey = str
|
|
33
|
+
# Entities have fixed keys.
|
|
34
|
+
EntityDetails = Mapping[str, Any]
|
|
35
|
+
EntityMap = Mapping[EntityKey, EntityDetails]
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from importlib import import_module
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def import_string(dotted_path: str) -> Any:
|
|
6
|
+
"""
|
|
7
|
+
Import a dotted module path and return the attribute/class designated by the
|
|
8
|
+
last name in the path. Raise ImportError if the import failed.
|
|
9
|
+
|
|
10
|
+
Taken from Django:
|
|
11
|
+
https://github.com/django/django/blob/f6bd00131e687aedf2719ad31e84b097562ca5f2/django/utils/module_loading.py#L7-L24
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
module_path, class_name = dotted_path.rsplit(".", 1)
|
|
15
|
+
except ValueError:
|
|
16
|
+
raise ImportError(f"{dotted_path} doesn't look like a module path")
|
|
17
|
+
|
|
18
|
+
module = import_module(module_path)
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
return getattr(module, class_name)
|
|
22
|
+
except AttributeError:
|
|
23
|
+
raise ImportError(
|
|
24
|
+
f'Module "{module_path}" does not define a "{class_name}" attribute/class'
|
|
25
|
+
)
|