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,56 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
from draftjs_exporter.types import Block
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Command:
|
|
7
|
+
"""
|
|
8
|
+
A Command represents an operation that has to be executed
|
|
9
|
+
on a block for it to be converted into an arbitrary number
|
|
10
|
+
of HTML nodes.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__slots__ = ("name", "index", "data")
|
|
14
|
+
|
|
15
|
+
def __init__(self, name: str, index: int, data: str = "") -> None:
|
|
16
|
+
self.name = name
|
|
17
|
+
self.index = index
|
|
18
|
+
self.data = data
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
return f"<Command {self.name} {self.index} {self.data}>"
|
|
22
|
+
|
|
23
|
+
def __repr__(self) -> str:
|
|
24
|
+
return str(self)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def from_entity_ranges(block: Block) -> List["Command"]:
|
|
28
|
+
"""
|
|
29
|
+
Creates a list of commands from a block’s list of entity ranges.
|
|
30
|
+
Each range is converted to two commands: a start_* and a stop_*.
|
|
31
|
+
"""
|
|
32
|
+
commands: List["Command"] = []
|
|
33
|
+
for r in block["entityRanges"]:
|
|
34
|
+
# Entity key is an integer in entity ranges, while a string in the entity map.
|
|
35
|
+
data = str(r["key"])
|
|
36
|
+
start = r["offset"]
|
|
37
|
+
stop = start + r["length"]
|
|
38
|
+
commands.append(Command("start_entity", start, data))
|
|
39
|
+
commands.append(Command("stop_entity", stop, data))
|
|
40
|
+
|
|
41
|
+
return commands
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def from_style_ranges(block: Block) -> List["Command"]:
|
|
45
|
+
"""
|
|
46
|
+
Creates a list of commands from a block’s list of style ranges.
|
|
47
|
+
Each range is converted to two commands: a start_* and a stop_*.
|
|
48
|
+
"""
|
|
49
|
+
commands: List["Command"] = []
|
|
50
|
+
for r in block["inlineStyleRanges"]:
|
|
51
|
+
data = r["style"]
|
|
52
|
+
start = r["offset"]
|
|
53
|
+
stop = start + r["length"]
|
|
54
|
+
commands.append(Command("start_inline_style", start, data))
|
|
55
|
+
commands.append(Command("stop_inline_style", stop, data))
|
|
56
|
+
return commands
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from operator import itemgetter
|
|
3
|
+
from typing import Any, Dict, Generator, List, Sequence, Tuple
|
|
4
|
+
|
|
5
|
+
from draftjs_exporter.dom import DOM
|
|
6
|
+
from draftjs_exporter.types import (
|
|
7
|
+
Block,
|
|
8
|
+
CompositeDecorators,
|
|
9
|
+
Decorator,
|
|
10
|
+
Element,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
br = "\n"
|
|
14
|
+
br_strategy = re.compile(r"\n")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_decorations(
|
|
18
|
+
decorators: CompositeDecorators, text: str
|
|
19
|
+
) -> List[Tuple[int, int, Any, Decorator]]:
|
|
20
|
+
occupied: Dict[int, int] = {}
|
|
21
|
+
decorations = []
|
|
22
|
+
|
|
23
|
+
for decorator in decorators:
|
|
24
|
+
for match in decorator["strategy"].finditer(text):
|
|
25
|
+
begin, end = match.span()
|
|
26
|
+
if not any(occupied.get(i) for i in range(begin, end)):
|
|
27
|
+
for i in range(begin, end):
|
|
28
|
+
occupied[i] = 1
|
|
29
|
+
decorations.append((begin, end, match, decorator))
|
|
30
|
+
|
|
31
|
+
decorations.sort(key=itemgetter(0))
|
|
32
|
+
|
|
33
|
+
return decorations
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def apply_decorators(
|
|
37
|
+
decorators: CompositeDecorators,
|
|
38
|
+
text: str,
|
|
39
|
+
block: Block,
|
|
40
|
+
blocks: Sequence[Block],
|
|
41
|
+
) -> Generator[str, None, None]:
|
|
42
|
+
decorations = get_decorations(decorators, text)
|
|
43
|
+
|
|
44
|
+
pointer = 0
|
|
45
|
+
for begin, end, match, decorator in decorations:
|
|
46
|
+
if pointer < begin:
|
|
47
|
+
yield text[pointer:begin]
|
|
48
|
+
|
|
49
|
+
yield DOM.create_element(
|
|
50
|
+
decorator["component"],
|
|
51
|
+
{"match": match, "block": block, "blocks": blocks},
|
|
52
|
+
match.group(0),
|
|
53
|
+
)
|
|
54
|
+
pointer = end
|
|
55
|
+
|
|
56
|
+
if pointer < len(text):
|
|
57
|
+
yield text[pointer:]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def render_decorators(
|
|
61
|
+
decorators: CompositeDecorators,
|
|
62
|
+
text: str,
|
|
63
|
+
block: Block,
|
|
64
|
+
blocks: Sequence[Block],
|
|
65
|
+
) -> Element:
|
|
66
|
+
decorated_children = list(apply_decorators(decorators, text, block, blocks))
|
|
67
|
+
|
|
68
|
+
if len(decorated_children) == 1:
|
|
69
|
+
decorated_node = decorated_children[0]
|
|
70
|
+
else:
|
|
71
|
+
decorated_node = DOM.create_element()
|
|
72
|
+
for decorated_child in decorated_children:
|
|
73
|
+
DOM.append_child(decorated_node, decorated_child)
|
|
74
|
+
|
|
75
|
+
return decorated_node
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def should_render_decorators(
|
|
79
|
+
decorators: CompositeDecorators,
|
|
80
|
+
text: str,
|
|
81
|
+
) -> bool:
|
|
82
|
+
nb_decorators = len(decorators)
|
|
83
|
+
|
|
84
|
+
if nb_decorators == 0:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
is_skippable_br = (
|
|
88
|
+
nb_decorators == 1
|
|
89
|
+
and decorators[0]["strategy"] == br_strategy
|
|
90
|
+
and br not in text
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return not is_skippable_br
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# http://stackoverflow.com/a/22723724/1798491
|
|
2
|
+
class Enum:
|
|
3
|
+
__slots__ = "elements"
|
|
4
|
+
|
|
5
|
+
def __init__(self, *elements: str) -> None:
|
|
6
|
+
self.elements = tuple(elements)
|
|
7
|
+
|
|
8
|
+
def __getattr__(self, name: str) -> str:
|
|
9
|
+
if name not in self.elements:
|
|
10
|
+
raise AttributeError(f"'Enum' has no attribute '{name}'")
|
|
11
|
+
|
|
12
|
+
return name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# https://github.com/facebook/draft-js/blob/master/src/model/constants/DraftBlockType.js
|
|
16
|
+
class BLOCK_TYPES:
|
|
17
|
+
UNSTYLED = "unstyled"
|
|
18
|
+
HEADER_ONE = "header-one"
|
|
19
|
+
HEADER_TWO = "header-two"
|
|
20
|
+
HEADER_THREE = "header-three"
|
|
21
|
+
HEADER_FOUR = "header-four"
|
|
22
|
+
HEADER_FIVE = "header-five"
|
|
23
|
+
HEADER_SIX = "header-six"
|
|
24
|
+
UNORDERED_LIST_ITEM = "unordered-list-item"
|
|
25
|
+
ORDERED_LIST_ITEM = "ordered-list-item"
|
|
26
|
+
BLOCKQUOTE = "blockquote"
|
|
27
|
+
PRE = "pre"
|
|
28
|
+
CODE = "code-block"
|
|
29
|
+
ATOMIC = "atomic"
|
|
30
|
+
# Special type to configure handling of missing components.
|
|
31
|
+
FALLBACK = "fallback"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ENTITY_TYPES = Enum(
|
|
35
|
+
"LINK",
|
|
36
|
+
"DOCUMENT",
|
|
37
|
+
"IMAGE",
|
|
38
|
+
"EMBED",
|
|
39
|
+
"HORIZONTAL_RULE",
|
|
40
|
+
# Special type to configure handling of missing components.
|
|
41
|
+
"FALLBACK",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
INLINE_STYLES = Enum(
|
|
45
|
+
"BOLD",
|
|
46
|
+
"CODE",
|
|
47
|
+
"ITALIC",
|
|
48
|
+
"UNDERLINE",
|
|
49
|
+
"STRIKETHROUGH",
|
|
50
|
+
"SUPERSCRIPT",
|
|
51
|
+
"SUBSCRIPT",
|
|
52
|
+
"MARK",
|
|
53
|
+
"QUOTATION",
|
|
54
|
+
"SMALL",
|
|
55
|
+
"SAMPLE",
|
|
56
|
+
"INSERT",
|
|
57
|
+
"DELETE",
|
|
58
|
+
"KEYBOARD",
|
|
59
|
+
# Special type to configure handling of missing components.
|
|
60
|
+
"FALLBACK",
|
|
61
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from draftjs_exporter.constants import BLOCK_TYPES, INLINE_STYLES
|
|
2
|
+
from draftjs_exporter.dom import DOM
|
|
3
|
+
from draftjs_exporter.types import Element, Props
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def render_children(props: Props) -> Element:
|
|
7
|
+
"""
|
|
8
|
+
Renders the children of a component without any specific
|
|
9
|
+
markup for the component itself.
|
|
10
|
+
"""
|
|
11
|
+
return props["children"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def code_block(props: Props) -> Element:
|
|
15
|
+
return DOM.create_element(
|
|
16
|
+
"pre", {}, DOM.create_element("code", {}, props["children"])
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Default block map to extend.
|
|
21
|
+
BLOCK_MAP = {
|
|
22
|
+
BLOCK_TYPES.UNSTYLED: "p",
|
|
23
|
+
BLOCK_TYPES.HEADER_ONE: "h1",
|
|
24
|
+
BLOCK_TYPES.HEADER_TWO: "h2",
|
|
25
|
+
BLOCK_TYPES.HEADER_THREE: "h3",
|
|
26
|
+
BLOCK_TYPES.HEADER_FOUR: "h4",
|
|
27
|
+
BLOCK_TYPES.HEADER_FIVE: "h5",
|
|
28
|
+
BLOCK_TYPES.HEADER_SIX: "h6",
|
|
29
|
+
BLOCK_TYPES.UNORDERED_LIST_ITEM: {"element": "li", "wrapper": "ul"},
|
|
30
|
+
BLOCK_TYPES.ORDERED_LIST_ITEM: {"element": "li", "wrapper": "ol"},
|
|
31
|
+
BLOCK_TYPES.BLOCKQUOTE: "blockquote",
|
|
32
|
+
BLOCK_TYPES.PRE: "pre",
|
|
33
|
+
BLOCK_TYPES.CODE: code_block,
|
|
34
|
+
BLOCK_TYPES.ATOMIC: render_children,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Default style map to extend.
|
|
38
|
+
# Tags come from https://developer.mozilla.org/en-US/docs/Web/HTML/Element.
|
|
39
|
+
# and are loosely aligned with https://github.com/jpuri/draftjs-to-html.
|
|
40
|
+
# Only styles that map to HTML elements are allowed as defaults.
|
|
41
|
+
STYLE_MAP = {
|
|
42
|
+
INLINE_STYLES.BOLD: "strong",
|
|
43
|
+
INLINE_STYLES.CODE: "code",
|
|
44
|
+
INLINE_STYLES.ITALIC: "em",
|
|
45
|
+
INLINE_STYLES.UNDERLINE: "u",
|
|
46
|
+
INLINE_STYLES.STRIKETHROUGH: "s",
|
|
47
|
+
INLINE_STYLES.SUPERSCRIPT: "sup",
|
|
48
|
+
INLINE_STYLES.SUBSCRIPT: "sub",
|
|
49
|
+
INLINE_STYLES.MARK: "mark",
|
|
50
|
+
INLINE_STYLES.QUOTATION: "q",
|
|
51
|
+
INLINE_STYLES.SMALL: "small",
|
|
52
|
+
INLINE_STYLES.SAMPLE: "samp",
|
|
53
|
+
INLINE_STYLES.INSERT: "ins",
|
|
54
|
+
INLINE_STYLES.DELETE: "del",
|
|
55
|
+
INLINE_STYLES.KEYBOARD: "kbd",
|
|
56
|
+
}
|
draftjs_exporter/dom.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from draftjs_exporter.engines.base import DOMEngine
|
|
5
|
+
from draftjs_exporter.types import HTML, Element, Props, RenderableType
|
|
6
|
+
from draftjs_exporter.utils.module_loading import import_string
|
|
7
|
+
|
|
8
|
+
# https://gist.github.com/yahyaKacem/8170675
|
|
9
|
+
_first_cap_re = re.compile(r"(.)([A-Z][a-z]+)")
|
|
10
|
+
_all_cap_re = re.compile("([a-z0-9])([A-Z])")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DOM:
|
|
14
|
+
"""
|
|
15
|
+
Component building API, abstracting the DOM implementation.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
HTML5LIB = "draftjs_exporter.engines.html5lib.DOM_HTML5LIB"
|
|
19
|
+
LXML = "draftjs_exporter.engines.lxml.DOM_LXML"
|
|
20
|
+
STRING = "draftjs_exporter.engines.string.DOMString"
|
|
21
|
+
STRING_COMPAT = "draftjs_exporter.engines.string_compat.DOMStringCompat"
|
|
22
|
+
|
|
23
|
+
dom: DOMEngine = None # type: ignore
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def camel_to_dash(camel_cased_str: str) -> str:
|
|
27
|
+
sub2 = _first_cap_re.sub(r"\1-\2", camel_cased_str)
|
|
28
|
+
dashed_case_str = _all_cap_re.sub(r"\1-\2", sub2).lower()
|
|
29
|
+
return dashed_case_str.replace("--", "-")
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def use(cls, engine: str) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Choose which DOM implementation to use.
|
|
35
|
+
"""
|
|
36
|
+
cls.dom = import_string(engine)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def create_element(
|
|
40
|
+
cls,
|
|
41
|
+
type_: RenderableType = None,
|
|
42
|
+
props: Optional[Props] = None,
|
|
43
|
+
*elt_children: Optional[Element],
|
|
44
|
+
) -> Element:
|
|
45
|
+
"""
|
|
46
|
+
Signature inspired by React.createElement.
|
|
47
|
+
createElement(
|
|
48
|
+
string/Component type,
|
|
49
|
+
[dict props],
|
|
50
|
+
[children ...]
|
|
51
|
+
)
|
|
52
|
+
https://facebook.github.io/react/docs/top-level-api.html#react.createelement
|
|
53
|
+
"""
|
|
54
|
+
# Create an empty document fragment.
|
|
55
|
+
if not type_:
|
|
56
|
+
return cls.dom.create_tag("fragment")
|
|
57
|
+
|
|
58
|
+
if props is None:
|
|
59
|
+
props = {}
|
|
60
|
+
|
|
61
|
+
# If the first element of children is a list, we use it as the list.
|
|
62
|
+
if len(elt_children) and isinstance(elt_children[0], (list, tuple)):
|
|
63
|
+
children = elt_children[0]
|
|
64
|
+
else:
|
|
65
|
+
children = elt_children
|
|
66
|
+
|
|
67
|
+
# The children prop is the first child if there is only one.
|
|
68
|
+
props["children"] = children[0] if len(children) == 1 else children
|
|
69
|
+
|
|
70
|
+
if callable(type_):
|
|
71
|
+
# Function component, via def or lambda.
|
|
72
|
+
elt = type_(props)
|
|
73
|
+
else:
|
|
74
|
+
# Raw tag, as a string.
|
|
75
|
+
attributes = {}
|
|
76
|
+
|
|
77
|
+
# Never render those attributes on a raw tag.
|
|
78
|
+
props.pop("children", None)
|
|
79
|
+
props.pop("block", None)
|
|
80
|
+
props.pop("blocks", None)
|
|
81
|
+
props.pop("entity", None)
|
|
82
|
+
props.pop("inline_style_range", None)
|
|
83
|
+
|
|
84
|
+
# Convert style object to style string, like the DOM would do.
|
|
85
|
+
if "style" in props and isinstance(props["style"], dict):
|
|
86
|
+
rules = [
|
|
87
|
+
f"{DOM.camel_to_dash(s)}: {v};" for s, v in props["style"].items()
|
|
88
|
+
]
|
|
89
|
+
props["style"] = "".join(rules)
|
|
90
|
+
|
|
91
|
+
# Convert props to HTML attributes.
|
|
92
|
+
for key in props:
|
|
93
|
+
if props[key] is False:
|
|
94
|
+
props[key] = "false"
|
|
95
|
+
|
|
96
|
+
if props[key] is True:
|
|
97
|
+
props[key] = "true"
|
|
98
|
+
|
|
99
|
+
if props[key] is not None:
|
|
100
|
+
attributes[key] = str(props[key])
|
|
101
|
+
|
|
102
|
+
elt = cls.dom.create_tag(type_, attributes)
|
|
103
|
+
|
|
104
|
+
# Append the children inside the element.
|
|
105
|
+
for child in children:
|
|
106
|
+
if child not in (None, ""):
|
|
107
|
+
cls.append_child(elt, child)
|
|
108
|
+
|
|
109
|
+
# If elt is "empty", create a fragment anyway to add children.
|
|
110
|
+
if elt in (None, ""):
|
|
111
|
+
elt = cls.dom.create_tag("fragment")
|
|
112
|
+
|
|
113
|
+
return elt
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def parse_html(cls, markup: HTML) -> Element:
|
|
117
|
+
return cls.dom.parse_html(markup)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def append_child(cls, elt: Element, child: Element) -> Any:
|
|
121
|
+
return cls.dom.append_child(elt, child)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def render(cls, elt: Element) -> HTML:
|
|
125
|
+
return cls.dom.render(elt)
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def render_debug(cls, elt: Element) -> HTML:
|
|
129
|
+
return cls.dom.render_debug(elt)
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from draftjs_exporter.types import HTML, Element, Tag
|
|
4
|
+
|
|
5
|
+
Attr = Dict[str, str]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DOMEngine:
|
|
9
|
+
"""
|
|
10
|
+
Parent class of all DOM implementations.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def create_tag(type_: Tag, attr: Optional[Attr] = None) -> Any:
|
|
15
|
+
"""
|
|
16
|
+
Creates and returns a tree node of the given type and attributes.
|
|
17
|
+
"""
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def parse_html(markup: HTML) -> Element:
|
|
22
|
+
"""
|
|
23
|
+
Creates nodes based on the input html.
|
|
24
|
+
Note: this method is used in component implementations only, and
|
|
25
|
+
is not required for the exporter to operate.
|
|
26
|
+
"""
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def append_child(elt: Element, child: Element) -> Any:
|
|
31
|
+
"""
|
|
32
|
+
Appends the given child node in the children of elt.
|
|
33
|
+
"""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def render(elt: Element) -> HTML:
|
|
38
|
+
"""
|
|
39
|
+
Renders a given element to HTML.
|
|
40
|
+
"""
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def render_debug(elt: Element) -> HTML:
|
|
45
|
+
"""
|
|
46
|
+
Renders a given element to HTML.
|
|
47
|
+
Note: this method is only used for draftjs_exporter's tests, and
|
|
48
|
+
is not required for the exporter to operate.
|
|
49
|
+
"""
|
|
50
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from draftjs_exporter.engines.base import Attr, DOMEngine
|
|
5
|
+
from draftjs_exporter.types import HTML, Element, Tag
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
|
|
10
|
+
# Cache empty soup so we can create tags in isolation without the performance overhead.
|
|
11
|
+
soup = BeautifulSoup("", "html5lib")
|
|
12
|
+
except ImportError:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
RENDER_RE = re.compile(r"</?(fragment|body|html|head)>")
|
|
16
|
+
RENDER_DEBUG_RE = re.compile(r"</?(body|html|head)>")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DOM_HTML5LIB(DOMEngine):
|
|
20
|
+
"""
|
|
21
|
+
html5lib implementation of the DOM API.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def create_tag(type_: Tag, attr: Optional[Attr] = None) -> Element:
|
|
26
|
+
if not attr:
|
|
27
|
+
attr = {}
|
|
28
|
+
|
|
29
|
+
return soup.new_tag(type_, **attr)
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def parse_html(markup: HTML) -> Element:
|
|
33
|
+
return BeautifulSoup(markup, "html5lib")
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def append_child(elt: Element, child: Element) -> None:
|
|
37
|
+
elt.append(child)
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def render(elt: Element) -> HTML:
|
|
41
|
+
return RENDER_RE.sub("", str(elt))
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def render_debug(elt: Element) -> HTML:
|
|
45
|
+
return RENDER_DEBUG_RE.sub("", str(elt))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from lxml import etree, html
|
|
5
|
+
|
|
6
|
+
from draftjs_exporter.engines.base import Attr, DOMEngine
|
|
7
|
+
from draftjs_exporter.types import HTML, Tag
|
|
8
|
+
|
|
9
|
+
NSMAP = {"xlink": "http://www.w3.org/1999/xlink"}
|
|
10
|
+
|
|
11
|
+
RENDER_RE = re.compile(r"</?fragment>")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DOM_LXML(DOMEngine):
|
|
15
|
+
"""
|
|
16
|
+
lxml implementation of the DOM API.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def create_tag(type_: Tag, attr: Optional[Attr] = None) -> etree.Element:
|
|
21
|
+
nsmap = None
|
|
22
|
+
|
|
23
|
+
if attr:
|
|
24
|
+
if "xlink:href" in attr:
|
|
25
|
+
attr[f"{{{NSMAP['xlink']}}}href"] = attr.pop("xlink:href")
|
|
26
|
+
nsmap = NSMAP
|
|
27
|
+
|
|
28
|
+
return etree.Element(type_, attrib=attr, nsmap=nsmap)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def parse_html(markup: HTML) -> etree.Element:
|
|
32
|
+
return html.fromstring(markup)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def append_child(elt: etree.Element, child: etree.Element) -> None:
|
|
36
|
+
if hasattr(child, "tag"):
|
|
37
|
+
elt.append(child)
|
|
38
|
+
else:
|
|
39
|
+
c = etree.Element("fragment")
|
|
40
|
+
c.text = child
|
|
41
|
+
elt.append(c)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def render(elt: etree.Element) -> HTML:
|
|
45
|
+
return RENDER_RE.sub("", etree.tostring(elt, method="html", encoding="unicode"))
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def render_debug(elt: etree.Element) -> HTML:
|
|
49
|
+
return etree.tostring(elt, method="html", encoding="unicode")
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from html import escape
|
|
2
|
+
from typing import List, Optional, Sequence, Union
|
|
3
|
+
|
|
4
|
+
from draftjs_exporter.engines.base import Attr, DOMEngine
|
|
5
|
+
from draftjs_exporter.types import HTML, Tag
|
|
6
|
+
|
|
7
|
+
# http://w3c.github.io/html/single-page.html#void-elements
|
|
8
|
+
# https://github.com/html5lib/html5lib-python/blob/0cae52b2073e3f2220db93a7650901f2200f2a13/html5lib/constants.py#L560
|
|
9
|
+
VOID_ELEMENTS = (
|
|
10
|
+
"area",
|
|
11
|
+
"base",
|
|
12
|
+
"br",
|
|
13
|
+
"col",
|
|
14
|
+
"embed",
|
|
15
|
+
"hr",
|
|
16
|
+
"img",
|
|
17
|
+
"input",
|
|
18
|
+
"link",
|
|
19
|
+
"meta",
|
|
20
|
+
"param",
|
|
21
|
+
"source",
|
|
22
|
+
"track",
|
|
23
|
+
"wbr",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Elt:
|
|
28
|
+
"""
|
|
29
|
+
A DOM element that the string engine manipulates.
|
|
30
|
+
This class doesn't do much, but the exporter relies on
|
|
31
|
+
comparing elements by reference so it's useful nonetheless.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
__slots__ = ("type", "attr", "children", "markup")
|
|
35
|
+
|
|
36
|
+
def __init__(self, type_: Tag, attr: Optional[Attr], markup: HTML = ""):
|
|
37
|
+
self.type = type_
|
|
38
|
+
self.attr = attr
|
|
39
|
+
self.children: List["Elt"] = []
|
|
40
|
+
self.markup = markup
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def from_html(markup: HTML) -> "Elt":
|
|
44
|
+
return Elt("escaped_html", None, markup)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DOMString(DOMEngine):
|
|
48
|
+
"""
|
|
49
|
+
String concatenation implementation of the DOM API.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def create_tag(type_: Tag, attr: Optional[Attr] = None) -> Elt:
|
|
54
|
+
return Elt(type_, attr)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def parse_html(markup: HTML) -> Elt:
|
|
58
|
+
"""
|
|
59
|
+
Allows inserting arbitrary HTML into the exporter output.
|
|
60
|
+
Treats the HTML as if it had been escaped and was safe already.
|
|
61
|
+
"""
|
|
62
|
+
return Elt.from_html(markup)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def append_child(elt: Elt, child: Elt) -> None:
|
|
66
|
+
# This check is necessary because the current wrapper_state implementation
|
|
67
|
+
# has an issue where it inserts elements multiple times.
|
|
68
|
+
# This must be skipped for text, which can be duplicated.
|
|
69
|
+
is_existing_ref = child in elt.children and isinstance(child, Elt)
|
|
70
|
+
if not is_existing_ref:
|
|
71
|
+
elt.children.append(child)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def render_attrs(attr: Attr) -> str:
|
|
75
|
+
attrs = [f' {k}="{escape(v)}"' for k, v in attr.items()]
|
|
76
|
+
return "".join(attrs)
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def render_children(children: Sequence[Union[HTML, Elt]]) -> HTML:
|
|
80
|
+
return "".join(
|
|
81
|
+
[
|
|
82
|
+
DOMString.render(c) if isinstance(c, Elt) else escape(c, quote=False)
|
|
83
|
+
for c in children
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def render(elt: Elt) -> HTML:
|
|
89
|
+
type_ = elt.type
|
|
90
|
+
attr = DOMString.render_attrs(elt.attr) if elt.attr else ""
|
|
91
|
+
children = DOMString.render_children(elt.children) if elt.children else ""
|
|
92
|
+
|
|
93
|
+
if type_ == "fragment":
|
|
94
|
+
return children
|
|
95
|
+
|
|
96
|
+
if type_ in VOID_ELEMENTS:
|
|
97
|
+
return f"<{type_}{attr}/>"
|
|
98
|
+
|
|
99
|
+
if type_ == "escaped_html":
|
|
100
|
+
return elt.markup
|
|
101
|
+
|
|
102
|
+
return f"<{type_}{attr}>{children}</{type_}>"
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def render_debug(elt: Elt) -> HTML:
|
|
106
|
+
type_ = elt.type
|
|
107
|
+
attr = DOMString.render_attrs(elt.attr) if elt.attr else ""
|
|
108
|
+
children = DOMString.render_children(elt.children) if elt.children else ""
|
|
109
|
+
|
|
110
|
+
if type_ in VOID_ELEMENTS:
|
|
111
|
+
return f"<{type_}{attr}/>"
|
|
112
|
+
|
|
113
|
+
if type_ == "escaped_html":
|
|
114
|
+
return elt.markup
|
|
115
|
+
|
|
116
|
+
return f"<{type_}{attr}>{children}</{type_}>"
|