Initial commit
This commit is contained in:
5
env/lib/python3.10/site-packages/draftjs_exporter/__init__.py
vendored
Normal file
5
env/lib/python3.10/site-packages/draftjs_exporter/__init__.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
__title__ = "draftjs_exporter"
|
||||
__version__ = "5.0.0"
|
||||
__author__ = "Springload"
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2016-present Springload"
|
||||
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/command.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/command.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/composite_decorators.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/composite_decorators.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/constants.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/constants.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/defaults.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/defaults.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/dom.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/dom.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/entity_state.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/entity_state.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/error.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/error.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/html.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/html.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/options.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/options.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/style_state.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/style_state.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/types.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/types.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/wrapper_state.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/__pycache__/wrapper_state.cpython-310.pyc
vendored
Normal file
Binary file not shown.
56
env/lib/python3.10/site-packages/draftjs_exporter/command.py
vendored
Normal file
56
env/lib/python3.10/site-packages/draftjs_exporter/command.py
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
from typing import List
|
||||
|
||||
from draftjs_exporter.types import Block
|
||||
|
||||
|
||||
class Command:
|
||||
"""
|
||||
A Command represents an operation that has to be executed
|
||||
on a block for it to be converted into an arbitrary number
|
||||
of HTML nodes.
|
||||
"""
|
||||
|
||||
__slots__ = ("name", "index", "data")
|
||||
|
||||
def __init__(self, name: str, index: int, data: str = "") -> None:
|
||||
self.name = name
|
||||
self.index = index
|
||||
self.data = data
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<Command {self.name} {self.index} {self.data}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return str(self)
|
||||
|
||||
@staticmethod
|
||||
def from_entity_ranges(block: Block) -> List["Command"]:
|
||||
"""
|
||||
Creates a list of commands from a block’s list of entity ranges.
|
||||
Each range is converted to two commands: a start_* and a stop_*.
|
||||
"""
|
||||
commands: List["Command"] = []
|
||||
for r in block["entityRanges"]:
|
||||
# Entity key is an integer in entity ranges, while a string in the entity map.
|
||||
data = str(r["key"])
|
||||
start = r["offset"]
|
||||
stop = start + r["length"]
|
||||
commands.append(Command("start_entity", start, data))
|
||||
commands.append(Command("stop_entity", stop, data))
|
||||
|
||||
return commands
|
||||
|
||||
@staticmethod
|
||||
def from_style_ranges(block: Block) -> List["Command"]:
|
||||
"""
|
||||
Creates a list of commands from a block’s list of style ranges.
|
||||
Each range is converted to two commands: a start_* and a stop_*.
|
||||
"""
|
||||
commands: List["Command"] = []
|
||||
for r in block["inlineStyleRanges"]:
|
||||
data = r["style"]
|
||||
start = r["offset"]
|
||||
stop = start + r["length"]
|
||||
commands.append(Command("start_inline_style", start, data))
|
||||
commands.append(Command("stop_inline_style", stop, data))
|
||||
return commands
|
||||
93
env/lib/python3.10/site-packages/draftjs_exporter/composite_decorators.py
vendored
Normal file
93
env/lib/python3.10/site-packages/draftjs_exporter/composite_decorators.py
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
import re
|
||||
from operator import itemgetter
|
||||
from typing import Any, Dict, Generator, List, Sequence, Tuple
|
||||
|
||||
from draftjs_exporter.dom import DOM
|
||||
from draftjs_exporter.types import (
|
||||
Block,
|
||||
CompositeDecorators,
|
||||
Decorator,
|
||||
Element,
|
||||
)
|
||||
|
||||
br = "\n"
|
||||
br_strategy = re.compile(r"\n")
|
||||
|
||||
|
||||
def get_decorations(
|
||||
decorators: CompositeDecorators, text: str
|
||||
) -> List[Tuple[int, int, Any, Decorator]]:
|
||||
occupied: Dict[int, int] = {}
|
||||
decorations = []
|
||||
|
||||
for decorator in decorators:
|
||||
for match in decorator["strategy"].finditer(text):
|
||||
begin, end = match.span()
|
||||
if not any(occupied.get(i) for i in range(begin, end)):
|
||||
for i in range(begin, end):
|
||||
occupied[i] = 1
|
||||
decorations.append((begin, end, match, decorator))
|
||||
|
||||
decorations.sort(key=itemgetter(0))
|
||||
|
||||
return decorations
|
||||
|
||||
|
||||
def apply_decorators(
|
||||
decorators: CompositeDecorators,
|
||||
text: str,
|
||||
block: Block,
|
||||
blocks: Sequence[Block],
|
||||
) -> Generator[str, None, None]:
|
||||
decorations = get_decorations(decorators, text)
|
||||
|
||||
pointer = 0
|
||||
for begin, end, match, decorator in decorations:
|
||||
if pointer < begin:
|
||||
yield text[pointer:begin]
|
||||
|
||||
yield DOM.create_element(
|
||||
decorator["component"],
|
||||
{"match": match, "block": block, "blocks": blocks},
|
||||
match.group(0),
|
||||
)
|
||||
pointer = end
|
||||
|
||||
if pointer < len(text):
|
||||
yield text[pointer:]
|
||||
|
||||
|
||||
def render_decorators(
|
||||
decorators: CompositeDecorators,
|
||||
text: str,
|
||||
block: Block,
|
||||
blocks: Sequence[Block],
|
||||
) -> Element:
|
||||
decorated_children = list(apply_decorators(decorators, text, block, blocks))
|
||||
|
||||
if len(decorated_children) == 1:
|
||||
decorated_node = decorated_children[0]
|
||||
else:
|
||||
decorated_node = DOM.create_element()
|
||||
for decorated_child in decorated_children:
|
||||
DOM.append_child(decorated_node, decorated_child)
|
||||
|
||||
return decorated_node
|
||||
|
||||
|
||||
def should_render_decorators(
|
||||
decorators: CompositeDecorators,
|
||||
text: str,
|
||||
) -> bool:
|
||||
nb_decorators = len(decorators)
|
||||
|
||||
if nb_decorators == 0:
|
||||
return False
|
||||
|
||||
is_skippable_br = (
|
||||
nb_decorators == 1
|
||||
and decorators[0]["strategy"] == br_strategy
|
||||
and not (br in text)
|
||||
)
|
||||
|
||||
return not is_skippable_br
|
||||
61
env/lib/python3.10/site-packages/draftjs_exporter/constants.py
vendored
Normal file
61
env/lib/python3.10/site-packages/draftjs_exporter/constants.py
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# http://stackoverflow.com/a/22723724/1798491
|
||||
class Enum:
|
||||
__slots__ = "elements"
|
||||
|
||||
def __init__(self, *elements: str) -> None:
|
||||
self.elements = tuple(elements)
|
||||
|
||||
def __getattr__(self, name: str) -> str:
|
||||
if name not in self.elements:
|
||||
raise AttributeError(f"'Enum' has no attribute '{name}'")
|
||||
|
||||
return name
|
||||
|
||||
|
||||
# https://github.com/facebook/draft-js/blob/master/src/model/constants/DraftBlockType.js
|
||||
class BLOCK_TYPES:
|
||||
UNSTYLED = "unstyled"
|
||||
HEADER_ONE = "header-one"
|
||||
HEADER_TWO = "header-two"
|
||||
HEADER_THREE = "header-three"
|
||||
HEADER_FOUR = "header-four"
|
||||
HEADER_FIVE = "header-five"
|
||||
HEADER_SIX = "header-six"
|
||||
UNORDERED_LIST_ITEM = "unordered-list-item"
|
||||
ORDERED_LIST_ITEM = "ordered-list-item"
|
||||
BLOCKQUOTE = "blockquote"
|
||||
PRE = "pre"
|
||||
CODE = "code-block"
|
||||
ATOMIC = "atomic"
|
||||
# Special type to configure handling of missing components.
|
||||
FALLBACK = "fallback"
|
||||
|
||||
|
||||
ENTITY_TYPES = Enum(
|
||||
"LINK",
|
||||
"DOCUMENT",
|
||||
"IMAGE",
|
||||
"EMBED",
|
||||
"HORIZONTAL_RULE",
|
||||
# Special type to configure handling of missing components.
|
||||
"FALLBACK",
|
||||
)
|
||||
|
||||
INLINE_STYLES = Enum(
|
||||
"BOLD",
|
||||
"CODE",
|
||||
"ITALIC",
|
||||
"UNDERLINE",
|
||||
"STRIKETHROUGH",
|
||||
"SUPERSCRIPT",
|
||||
"SUBSCRIPT",
|
||||
"MARK",
|
||||
"QUOTATION",
|
||||
"SMALL",
|
||||
"SAMPLE",
|
||||
"INSERT",
|
||||
"DELETE",
|
||||
"KEYBOARD",
|
||||
# Special type to configure handling of missing components.
|
||||
"FALLBACK",
|
||||
)
|
||||
56
env/lib/python3.10/site-packages/draftjs_exporter/defaults.py
vendored
Normal file
56
env/lib/python3.10/site-packages/draftjs_exporter/defaults.py
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
from draftjs_exporter.constants import BLOCK_TYPES, INLINE_STYLES
|
||||
from draftjs_exporter.dom import DOM
|
||||
from draftjs_exporter.types import Element, Props
|
||||
|
||||
|
||||
def render_children(props: Props) -> Element:
|
||||
"""
|
||||
Renders the children of a component without any specific
|
||||
markup for the component itself.
|
||||
"""
|
||||
return props["children"]
|
||||
|
||||
|
||||
def code_block(props: Props) -> Element:
|
||||
return DOM.create_element(
|
||||
"pre", {}, DOM.create_element("code", {}, props["children"])
|
||||
)
|
||||
|
||||
|
||||
# Default block map to extend.
|
||||
BLOCK_MAP = {
|
||||
BLOCK_TYPES.UNSTYLED: "p",
|
||||
BLOCK_TYPES.HEADER_ONE: "h1",
|
||||
BLOCK_TYPES.HEADER_TWO: "h2",
|
||||
BLOCK_TYPES.HEADER_THREE: "h3",
|
||||
BLOCK_TYPES.HEADER_FOUR: "h4",
|
||||
BLOCK_TYPES.HEADER_FIVE: "h5",
|
||||
BLOCK_TYPES.HEADER_SIX: "h6",
|
||||
BLOCK_TYPES.UNORDERED_LIST_ITEM: {"element": "li", "wrapper": "ul"},
|
||||
BLOCK_TYPES.ORDERED_LIST_ITEM: {"element": "li", "wrapper": "ol"},
|
||||
BLOCK_TYPES.BLOCKQUOTE: "blockquote",
|
||||
BLOCK_TYPES.PRE: "pre",
|
||||
BLOCK_TYPES.CODE: code_block,
|
||||
BLOCK_TYPES.ATOMIC: render_children,
|
||||
}
|
||||
|
||||
# Default style map to extend.
|
||||
# Tags come from https://developer.mozilla.org/en-US/docs/Web/HTML/Element.
|
||||
# and are loosely aligned with https://github.com/jpuri/draftjs-to-html.
|
||||
# Only styles that map to HTML elements are allowed as defaults.
|
||||
STYLE_MAP = {
|
||||
INLINE_STYLES.BOLD: "strong",
|
||||
INLINE_STYLES.CODE: "code",
|
||||
INLINE_STYLES.ITALIC: "em",
|
||||
INLINE_STYLES.UNDERLINE: "u",
|
||||
INLINE_STYLES.STRIKETHROUGH: "s",
|
||||
INLINE_STYLES.SUPERSCRIPT: "sup",
|
||||
INLINE_STYLES.SUBSCRIPT: "sub",
|
||||
INLINE_STYLES.MARK: "mark",
|
||||
INLINE_STYLES.QUOTATION: "q",
|
||||
INLINE_STYLES.SMALL: "small",
|
||||
INLINE_STYLES.SAMPLE: "samp",
|
||||
INLINE_STYLES.INSERT: "ins",
|
||||
INLINE_STYLES.DELETE: "del",
|
||||
INLINE_STYLES.KEYBOARD: "kbd",
|
||||
}
|
||||
130
env/lib/python3.10/site-packages/draftjs_exporter/dom.py
vendored
Normal file
130
env/lib/python3.10/site-packages/draftjs_exporter/dom.py
vendored
Normal file
@@ -0,0 +1,130 @@
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
from draftjs_exporter.engines.base import DOMEngine
|
||||
from draftjs_exporter.types import HTML, Element, Props, RenderableType
|
||||
from draftjs_exporter.utils.module_loading import import_string
|
||||
|
||||
# https://gist.github.com/yahyaKacem/8170675
|
||||
_first_cap_re = re.compile(r"(.)([A-Z][a-z]+)")
|
||||
_all_cap_re = re.compile("([a-z0-9])([A-Z])")
|
||||
|
||||
|
||||
class DOM:
|
||||
"""
|
||||
Component building API, abstracting the DOM implementation.
|
||||
"""
|
||||
|
||||
HTML5LIB = "draftjs_exporter.engines.html5lib.DOM_HTML5LIB"
|
||||
LXML = "draftjs_exporter.engines.lxml.DOM_LXML"
|
||||
STRING = "draftjs_exporter.engines.string.DOMString"
|
||||
STRING_COMPAT = "draftjs_exporter.engines.string_compat.DOMStringCompat"
|
||||
|
||||
dom: DOMEngine = None # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def camel_to_dash(camel_cased_str: str) -> str:
|
||||
sub2 = _first_cap_re.sub(r"\1-\2", camel_cased_str)
|
||||
dashed_case_str = _all_cap_re.sub(r"\1-\2", sub2).lower()
|
||||
return dashed_case_str.replace("--", "-")
|
||||
|
||||
@classmethod
|
||||
def use(cls, engine: str) -> None:
|
||||
"""
|
||||
Choose which DOM implementation to use.
|
||||
"""
|
||||
cls.dom = import_string(engine)
|
||||
|
||||
@classmethod
|
||||
def create_element(
|
||||
cls,
|
||||
type_: RenderableType = None,
|
||||
props: Optional[Props] = None,
|
||||
*elt_children: Optional[Element],
|
||||
) -> Element:
|
||||
"""
|
||||
Signature inspired by React.createElement.
|
||||
createElement(
|
||||
string/Component type,
|
||||
[dict props],
|
||||
[children ...]
|
||||
)
|
||||
https://facebook.github.io/react/docs/top-level-api.html#react.createelement
|
||||
"""
|
||||
# Create an empty document fragment.
|
||||
if not type_:
|
||||
return cls.dom.create_tag("fragment")
|
||||
|
||||
if props is None:
|
||||
props = {}
|
||||
|
||||
# If the first element of children is a list, we use it as the list.
|
||||
if len(elt_children) and isinstance(elt_children[0], (list, tuple)):
|
||||
children = elt_children[0]
|
||||
else:
|
||||
children = elt_children
|
||||
|
||||
# The children prop is the first child if there is only one.
|
||||
props["children"] = children[0] if len(children) == 1 else children
|
||||
|
||||
if callable(type_):
|
||||
# Function component, via def or lambda.
|
||||
elt = type_(props)
|
||||
else:
|
||||
# Raw tag, as a string.
|
||||
attributes = {}
|
||||
|
||||
# Never render those attributes on a raw tag.
|
||||
props.pop("children", None)
|
||||
props.pop("block", None)
|
||||
props.pop("blocks", None)
|
||||
props.pop("entity", None)
|
||||
props.pop("inline_style_range", None)
|
||||
|
||||
# Convert style object to style string, like the DOM would do.
|
||||
if "style" in props and isinstance(props["style"], dict):
|
||||
rules = [
|
||||
f"{DOM.camel_to_dash(s)}: {v};"
|
||||
for s, v in props["style"].items()
|
||||
]
|
||||
props["style"] = "".join(rules)
|
||||
|
||||
# Convert props to HTML attributes.
|
||||
for key in props:
|
||||
if props[key] is False:
|
||||
props[key] = "false"
|
||||
|
||||
if props[key] is True:
|
||||
props[key] = "true"
|
||||
|
||||
if props[key] is not None:
|
||||
attributes[key] = str(props[key])
|
||||
|
||||
elt = cls.dom.create_tag(type_, attributes)
|
||||
|
||||
# Append the children inside the element.
|
||||
for child in children:
|
||||
if child not in (None, ""):
|
||||
cls.append_child(elt, child)
|
||||
|
||||
# If elt is "empty", create a fragment anyway to add children.
|
||||
if elt in (None, ""):
|
||||
elt = cls.dom.create_tag("fragment")
|
||||
|
||||
return elt
|
||||
|
||||
@classmethod
|
||||
def parse_html(cls, markup: HTML) -> Element:
|
||||
return cls.dom.parse_html(markup)
|
||||
|
||||
@classmethod
|
||||
def append_child(cls, elt: Element, child: Element) -> Any:
|
||||
return cls.dom.append_child(elt, child)
|
||||
|
||||
@classmethod
|
||||
def render(cls, elt: Element) -> HTML:
|
||||
return cls.dom.render(elt)
|
||||
|
||||
@classmethod
|
||||
def render_debug(cls, elt: Element) -> HTML:
|
||||
return cls.dom.render_debug(elt)
|
||||
0
env/lib/python3.10/site-packages/draftjs_exporter/engines/__init__.py
vendored
Normal file
0
env/lib/python3.10/site-packages/draftjs_exporter/engines/__init__.py
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/base.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/base.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/html5lib.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/html5lib.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/lxml.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/lxml.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/string.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/string.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/string_compat.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/engines/__pycache__/string_compat.cpython-310.pyc
vendored
Normal file
Binary file not shown.
50
env/lib/python3.10/site-packages/draftjs_exporter/engines/base.py
vendored
Normal file
50
env/lib/python3.10/site-packages/draftjs_exporter/engines/base.py
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from draftjs_exporter.types import HTML, Element, Tag
|
||||
|
||||
Attr = Dict[str, str]
|
||||
|
||||
|
||||
class DOMEngine:
|
||||
"""
|
||||
Parent class of all DOM implementations.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_tag(type_: Tag, attr: Optional[Attr] = None) -> Any:
|
||||
"""
|
||||
Creates and returns a tree node of the given type and attributes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def parse_html(markup: HTML) -> Element:
|
||||
"""
|
||||
Creates nodes based on the input html.
|
||||
Note: this method is used in component implementations only, and
|
||||
is not required for the exporter to operate.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def append_child(elt: Element, child: Element) -> Any:
|
||||
"""
|
||||
Appends the given child node in the children of elt.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def render(elt: Element) -> HTML:
|
||||
"""
|
||||
Renders a given element to HTML.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def render_debug(elt: Element) -> HTML:
|
||||
"""
|
||||
Renders a given element to HTML.
|
||||
Note: this method is only used for draftjs_exporter's tests, and
|
||||
is not required for the exporter to operate.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
45
env/lib/python3.10/site-packages/draftjs_exporter/engines/html5lib.py
vendored
Normal file
45
env/lib/python3.10/site-packages/draftjs_exporter/engines/html5lib.py
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from draftjs_exporter.engines.base import Attr, DOMEngine
|
||||
from draftjs_exporter.types import HTML, Element, Tag
|
||||
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# Cache empty soup so we can create tags in isolation without the performance overhead.
|
||||
soup = BeautifulSoup("", "html5lib")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
RENDER_RE = re.compile(r"</?(fragment|body|html|head)>")
|
||||
RENDER_DEBUG_RE = re.compile(r"</?(body|html|head)>")
|
||||
|
||||
|
||||
class DOM_HTML5LIB(DOMEngine):
|
||||
"""
|
||||
html5lib implementation of the DOM API.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_tag(type_: Tag, attr: Optional[Attr] = None) -> Element:
|
||||
if not attr:
|
||||
attr = {}
|
||||
|
||||
return soup.new_tag(type_, **attr)
|
||||
|
||||
@staticmethod
|
||||
def parse_html(markup: HTML) -> Element:
|
||||
return BeautifulSoup(markup, "html5lib")
|
||||
|
||||
@staticmethod
|
||||
def append_child(elt: Element, child: Element) -> None:
|
||||
elt.append(child)
|
||||
|
||||
@staticmethod
|
||||
def render(elt: Element) -> HTML:
|
||||
return RENDER_RE.sub("", str(elt))
|
||||
|
||||
@staticmethod
|
||||
def render_debug(elt: Element) -> HTML:
|
||||
return RENDER_DEBUG_RE.sub("", str(elt))
|
||||
54
env/lib/python3.10/site-packages/draftjs_exporter/engines/lxml.py
vendored
Normal file
54
env/lib/python3.10/site-packages/draftjs_exporter/engines/lxml.py
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from draftjs_exporter.engines.base import Attr, DOMEngine
|
||||
from draftjs_exporter.types import HTML, Tag
|
||||
|
||||
try:
|
||||
from lxml import etree, html
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
NSMAP = {"xlink": "http://www.w3.org/1999/xlink"}
|
||||
|
||||
RENDER_RE = re.compile(r"</?fragment>")
|
||||
|
||||
|
||||
class DOM_LXML(DOMEngine):
|
||||
"""
|
||||
lxml implementation of the DOM API.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_tag(type_: Tag, attr: Optional[Attr] = None) -> etree.Element:
|
||||
nsmap = None
|
||||
|
||||
if attr:
|
||||
if "xlink:href" in attr:
|
||||
attr[f"{{{NSMAP['xlink']}}}href"] = attr.pop("xlink:href")
|
||||
nsmap = NSMAP
|
||||
|
||||
return etree.Element(type_, attrib=attr, nsmap=nsmap)
|
||||
|
||||
@staticmethod
|
||||
def parse_html(markup: HTML) -> etree.Element:
|
||||
return html.fromstring(markup)
|
||||
|
||||
@staticmethod
|
||||
def append_child(elt: etree.Element, child: etree.Element) -> None:
|
||||
if hasattr(child, "tag"):
|
||||
elt.append(child)
|
||||
else:
|
||||
c = etree.Element("fragment")
|
||||
c.text = child
|
||||
elt.append(c)
|
||||
|
||||
@staticmethod
|
||||
def render(elt: etree.Element) -> HTML:
|
||||
return RENDER_RE.sub(
|
||||
"", etree.tostring(elt, method="html", encoding="unicode")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def render_debug(elt: etree.Element) -> HTML:
|
||||
return etree.tostring(elt, method="html", encoding="unicode")
|
||||
122
env/lib/python3.10/site-packages/draftjs_exporter/engines/string.py
vendored
Normal file
122
env/lib/python3.10/site-packages/draftjs_exporter/engines/string.py
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
from html import escape
|
||||
from typing import List, Optional, Sequence, Union
|
||||
|
||||
from draftjs_exporter.engines.base import Attr, DOMEngine
|
||||
from draftjs_exporter.types import HTML, Tag
|
||||
|
||||
# http://w3c.github.io/html/single-page.html#void-elements
|
||||
# https://github.com/html5lib/html5lib-python/blob/0cae52b2073e3f2220db93a7650901f2200f2a13/html5lib/constants.py#L560
|
||||
VOID_ELEMENTS = (
|
||||
"area",
|
||||
"base",
|
||||
"br",
|
||||
"col",
|
||||
"embed",
|
||||
"hr",
|
||||
"img",
|
||||
"input",
|
||||
"link",
|
||||
"meta",
|
||||
"param",
|
||||
"source",
|
||||
"track",
|
||||
"wbr",
|
||||
)
|
||||
|
||||
|
||||
class Elt:
|
||||
"""
|
||||
A DOM element that the string engine manipulates.
|
||||
This class doesn't do much, but the exporter relies on
|
||||
comparing elements by reference so it's useful nonetheless.
|
||||
"""
|
||||
|
||||
__slots__ = ("type", "attr", "children", "markup")
|
||||
|
||||
def __init__(self, type_: Tag, attr: Optional[Attr], markup: HTML = None):
|
||||
self.type = type_
|
||||
self.attr = attr
|
||||
self.children: List["Elt"] = []
|
||||
self.markup = markup
|
||||
|
||||
@staticmethod
|
||||
def from_html(markup: HTML) -> "Elt":
|
||||
return Elt("escaped_html", None, markup)
|
||||
|
||||
|
||||
class DOMString(DOMEngine):
|
||||
"""
|
||||
String concatenation implementation of the DOM API.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_tag(type_: Tag, attr: Optional[Attr] = None) -> Elt:
|
||||
return Elt(type_, attr)
|
||||
|
||||
@staticmethod
|
||||
def parse_html(markup: HTML) -> Elt:
|
||||
"""
|
||||
Allows inserting arbitrary HTML into the exporter output.
|
||||
Treats the HTML as if it had been escaped and was safe already.
|
||||
"""
|
||||
return Elt.from_html(markup)
|
||||
|
||||
@staticmethod
|
||||
def append_child(elt: Elt, child: Elt) -> None:
|
||||
# This check is necessary because the current wrapper_state implementation
|
||||
# has an issue where it inserts elements multiple times.
|
||||
# This must be skipped for text, which can be duplicated.
|
||||
is_existing_ref = child in elt.children and isinstance(child, Elt)
|
||||
if not is_existing_ref:
|
||||
elt.children.append(child)
|
||||
|
||||
@staticmethod
|
||||
def render_attrs(attr: Attr) -> str:
|
||||
attrs = [f' {k}="{escape(v)}"' for k, v in attr.items()]
|
||||
return "".join(attrs)
|
||||
|
||||
@staticmethod
|
||||
def render_children(children: Sequence[Union[HTML, Elt]]) -> HTML:
|
||||
return "".join(
|
||||
[
|
||||
DOMString.render(c)
|
||||
if isinstance(c, Elt)
|
||||
else escape(c, quote=False)
|
||||
for c in children
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def render(elt: Elt) -> HTML:
|
||||
type_ = elt.type
|
||||
attr = DOMString.render_attrs(elt.attr) if elt.attr else ""
|
||||
children = (
|
||||
DOMString.render_children(elt.children) if elt.children else ""
|
||||
)
|
||||
|
||||
if type_ == "fragment":
|
||||
return children
|
||||
|
||||
if type_ in VOID_ELEMENTS:
|
||||
return f"<{type_}{attr}/>"
|
||||
|
||||
if type_ == "escaped_html":
|
||||
return elt.markup # type: ignore
|
||||
|
||||
return f"<{type_}{attr}>{children}</{type_}>"
|
||||
|
||||
@staticmethod
|
||||
def render_debug(elt: Elt) -> HTML:
|
||||
type_ = elt.type
|
||||
attr = DOMString.render_attrs(elt.attr) if elt.attr else ""
|
||||
children = (
|
||||
DOMString.render_children(elt.children) if elt.children else ""
|
||||
)
|
||||
|
||||
if type_ in VOID_ELEMENTS:
|
||||
return f"<{type_}{attr}/>"
|
||||
|
||||
if type_ == "escaped_html":
|
||||
return elt.markup # type: ignore
|
||||
|
||||
return f"<{type_}{attr}>{children}</{type_}>"
|
||||
69
env/lib/python3.10/site-packages/draftjs_exporter/engines/string_compat.py
vendored
Normal file
69
env/lib/python3.10/site-packages/draftjs_exporter/engines/string_compat.py
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
from html import escape
|
||||
from typing import Sequence, Union
|
||||
|
||||
from draftjs_exporter.engines.base import Attr
|
||||
from draftjs_exporter.engines.string import DOMString, Elt, VOID_ELEMENTS
|
||||
from draftjs_exporter.types import HTML
|
||||
|
||||
|
||||
class DOMStringCompat(DOMString):
|
||||
"""
|
||||
The same as DOMString, but with as much backwards-compatibility as possible.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def render_attrs(attr: Attr) -> str:
|
||||
attrs = [f' {k}="{escape(v)}"' for k, v in attr.items()]
|
||||
# Compat: reverts "Remove HTML attributes alphabetical sorting of default string engine ([#129](https://github.com/springload/draftjs_exporter/pull/129))"
|
||||
attrs.sort()
|
||||
return "".join(attrs)
|
||||
|
||||
@staticmethod
|
||||
def render_children(children: Sequence[Union[HTML, Elt]]) -> HTML:
|
||||
return "".join(
|
||||
[
|
||||
DOMStringCompat.render(c) if isinstance(c, Elt)
|
||||
# Compat: reverts "Disable single and double quotes escaping outside of attributes for string engine ([#129](https://github.com/springload/draftjs_exporter/pull/129))"
|
||||
else escape(c, quote=True)
|
||||
for c in children
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def render(elt: Elt) -> HTML:
|
||||
type_ = elt.type
|
||||
attr = DOMStringCompat.render_attrs(elt.attr) if elt.attr else ""
|
||||
children = (
|
||||
DOMStringCompat.render_children(elt.children)
|
||||
if elt.children
|
||||
else ""
|
||||
)
|
||||
|
||||
if type_ == "fragment":
|
||||
return children
|
||||
|
||||
if type_ in VOID_ELEMENTS:
|
||||
return f"<{type_}{attr}/>"
|
||||
|
||||
if type_ == "escaped_html":
|
||||
return elt.markup # type: ignore
|
||||
|
||||
return f"<{type_}{attr}>{children}</{type_}>"
|
||||
|
||||
@staticmethod
|
||||
def render_debug(elt: Elt) -> HTML:
|
||||
type_ = elt.type
|
||||
attr = DOMStringCompat.render_attrs(elt.attr) if elt.attr else ""
|
||||
children = (
|
||||
DOMStringCompat.render_children(elt.children)
|
||||
if elt.children
|
||||
else ""
|
||||
)
|
||||
|
||||
if type_ in VOID_ELEMENTS:
|
||||
return f"<{type_}{attr}/>"
|
||||
|
||||
if type_ == "escaped_html":
|
||||
return elt.markup # type: ignore
|
||||
|
||||
return f"<{type_}{attr}>{children}</{type_}>"
|
||||
112
env/lib/python3.10/site-packages/draftjs_exporter/entity_state.py
vendored
Normal file
112
env/lib/python3.10/site-packages/draftjs_exporter/entity_state.py
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
from draftjs_exporter.command import Command
|
||||
from draftjs_exporter.constants import ENTITY_TYPES
|
||||
from draftjs_exporter.dom import DOM
|
||||
from draftjs_exporter.error import ExporterException
|
||||
from draftjs_exporter.options import Options, OptionsMap
|
||||
from draftjs_exporter.types import (
|
||||
Block,
|
||||
Element,
|
||||
EntityDetails,
|
||||
EntityKey,
|
||||
EntityMap,
|
||||
)
|
||||
|
||||
|
||||
class EntityException(ExporterException):
|
||||
pass
|
||||
|
||||
|
||||
class EntityState:
|
||||
__slots__ = (
|
||||
"entity_options",
|
||||
"entity_map",
|
||||
"entity_stack",
|
||||
"completed_entity",
|
||||
"element_stack",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, entity_options: OptionsMap, entity_map: EntityMap
|
||||
) -> None:
|
||||
self.entity_options = entity_options
|
||||
self.entity_map = entity_map
|
||||
|
||||
self.entity_stack: List[EntityKey] = []
|
||||
self.completed_entity: Optional[EntityKey] = None
|
||||
self.element_stack: List[Element] = []
|
||||
|
||||
def apply(self, command: Command) -> None:
|
||||
if command.name == "start_entity":
|
||||
self.entity_stack.append(command.data)
|
||||
elif command.name == "stop_entity":
|
||||
expected_entity = self.entity_stack[-1]
|
||||
|
||||
if command.data != expected_entity:
|
||||
raise EntityException(
|
||||
f"Expected {expected_entity}, got {command.data}"
|
||||
)
|
||||
|
||||
self.completed_entity = self.entity_stack.pop()
|
||||
|
||||
def has_entity(self) -> List[EntityKey]:
|
||||
return self.entity_stack
|
||||
|
||||
def has_no_entity(self) -> bool:
|
||||
return not self.entity_stack
|
||||
|
||||
def get_entity_details(self, entity_key: EntityKey) -> EntityDetails:
|
||||
details = self.entity_map.get(entity_key)
|
||||
|
||||
if details is None:
|
||||
raise EntityException(
|
||||
f'Entity "{entity_key}" does not exist in the entityMap'
|
||||
)
|
||||
|
||||
return details
|
||||
|
||||
def render_entities(
|
||||
self, style_node: Element, block: Block, blocks: Sequence[Block]
|
||||
) -> Element:
|
||||
# We have a complete (start, stop) entity to render.
|
||||
if self.completed_entity is not None:
|
||||
entity_details = self.get_entity_details(self.completed_entity)
|
||||
options = Options.get(
|
||||
self.entity_options,
|
||||
entity_details["type"],
|
||||
ENTITY_TYPES.FALLBACK,
|
||||
)
|
||||
props = entity_details["data"].copy()
|
||||
props["entity"] = {
|
||||
"type": entity_details["type"],
|
||||
"mutability": entity_details["mutability"]
|
||||
if "mutability" in entity_details
|
||||
else None,
|
||||
"block": block,
|
||||
"blocks": blocks,
|
||||
"entity_range": {"key": self.completed_entity},
|
||||
}
|
||||
|
||||
if len(self.element_stack) == 1:
|
||||
children = self.element_stack[0]
|
||||
else:
|
||||
children = DOM.create_element()
|
||||
|
||||
for n in self.element_stack:
|
||||
DOM.append_child(children, n)
|
||||
|
||||
self.completed_entity = None
|
||||
self.element_stack = []
|
||||
|
||||
# Is there still another entity? (adjacent) if so add the current style_node for it.
|
||||
if self.has_entity():
|
||||
self.element_stack.append(style_node)
|
||||
|
||||
return DOM.create_element(options.element, props, children)
|
||||
|
||||
if self.has_entity():
|
||||
self.element_stack.append(style_node)
|
||||
return None
|
||||
|
||||
return style_node
|
||||
6
env/lib/python3.10/site-packages/draftjs_exporter/error.py
vendored
Normal file
6
env/lib/python3.10/site-packages/draftjs_exporter/error.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
class ExporterException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigException(ExporterException):
|
||||
pass
|
||||
191
env/lib/python3.10/site-packages/draftjs_exporter/html.py
vendored
Normal file
191
env/lib/python3.10/site-packages/draftjs_exporter/html.py
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
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"]))]
|
||||
)
|
||||
105
env/lib/python3.10/site-packages/draftjs_exporter/options.py
vendored
Normal file
105
env/lib/python3.10/site-packages/draftjs_exporter/options.py
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES, INLINE_STYLES
|
||||
from draftjs_exporter.error import ConfigException
|
||||
from draftjs_exporter.types import ConfigMap, Props, RenderableType
|
||||
|
||||
# Internal equivalent of a ConfigMap.
|
||||
OptionsMap = Dict[str, "Options"]
|
||||
|
||||
|
||||
class Options:
|
||||
"""
|
||||
Facilitates querying configuration from a config map.
|
||||
"""
|
||||
|
||||
__slots__ = ("type", "element", "props", "wrapper", "wrapper_props")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type_: str,
|
||||
element: RenderableType,
|
||||
props: Optional[Props] = None,
|
||||
wrapper: RenderableType = None,
|
||||
wrapper_props: Optional[Props] = None,
|
||||
) -> None:
|
||||
self.type = type_
|
||||
self.element = element
|
||||
self.props = props if props else {}
|
||||
self.wrapper = wrapper
|
||||
self.wrapper_props = wrapper_props
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<Options {self.type} {self.element} {self.props} {self.wrapper} {self.wrapper_props}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return str(self)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""
|
||||
Equality used in test code only, not to be relied on for the exporter.
|
||||
"""
|
||||
return str(self) == str(other)
|
||||
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self == other
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(str(self))
|
||||
|
||||
@staticmethod
|
||||
def create(kind_map: ConfigMap, type_: str, fallback_key: str) -> "Options":
|
||||
"""
|
||||
Create an Options object from any mapping.
|
||||
"""
|
||||
if type_ not in kind_map:
|
||||
if fallback_key not in kind_map:
|
||||
raise ConfigException(
|
||||
f'"{type_}" is not in the config and has no fallback'
|
||||
)
|
||||
|
||||
config = kind_map[fallback_key]
|
||||
else:
|
||||
config = kind_map[type_]
|
||||
|
||||
if isinstance(config, dict):
|
||||
if "element" not in config:
|
||||
raise ConfigException(f'"{type_}" does not define an element')
|
||||
|
||||
opts = Options(type_, **config)
|
||||
else:
|
||||
opts = Options(type_, config)
|
||||
|
||||
return opts
|
||||
|
||||
@staticmethod
|
||||
def map(kind_map: ConfigMap, fallback_key: str) -> OptionsMap:
|
||||
options = {}
|
||||
for type_ in kind_map:
|
||||
options[type_] = Options.create(kind_map, type_, fallback_key)
|
||||
|
||||
return options
|
||||
|
||||
@staticmethod
|
||||
def map_blocks(block_map: ConfigMap) -> OptionsMap:
|
||||
return Options.map(block_map, BLOCK_TYPES.FALLBACK)
|
||||
|
||||
@staticmethod
|
||||
def map_styles(style_map: ConfigMap) -> OptionsMap:
|
||||
return Options.map(style_map, INLINE_STYLES.FALLBACK)
|
||||
|
||||
@staticmethod
|
||||
def map_entities(entity_map: ConfigMap) -> OptionsMap:
|
||||
return Options.map(entity_map, ENTITY_TYPES.FALLBACK)
|
||||
|
||||
@staticmethod
|
||||
def get(options: OptionsMap, type_: str, fallback_key: str) -> "Options":
|
||||
try:
|
||||
return options[type_]
|
||||
except KeyError:
|
||||
try:
|
||||
return options[fallback_key]
|
||||
except KeyError:
|
||||
raise ConfigException(
|
||||
f'"{type_}" is not in the config and has no fallback'
|
||||
)
|
||||
0
env/lib/python3.10/site-packages/draftjs_exporter/py.typed
vendored
Normal file
0
env/lib/python3.10/site-packages/draftjs_exporter/py.typed
vendored
Normal file
51
env/lib/python3.10/site-packages/draftjs_exporter/style_state.py
vendored
Normal file
51
env/lib/python3.10/site-packages/draftjs_exporter/style_state.py
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
from typing import List, Sequence
|
||||
|
||||
from draftjs_exporter.command import Command
|
||||
from draftjs_exporter.constants import INLINE_STYLES
|
||||
from draftjs_exporter.dom import DOM
|
||||
from draftjs_exporter.options import Options, OptionsMap
|
||||
from draftjs_exporter.types import Block, Element
|
||||
|
||||
|
||||
class StyleState:
|
||||
"""
|
||||
Handles the creation of inline styles on elements.
|
||||
Receives inline_style commands, and generates the element's `style`
|
||||
attribute from those.
|
||||
"""
|
||||
|
||||
__slots__ = ("styles", "style_options")
|
||||
|
||||
def __init__(self, style_options: OptionsMap) -> None:
|
||||
self.styles: List[str] = []
|
||||
self.style_options = style_options
|
||||
|
||||
def apply(self, command: Command) -> None:
|
||||
if command.name == "start_inline_style":
|
||||
self.styles.append(command.data)
|
||||
elif command.name == "stop_inline_style":
|
||||
self.styles.remove(command.data)
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
return not self.styles
|
||||
|
||||
def render_styles(
|
||||
self, decorated_node: Element, block: Block, blocks: Sequence[Block]
|
||||
) -> Element:
|
||||
node = decorated_node
|
||||
if not self.is_empty():
|
||||
# This will mutate self.styles, but it’s going to be reset after rendering anyway.
|
||||
self.styles.sort(reverse=True)
|
||||
|
||||
# Nest the tags.
|
||||
for style in self.styles:
|
||||
options = Options.get(
|
||||
self.style_options, style, INLINE_STYLES.FALLBACK
|
||||
)
|
||||
props = dict(options.props)
|
||||
props["block"] = block
|
||||
props["blocks"] = blocks
|
||||
props["inline_style_range"] = {"style": style}
|
||||
node = DOM.create_element(options.element, props, node)
|
||||
|
||||
return node
|
||||
35
env/lib/python3.10/site-packages/draftjs_exporter/types.py
vendored
Normal file
35
env/lib/python3.10/site-packages/draftjs_exporter/types.py
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import Any, Callable, Dict, List, Mapping, Union
|
||||
|
||||
# Element represents an instance of a RenderableType. It’s engine-specific so very hard to type.
|
||||
Element = Any
|
||||
# Props are always a dictionary with string keys and arbitrary values.
|
||||
# TODO Other types than string for keys.
|
||||
Props = Dict[str, Any]
|
||||
|
||||
# A DOM tag name.
|
||||
Tag = str
|
||||
# A component function, taking props as a parameter and returning an Element by calling DOM.create_element.
|
||||
Component = Callable[[Props], Element]
|
||||
# What can be rendered: None, DOM tag name, Component.
|
||||
RenderableType = Union[None, Tag, Component]
|
||||
# The output of the exporter.
|
||||
HTML = str
|
||||
|
||||
# The whole config object.
|
||||
Config = Dict[str, Any]
|
||||
# block_map, style_map, entity_decorators.
|
||||
# The dict could be limited to a fixed set of keys, but this would require TypedDict.
|
||||
ConfigMap = Mapping[str, Union[Dict[str, Any], RenderableType]]
|
||||
# composite_decorators.
|
||||
Decorator = Dict[str, Any]
|
||||
CompositeDecorators = List[Decorator]
|
||||
|
||||
# The whole content state. blocks and entity_map.
|
||||
ContentState = Mapping[str, Any]
|
||||
# Blocks have a predetermined set of keys and values, but let’s be permissive without TypedDict.
|
||||
Block = Mapping[str, Any]
|
||||
# Entity key is int in blocks, str in Entity map.
|
||||
EntityKey = str
|
||||
# Entities have fixed keys.
|
||||
EntityDetails = Mapping[str, Any]
|
||||
EntityMap = Mapping[EntityKey, EntityDetails]
|
||||
0
env/lib/python3.10/site-packages/draftjs_exporter/utils/__init__.py
vendored
Normal file
0
env/lib/python3.10/site-packages/draftjs_exporter/utils/__init__.py
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/utils/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/utils/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/draftjs_exporter/utils/__pycache__/module_loading.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/draftjs_exporter/utils/__pycache__/module_loading.cpython-310.pyc
vendored
Normal file
Binary file not shown.
25
env/lib/python3.10/site-packages/draftjs_exporter/utils/module_loading.py
vendored
Normal file
25
env/lib/python3.10/site-packages/draftjs_exporter/utils/module_loading.py
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
|
||||
def import_string(dotted_path: str) -> Any:
|
||||
"""
|
||||
Import a dotted module path and return the attribute/class designated by the
|
||||
last name in the path. Raise ImportError if the import failed.
|
||||
|
||||
Taken from Django:
|
||||
https://github.com/django/django/blob/f6bd00131e687aedf2719ad31e84b097562ca5f2/django/utils/module_loading.py#L7-L24
|
||||
"""
|
||||
try:
|
||||
module_path, class_name = dotted_path.rsplit(".", 1)
|
||||
except ValueError:
|
||||
raise ImportError(f"{dotted_path} doesn't look like a module path")
|
||||
|
||||
module = import_module(module_path)
|
||||
|
||||
try:
|
||||
return getattr(module, class_name)
|
||||
except AttributeError:
|
||||
raise ImportError(
|
||||
f'Module "{module_path}" does not define a "{class_name}" attribute/class'
|
||||
)
|
||||
172
env/lib/python3.10/site-packages/draftjs_exporter/wrapper_state.py
vendored
Normal file
172
env/lib/python3.10/site-packages/draftjs_exporter/wrapper_state.py
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
from typing import List, Optional, Sequence, Union
|
||||
|
||||
from draftjs_exporter.constants import BLOCK_TYPES
|
||||
from draftjs_exporter.dom import DOM
|
||||
from draftjs_exporter.options import Options, OptionsMap
|
||||
from draftjs_exporter.types import Block, Element, Props, RenderableType
|
||||
|
||||
|
||||
class Wrapper:
|
||||
"""
|
||||
A wrapper is an element that wraps other nodes. It gets created
|
||||
when the depth of a block is different than 0, so the DOM elements
|
||||
have the appropriate amount of nesting.
|
||||
"""
|
||||
|
||||
__slots__ = ("depth", "last_child", "type", "props", "elt")
|
||||
|
||||
def __init__(self, depth: int, options: Optional[Options] = None) -> None:
|
||||
self.depth = depth
|
||||
self.last_child = None
|
||||
|
||||
if options:
|
||||
self.type = options.wrapper
|
||||
self.props = options.wrapper_props
|
||||
|
||||
wrapper_props = dict(self.props) if self.props else {}
|
||||
wrapper_props["block"] = {"type": options.type, "depth": depth}
|
||||
|
||||
self.elt = DOM.create_element(self.type, wrapper_props)
|
||||
else:
|
||||
self.type = None
|
||||
self.props = None
|
||||
self.elt = DOM.create_element()
|
||||
|
||||
def is_different(
|
||||
self, depth: int, type_: RenderableType, props: Optional[Props]
|
||||
) -> bool:
|
||||
return depth > self.depth or type_ != self.type or props != self.props
|
||||
|
||||
|
||||
class WrapperStack:
|
||||
"""
|
||||
Stack data structure for element wrappers.
|
||||
The bottom of the stack contains the elements closest to the page body.
|
||||
The top of the stack contains the most nested nodes.
|
||||
"""
|
||||
|
||||
__slots__ = "stack"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.stack: List[Wrapper] = []
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.stack)
|
||||
|
||||
def length(self) -> int:
|
||||
return len(self.stack)
|
||||
|
||||
def append(self, wrapper: Wrapper) -> None:
|
||||
return self.stack.append(wrapper)
|
||||
|
||||
def get(self, index: int) -> Wrapper:
|
||||
return self.stack[index]
|
||||
|
||||
def slice(self, length: int) -> None:
|
||||
self.stack = self.stack[:length]
|
||||
|
||||
def head(self) -> Wrapper:
|
||||
if self.length() > 0:
|
||||
wrapper = self.stack[-1]
|
||||
else:
|
||||
wrapper = Wrapper(-1)
|
||||
|
||||
return wrapper
|
||||
|
||||
def tail(self) -> Wrapper:
|
||||
return self.stack[0]
|
||||
|
||||
|
||||
class WrapperState:
|
||||
"""
|
||||
This class does the initial node building for the tree.
|
||||
It sets elements with the right tag, text content, and props.
|
||||
It adds a wrapper element around elements, if required.
|
||||
"""
|
||||
|
||||
__slots__ = ("block_options", "blocks", "stack")
|
||||
|
||||
def __init__(
|
||||
self, block_options: OptionsMap, blocks: Sequence[Block]
|
||||
) -> None:
|
||||
self.block_options = block_options
|
||||
self.blocks = blocks
|
||||
self.stack = WrapperStack()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"<WrapperState: {self.stack}>"
|
||||
|
||||
def element_for(
|
||||
self, block: Block, block_content: Union[Element, Sequence[Element]]
|
||||
) -> Element:
|
||||
type_ = block["type"] if "type" in block else "unstyled"
|
||||
depth = block["depth"] if "depth" in block else 0
|
||||
options = Options.get(self.block_options, type_, BLOCK_TYPES.FALLBACK)
|
||||
props = dict(options.props)
|
||||
props["block"] = block
|
||||
props["blocks"] = self.blocks
|
||||
|
||||
# Make an element from the options specified in the block map.
|
||||
elt = DOM.create_element(options.element, props, block_content)
|
||||
|
||||
parent = self.parent_for(options, depth, elt)
|
||||
|
||||
return parent
|
||||
|
||||
def parent_for(self, options: Options, depth: int, elt: Element) -> Element:
|
||||
if options.wrapper:
|
||||
parent = self.get_wrapper_elt(options, depth)
|
||||
DOM.append_child(parent, elt)
|
||||
self.stack.stack[-1].last_child = elt
|
||||
else:
|
||||
# Reset the stack if there is no wrapper.
|
||||
if self.stack.length() > 0:
|
||||
self.stack = WrapperStack()
|
||||
parent = elt
|
||||
|
||||
return parent
|
||||
|
||||
def get_wrapper_elt(self, options: Options, depth: int) -> Element:
|
||||
head = self.stack.head()
|
||||
if head.is_different(depth, options.wrapper, options.wrapper_props):
|
||||
self.update_stack(options, depth)
|
||||
|
||||
# If depth is lower than the maximum, we cut the stack.
|
||||
if depth < head.depth:
|
||||
self.stack.slice(depth + 1)
|
||||
|
||||
return self.stack.get(depth).elt
|
||||
|
||||
def update_stack(self, options: Options, depth: int) -> None:
|
||||
if depth >= self.stack.length():
|
||||
# If the depth is gte the stack length, we need more wrappers.
|
||||
depth_levels = range(self.stack.length(), depth + 1)
|
||||
|
||||
for level in depth_levels:
|
||||
new_wrapper = Wrapper(level, options)
|
||||
|
||||
# Determine where to append the new wrapper.
|
||||
if self.stack.head().last_child is None:
|
||||
# If there is no content in the current wrapper, we need
|
||||
# to add an intermediary node.
|
||||
props = dict(options.props)
|
||||
props["block"] = {
|
||||
"type": options.type,
|
||||
"depth": depth,
|
||||
"data": {},
|
||||
}
|
||||
props["blocks"] = self.blocks
|
||||
|
||||
wrapper_parent = DOM.create_element(options.element, props)
|
||||
DOM.append_child(self.stack.head().elt, wrapper_parent)
|
||||
else:
|
||||
# Otherwise we can append at the end of the last child.
|
||||
wrapper_parent = self.stack.head().last_child
|
||||
|
||||
DOM.append_child(wrapper_parent, new_wrapper.elt)
|
||||
|
||||
self.stack.append(new_wrapper)
|
||||
else:
|
||||
# Cut the stack to where it now stops, and add new wrapper.
|
||||
self.stack.slice(depth)
|
||||
self.stack.append(Wrapper(depth, options))
|
||||
Reference in New Issue
Block a user