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.
@@ -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
@@ -0,0 +1,6 @@
1
+ class ExporterException(Exception):
2
+ pass
3
+
4
+
5
+ class ConfigException(ExporterException):
6
+ pass
@@ -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
+ )