markdown.py 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. import os
  2. import re
  3. from functools import lru_cache
  4. from typing import List
  5. import markdown2
  6. from pygments.formatters import HtmlFormatter
  7. from ..dependencies import register_component
  8. from .mixins.content_element import ContentElement
  9. register_component('markdown', __file__, 'markdown.js', ['lib/mermaid.min.js'])
  10. class Markdown(ContentElement):
  11. def __init__(self, content: str = '', *, extras: List[str] = ['fenced-code-blocks', 'tables']) -> None:
  12. """Markdown Element
  13. Renders Markdown onto the page.
  14. :param content: the Markdown content to be displayed
  15. :param extras: list of `markdown2 extensions <https://github.com/trentm/python-markdown2/wiki/Extras#implemented-extras>`_ (default: `['fenced-code-blocks', 'tables']`)
  16. """
  17. self.extras = extras
  18. super().__init__(tag='markdown', content=content)
  19. self._props['codehilite_css'] = HtmlFormatter(nobackground=True).get_style_defs('.codehilite')
  20. def on_content_change(self, content: str) -> None:
  21. html = prepare_content(content, extras=' '.join(self.extras))
  22. if self._props.get('innerHTML') != html:
  23. self._props['innerHTML'] = html
  24. self.run_method('update', html)
  25. @lru_cache(maxsize=int(os.environ.get('MARKDOWN_CONTENT_CACHE_SIZE', '1000')))
  26. def prepare_content(content: str, extras: str) -> str:
  27. html = markdown2.markdown(remove_indentation(content), extras=extras.split())
  28. return apply_tailwind(html) # we need explicit Markdown styling because tailwind CSS removes all default styles
  29. def apply_tailwind(html: str) -> str:
  30. rep = {
  31. '<h1': '<h1 class="text-5xl mb-4 mt-6"',
  32. '<h2': '<h2 class="text-4xl mb-3 mt-5"',
  33. '<h3': '<h3 class="text-3xl mb-2 mt-4"',
  34. '<h4': '<h4 class="text-2xl mb-1 mt-3"',
  35. '<h5': '<h5 class="text-1xl mb-0.5 mt-2"',
  36. '<a': '<a class="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"',
  37. '<ul': '<ul class="list-disc ml-6"',
  38. '<p>': '<p class="mb-2">',
  39. r'<div\ class="codehilite">': '<div class="codehilite mb-2 p-2">',
  40. '<code': '<code style="background-color: transparent"',
  41. }
  42. pattern = re.compile('|'.join(rep.keys()))
  43. return pattern.sub(lambda m: rep[re.escape(m.group(0))], html)
  44. def remove_indentation(text: str) -> str:
  45. """Remove indentation from a multi-line string based on the indentation of the first non-empty line."""
  46. lines = text.splitlines()
  47. while lines and not lines[0].strip():
  48. lines.pop(0)
  49. if not lines:
  50. return ''
  51. indentation = len(lines[0]) - len(lines[0].lstrip())
  52. return '\n'.join(line[indentation:] for line in lines)