"""Shortcode rendering. Shortcodes are little snippets between square brackets, which can be rendered into HTML. Markdown passes such snippets unchanged to its HTML output, so this module assumes its input is HTML-with-shortcodes. See mulholland.xyz/docs/shortcodes/. {iframe src='http://hey' has-cap='subscriber'} NOTE: nested braces fail, so something like {shortcode abc='{}'} is not supported. NOTE: only single-line shortcodes are supported for now, due to the need to pass them though Markdown unscathed. """ import html as html_module # I want to be able to use the name 'html' in local scope. import logging import re import typing import urllib.parse import shortcodes _parser: shortcodes.Parser = None _commented_parser: shortcodes.Parser = None log = logging.getLogger(__name__) def shortcode(name: str): """Class decorator for shortcodes.""" def decorator(cls): assert hasattr(cls, '__call__'), '@shortcode should be used on callables.' if isinstance(cls, type): instance = cls() else: instance = cls shortcodes.register(name)(instance) return cls return decorator @shortcode('test') class Test: def __call__(self, context: typing.Any, content: str, pargs: typing.List[str], kwargs: typing.Dict[str, str]) -> str: """Just for testing. "{test abc='def'}" → "
{html}
' kwargs['class'] = f'shortcode {kwargs.get("class", "")}'.strip() element = ET.Element('iframe', kwargs) html = ET.tostring(element, encoding='unicode', method='html', short_empty_elements=True) return html def _get_parser() -> typing.Tuple[shortcodes.Parser, shortcodes.Parser]: """Return the shortcodes parser, create it if necessary.""" global _parser, _commented_parser if _parser is None: start, end = '{}' _parser = shortcodes.Parser(start, end) _commented_parser = shortcodes.Parser(f'') return _parser, _commented_parser def render_commented(text: str, context: typing.Any = None) -> str: """Parse and render HTML-commented shortcodes. Expects shortcodes like "", as output by escape_html(). """ _, parser = _get_parser() # TODO(Sybren): catch exceptions and handle those gracefully in the response. try: return parser.parse(text, context) except shortcodes.InvalidTagError as ex: return html_module.escape('{%s}' % ex) except shortcodes.RenderingError as ex: return html_module.escape('{unable to render tag: %s}' % str(ex.__cause__ or ex)) def render(text: str, context: typing.Any = None) -> str: """Parse and render shortcodes.""" parser, _ = _get_parser() # TODO(Sybren): catch exceptions and handle those gracefully in the response. return parser.parse(text, context) def comment_shortcodes(html: str) -> str: """Escape shortcodes in HTML comments. This is required to pass the shortcodes as-is through Markdown. Render the shortcodes afterwards with render_commented(). >>> comment_shortcodes("text\\n{shortcode abc='def'}\\n") "text\\n\\n" """ parser, _ = _get_parser() return parser.regex.sub(r'', html)