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,5 @@
1
+ __title__ = "draftjs_exporter"
2
+ __version__ = "5.2.0"
3
+ __author__ = "Springload"
4
+ __license__ = "MIT"
5
+ __copyright__ = "Copyright 2016-present Springload"
@@ -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
+ }
@@ -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_}>"