Skip to content

Commit

Permalink
Merge pull request #162 from PlaidWeb/fluffy/136-html-renditions
Browse files Browse the repository at this point in the history
Support entry and image links in plain-HTML entries
  • Loading branch information
fluffy-critter committed Feb 14, 2019
2 parents 4fd7418 + 4f5704b commit 2a777c6
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 76 deletions.
8 changes: 6 additions & 2 deletions publ/entry.py
Expand Up @@ -25,6 +25,7 @@
from . import utils
from . import cards
from . import caching
from . import html_entry
from .utils import CallableProxy, TrueCallableProxy, make_slug

logger = logging.getLogger(__name__) # pylint: disable=invalid-name
Expand Down Expand Up @@ -99,7 +100,7 @@ def type(self):
@cached_property
def status(self):
""" Returns a string version of the entry status """
return Model.PublishStatus(self.status)
return model.PublishStatus(self.status)

@cached_property
def next(self):
Expand Down Expand Up @@ -246,7 +247,10 @@ def _get_markup(self, text, is_markdown, **kwargs):
config=kwargs,
search_path=self.search_path)

return flask.Markup(text)
return html_entry.process(
text,
config=kwargs,
search_path=self.search_path)

def _get_card(self, text, **kwargs):
""" Render out the tags for a Twitter/OpenGraph card for this entry. """
Expand Down
102 changes: 102 additions & 0 deletions publ/html_entry.py
@@ -0,0 +1,102 @@
# html_entry.py
""" HTML entry processing functionality """

import misaka
import flask

from . import utils, links, image


class HTMLEntry(utils.HTMLTransform):
""" An HTML manipulator to fixup src and href attributes """

def __init__(self, config, search_path):
super().__init__()

self._search_path = search_path
self._config = config

def handle_data(self, data):
self.append(data)

def handle_starttag(self, tag, attrs):
""" Handle a start tag """
self._handle_tag(tag, attrs, False)

def handle_endtag(self, tag):
""" Handle an end tag """
self.append('</' + tag + '>')

def handle_startendtag(self, tag, attrs):
""" Handle a self-closing tag """
self._handle_tag(tag, attrs, True)

def _handle_tag(self, tag, attrs, self_closing):
""" Handle a tag.
attrs -- the attributes of the tag
self_closing -- whether this is self-closing
"""

if tag.lower() == 'img':
attrs = self._image_attrs(attrs)

# Remap the attributes
out_attrs = []
for key, val in attrs:
if (key.lower() == 'href'
or (key.lower() == 'src' and not tag.lower() == 'img')):
out_attrs.append((key, links.remap_path(
val, self._search_path, self._config.get('absolute'))))
else:
out_attrs.append((key, val))

self.append(
utils.make_tag(
tag,
out_attrs,
self_closing))

def _image_attrs(self, attrs):
""" Rewrite the SRC attribute on an <img> tag, possibly adding a SRCSET.
"""

path = None
config = {**self._config}

for key, val in attrs:
if key.lower() == 'width' or key.lower() == 'height':
try:
config[key.lower()] = int(val)
except ValueError:
pass
elif key.lower() == 'src':
path = val

img_path, img_args, _ = image.parse_image_spec(path)
img = image.get_image(img_path, self._search_path)

for key, val in img_args.items():
print(key, val)
if val and key not in config:
config[key] = val

try:
img_attrs = img.get_img_attrs(**config)
except FileNotFoundError as error:
return [('data-publ-error', 'file not found: {}'.format(error.filename))]

# return the original attr list with the computed overrides in place
return [(key, val) for key, val in attrs if key not in img_attrs] + [i for i in img_attrs.items()]


def process(text, config, search_path):
""" Process an HTML entry's HTML """
processor = HTMLEntry(config, search_path)
processor.feed(text)
text = processor.get_data()

if not config.get('no_smartquotes'):
text = misaka.smartypants(text)

return flask.Markup(text)
62 changes: 34 additions & 28 deletions publ/image.py
Expand Up @@ -52,13 +52,21 @@ def get_rendition(self, output_scale=1, **kwargs):
Returns: a tuple of (url, size) for the image. """

@abstractmethod
def get_img_attrs(self, style=None, **kwargs):
""" Get an attribute list (src, srcset, style, et al) for the image.
style -- an optional list of CSS style fragments
Returns: a dict of attributes e.g. {'src':'foo.jpg','srcset':'foo.jpg 1x, bar.jpg 2x']
"""

def get_img_tag(self, title='', alt_text='', **kwargs):
""" Build a <img> tag for the image with the specified options.
Returns: an HTML fragment. """

try:

style = []

for key in ('img_style', 'style'):
Expand All @@ -76,15 +84,25 @@ def get_img_tag(self, title='', alt_text='', **kwargs):
if shape:
style.append("shape-outside: url('{}')".format(shape))

attrs = {
'alt_text': alt_text,
'title': title,
**self.get_img_attrs(style, **kwargs)
}

return flask.Markup(
self._wrap_link_target(
kwargs,
self._img_tag(title, alt_text, style, **kwargs),
utils.make_tag(
'img', attrs, start_end=kwargs.get('xhtml')),
title))
except FileNotFoundError as error:
return flask.Markup(
'<span class="error">File not found: {}</span>'.format(
error.filename))
text = '<span class="error">Image not found: <code>{}</code>'.format(
html.escape(error.filename))
if ' ' in error.filename:
text += ' (Did you forget a <code>|</code>?)'
text += '</span>'
return flask.Markup(text)

def _get_shape_style(self, **kwargs):
shape = kwargs['shape']
Expand All @@ -104,10 +122,6 @@ def _get_shape_style(self, **kwargs):
url, _ = other_image.get_rendition(1, **size_args)
return url

@abstractmethod
def _img_tag(self, title, alt_text, style, **kwargs):
""" Implemented by the subclasses for actually emitting the HTML """

def get_css_background(self, uncomment=False, **kwargs):
""" Get the CSS background attributes for an element.
Expand Down Expand Up @@ -520,23 +534,21 @@ def _get_renditions(self, kwargs):

return (img_1x, img_2x, size)

def _img_tag(self, title='', alt_text='', style=None, **kwargs):
def get_img_attrs(self, style=None, **kwargs):
""" Get an <img> tag for this image, hidpi-aware """

# Get the 1x and 2x renditions
img_1x, img_2x, size = self._get_renditions(kwargs)

return utils.make_tag('img', {
return {
'src': img_1x,
'width': size[0],
'height': size[1],
'srcset': "{} 1x, {} 2x".format(img_1x, img_2x) if img_1x != img_2x else None,
'style': ';'.join(style) if style else None,
'class': kwargs.get('class', kwargs.get('img_class')),
'id': kwargs.get('img_id'),
'title': title,
'alt': alt_text
}, start_end=kwargs.get('xhtml'))
'id': kwargs.get('img_id')
}

def _css_background(self, **kwargs):
""" Get the CSS specifiers for this as a hidpi-capable background image """
Expand Down Expand Up @@ -599,10 +611,8 @@ def get_rendition(self, output_scale=1, **kwargs):
# pylint: disable=unused-argument
return self.url, None

def _img_tag(self, title='', alt_text='', style=None, **kwargs):
def get_img_attrs(self, style=None, **kwargs):
attrs = {
'title': title,
'alt': alt_text,
'class': kwargs.get('class', kwargs.get('img_class')),
'id': kwargs.get('img_id'),
}
Expand Down Expand Up @@ -645,7 +655,7 @@ def _img_tag(self, title='', alt_text='', style=None, **kwargs):
if style_parts:
attrs['style'] = ';'.join(style_parts)

return utils.make_tag('img', attrs, start_end=kwargs.get('xhtml'))
return attrs

def _css_background(self, **kwargs):
""" Get the CSS background-image for the remote image """
Expand All @@ -667,9 +677,9 @@ def get_rendition(self, output_scale=1, **kwargs):
url = utils.static_url(self.path, absolute=kwargs.get('absolute'))
return RemoteImage(url, self.search_path).get_rendition(output_scale, **kwargs)

def _img_tag(self, title='', alt_text='', style=None, **kwargs):
def get_img_attrs(self, style=None, **kwargs):
url = utils.static_url(self.path, absolute=kwargs.get('absolute'))
return RemoteImage(url, self.search_path)._img_tag(title, alt_text, style, **kwargs)
return RemoteImage(url, self.search_path).get_img_attrs(style, **kwargs)

def _css_background(self, **kwargs):
# pylint: disable=arguments-differ
Expand All @@ -692,14 +702,10 @@ def get_rendition(self, output_scale=1, **kwargs):
raise FileNotFoundError(
errno.ENOENT, os.strerror(errno.ENOENT), self.path)

def _img_tag(self, title='', alt_text='', style=None, **kwargs):
def get_img_attrs(self, style=None, **kwargs):
# pylint:disable=unused-argument
text = '<span class="error">Image not found: <code>{}</code>'.format(
html.escape(self.path))
if ' ' in self.path:
text += ' (Did you forget a <code>|</code>?)'
text += '</span>'
return flask.Markup(text)
raise FileNotFoundError(
errno.ENOENT, os.strerror(errno.ENOENT), self.path)

def _css_background(self, **kwargs):
return '/* not found: {} */'.format(self.path)
Expand Down
24 changes: 24 additions & 0 deletions publ/links.py
@@ -0,0 +1,24 @@
# links.py
""" Functions for manipulating outgoing HTML links """

from . import image
from . import utils


def remap_path(path, search_path, absolute=False):
""" Remap a link or source target to an appropriate entry or image rendition """

# Remote or static URL: do the thing
if not path.startswith('//') and not '://' in path:
path, sep, anchor = path.partition('#')
entry = utils.find_entry(path, search_path)
if entry:
return entry.link(absolute=absolute) + sep + anchor

# Image URL: do the thing
img_path, img_args, _ = image.parse_image_spec(path)
img = image.get_image(img_path, search_path)
if isinstance(img, image.LocalImage):
path, _ = img.get_rendition(**img_args)

return utils.remap_link_target(path, absolute)
52 changes: 7 additions & 45 deletions publ/markdown.py
@@ -1,10 +1,7 @@
# markdown.py
""" handler for markdown formatting """

from __future__ import absolute_import
""" markdown formatting functionality """

import logging
import html.parser
import re

import misaka
Expand All @@ -14,7 +11,7 @@
import pygments.formatters
import pygments.lexers

from . import image, utils
from . import image, utils, links

TITLE_EXTENSIONS = (
'strikethrough', 'math',
Expand Down Expand Up @@ -121,7 +118,8 @@ def blockcode(self, text, lang):
def link(self, content, link, title=''):
""" Emit a link, potentially remapped based on our embed or static rules """

link = self._remap_path(link)
link = links.remap_path(link, self._search_path,
self._config.get('absolute'))

return '{}{}</a>'.format(
utils.make_tag('a', {
Expand All @@ -143,24 +141,6 @@ def paragraph(content):
text = re.sub(r'<p>\s*</p>', r'', text)
return text or ' '

def _remap_path(self, path):
""" Remap a path to an appropriate URL """

# Remote or static URL: do the thing
if not path.startswith('//') and not '://' in path:
path, sep, anchor = path.partition('#')
entry = utils.find_entry(path, self._search_path)
if entry:
return entry.link(self._config) + sep + anchor

# Image URL: do the thing
img_path, img_args, _ = image.parse_image_spec(path)
img = image.get_image(img_path, self._search_path)
if isinstance(img, image.LocalImage):
path, _ = img.get_rendition(**img_args)

return utils.remap_link_target(path, self._config.get('absolute'))

def _render_image(self, spec, container_args, alt_text=None):
""" Render an image specification into an <img> tag """

Expand Down Expand Up @@ -228,29 +208,11 @@ def header(content, level):
return content


class HTMLStripper(html.parser.HTMLParser):
""" A utility class to strip HTML from a string; based on
https://stackoverflow.com/a/925630/318857 """

def __init__(self):
super().__init__()

self.reset()
self.strict = False
self.convert_charrefs = True
self.fed = []
class HTMLStripper(utils.HTMLTransform):
""" Strip all HTML tags from a document """

def handle_data(self, data):
""" Append the text data """
self.fed.append(data)

def get_data(self):
""" Concatenate the output """
return ''.join(self.fed)

def error(self, message):
""" Deprecated, per https://bugs.python.org/issue31844 """
return message
self.append(data)


def render_title(text, markup=True, no_smartquotes=False):
Expand Down

0 comments on commit 2a777c6

Please sign in to comment.