Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
> All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [[v0.7.0]](https://github.com/springload/draftjs_exporter/releases/tag/v0.7.0) - 2017-02-15

### Added

- Add support for decorators thanks to @su27 (#16, #17).

## [[v0.6.2]](https://github.com/springload/draftjs_exporter/releases/tag/v0.6.2) - 2017-01-18

### Added

- Add profiling tooling thanks to [@su27](https://github.com/su27) [#31](https://github.com/springload/draftjs_exporter/issues/31).
- Add more common entity types in constants (#34)
- Add profiling tooling thanks to @su27 (#31).
- Add more common entity types in constants (#34).

### Fixed

- Stop mutating entity data when rendering entities (#36)
- Stop mutating entity data when rendering entities (#36).

## [[v0.6.1]](https://github.com/springload/draftjs_exporter/releases/tag/v0.6.1) - 2016-12-21

Expand Down
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ This project adheres to `Semantic Versioning`_, and measures performance and `co
* Convert line breaks to ``<br>`` elements.
* Define any attribute in the block map – custom class names for elements.
* React-like API to create custom entity decorators.
* React-like API to create composite decorators for text.
* Automatic conversion of entity data to HTML attributes (int & boolean to string, ``className`` to ``class``).
* Wrapped blocks (``<li>`` elements go inside ``<ul>``).
* Nested wrapped blocks (multiple list levels, arbitrary type and depth).
Expand Down
49 changes: 49 additions & 0 deletions draftjs_exporter/composite_decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import absolute_import, unicode_literals

from operator import itemgetter
from draftjs_exporter.dom import DOM


def get_decorations(decorators, text):
occupied = {}
decorations = []

for decorator in decorators:
for match in decorator.SEARCH_RE.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, text, block_type):
decorations = get_decorations(decorators, text)

pointer = 0
for begin, end, match, decorator in decorations:
if pointer < begin:
yield DOM.create_text_node(text[pointer:begin])

yield decorator.render({
'children': match.group(0),
'match': match,
'block_type': block_type,
})
pointer = end

if pointer < len(text):
yield DOM.create_text_node(text[pointer:])


def render_decorators(decorators, text, block_type):
decorated_node = DOM.create_document_fragment()

for decorated_child in apply_decorators(decorators, text, block_type):
DOM.append_child(decorated_node, decorated_child)

return decorated_node
19 changes: 10 additions & 9 deletions draftjs_exporter/entity_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ def apply(self, command):
elif command.name == 'stop_entity':
self.stop_command(command)

def is_empty(self):
return not self.entity_stack

def get_entity_details(self, command):
key = str(command.data)
details = self.entity_map.get(key)
Expand Down Expand Up @@ -53,15 +56,15 @@ def stop_command(self, command):
self.entity_stack.pop()

def render_entitities(self, root_element, style_node):
stack_start = DOM.create_document_fragment()
DOM.append_child(root_element, stack_start)

element_stack = [stack_start]
new_element = stack_start

if len(self.entity_stack) == 0:
if self.is_empty():
DOM.append_child(root_element, style_node)
else:
stack_start = DOM.create_document_fragment()
DOM.append_child(root_element, stack_start)

element_stack = [stack_start]
new_element = stack_start

for entity_details in self.entity_stack:
decorator = self.get_entity_decorator(entity_details)
props = entity_details.copy()
Expand All @@ -71,5 +74,3 @@ def render_entitities(self, root_element, style_node):
new_element = decorator.render(props)
DOM.append_child(element_stack[-1], new_element)
element_stack.append(new_element)

return new_element
13 changes: 11 additions & 2 deletions draftjs_exporter/html.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import absolute_import, unicode_literals

from draftjs_exporter.command import Command
from draftjs_exporter.composite_decorators import 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.style_state import StyleState
from draftjs_exporter.wrapper_state import WrapperState
Expand All @@ -17,6 +19,7 @@ def __init__(self, config=None):
config = {}

self.entity_decorators = config.get('entity_decorators', {})
self.composite_decorators = config.get('composite_decorators', [])
self.block_map = config.get('block_map', BLOCK_MAP)
self.style_map = config.get('style_map', STYLE_MAP)

Expand Down Expand Up @@ -44,8 +47,14 @@ def render_block(self, block, entity_map):
entity_state.apply(command)
self.style_state.apply(command)

style_node = self.style_state.create_node(text)
entity_state.render_entitities(element, style_node)
# Decorators are not rendered inside entities.
if entity_state.is_empty() and len(self.composite_decorators) > 0:
decorated_node = render_decorators(self.composite_decorators, text, block.get('type', None))
else:
decorated_node = DOM.create_text_node(text)

styled_node = self.style_state.render_styles(decorated_node)
entity_state.render_entitities(element, styled_node)

def build_command_groups(self, block):
"""
Expand Down
34 changes: 8 additions & 26 deletions draftjs_exporter/style_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def apply(self, command):
elif command.name == 'stop_inline_style':
self.styles.remove(command.data)

def is_unstyled(self):
def is_empty(self):
return not self.styles

def get_style_tags(self):
Expand All @@ -55,28 +55,11 @@ def get_style_value(self):

return ''.join(sorted(rules))

def replace_linebreaks(self, text):
lines = text.split('\n')

if len(lines) > 1:
wrapper = DOM.create_document_fragment()

DOM.append_child(wrapper, DOM.create_text_node(lines[0]))

for l in lines[1:]:
DOM.append_child(wrapper, DOM.create_element('br'))
DOM.append_child(wrapper, DOM.create_text_node(l))
else:
wrapper = DOM.create_text_node(text)

return wrapper

def create_node(self, text):
text_lines = self.replace_linebreaks(text)

if self.is_unstyled():
node = text_lines
def render_styles(self, text_node):
if self.is_empty():
node = text_node
else:
style = self.get_style_value()
tags = self.get_style_tags()
node = DOM.create_element(tags[0])
child = node
Expand All @@ -88,10 +71,9 @@ def create_node(self, text):
DOM.append_child(child, new_child)
child = new_child

style_value = self.get_style_value()
if style_value:
DOM.set_attribute(child, 'style', style_value)
if style:
DOM.set_attribute(child, 'style', style)

DOM.append_child(child, text_lines)
DOM.append_child(child, text_node)

return node
41 changes: 37 additions & 4 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import absolute_import, unicode_literals

import codecs
import re

from draftjs_exporter.constants import BLOCK_TYPES, ENTITY_TYPES
from draftjs_exporter.defaults import BLOCK_MAP, STYLE_MAP
Expand Down Expand Up @@ -35,12 +36,44 @@ def render(self, props):
return DOM.create_element('a', {'href': href}, props['children'])


class BR:
"""
Replace line breaks (\n) with br tags.
"""
SEARCH_RE = re.compile(r'\n')

def render(self, props):
# Do not process matches inside code blocks.
if props['block_type'] == BLOCK_TYPES.CODE:
return props['children']

return DOM.create_element('br')


class Hashtag:
"""
Wrap hashtags in spans with a specific class.
"""
SEARCH_RE = re.compile(r'#\w+')

def render(self, props):
# Do not process matches inside code blocks.
if props['block_type'] == BLOCK_TYPES.CODE:
return props['children']

return DOM.create_element('span', {'class': 'hashtag'}, props['children'])


config = {
'entity_decorators': {
ENTITY_TYPES.LINK: Link(),
ENTITY_TYPES.IMAGE: Image(),
ENTITY_TYPES.HORIZONTAL_RULE: HR(),
},
'composite_decorators': [
BR(),
Hashtag(),
],
# Extend/override the default block map.
'block_map': dict(BLOCK_MAP, **{
BLOCK_TYPES.HEADER_TWO: {
Expand Down Expand Up @@ -104,15 +137,15 @@ def render(self, props):
},
{
'key': '5384u',
'text': 'Everyone 🍺 Springload applies the best principles of UX to their work.',
'text': 'Everyone 🍺 Springload applies the best #principles of UX to their work.',
'type': 'blockquote',
'depth': 0,
'inlineStyleRanges': [],
'entityRanges': []
},
{
'key': 'eelkd',
'text': 'The design decisions we make building tools and services for your customers are based on empathy for what your customers need.',
'text': 'The design decisions we make building #tools and #services for your customers are based on empathy for what your customers need.',
'type': 'unstyled',
'depth': 0,
'inlineStyleRanges': [],
Expand Down Expand Up @@ -186,8 +219,8 @@ def render(self, props):
'inlineStyleRanges': [],
'entityRanges': [
{
'offset': 0,
'length': 71,
'offset': 53,
'length': 12,
'key': 1
}
]
Expand Down
Loading