diff --git a/examples/basic_example.yaml b/examples/basic_example.yaml index dd8ad2db..7be4a564 100644 --- a/examples/basic_example.yaml +++ b/examples/basic_example.yaml @@ -14,10 +14,12 @@ pages: image: ./example_banner.png title: My banner image - Webviz created from configuration file. + - Some other text, potentially with strange letters like Åre, Smørbukk Sør. - - title: Another page + - title: Markdown example content: - - Some other text, potentially with strange letters like Åre, Smørbukk Sør. + - container: Markdown + markdown_file: ./example-markdown.md - title: Table example content: diff --git a/examples/example-markdown.md b/examples/example-markdown.md new file mode 100644 index 00000000..6cfc2b44 --- /dev/null +++ b/examples/example-markdown.md @@ -0,0 +1,39 @@ +# This is a big title + +## This is a somewhat smaller title + +### This is a subsubtitle + +Hi from a Markdown container containing Norwegian letters (æ ø å), some +**bold** letters, _italic_ letters. _You can also **combine** them._ + +#### An unordered list + +* Item 1 +* Item 2 + * Item 2a + * Item 2b + +#### An automatically ordered list + +1. Item 1 +1. Item 2 + 1. Item 2a + 1. Item 2b + +#### An image with a caption + +![Alt text](./example_banner.png "Some caption") + +#### Quote + +> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +> tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, +> quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +#### An example table + +First Header | Second Header +------------ | ------------- +Content Cell | Content Cell +Content Cell | Content Cell diff --git a/setup.py b/setup.py index d80204a6..b74b7b6f 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,6 @@ tests_requires = [ 'chromedriver-binary>=74.0.3729.6.0', - 'markdown>=3.0.1', 'pylint>=2.3.1', 'pytest-dash==2.1.2', 'pycodestyle>=2.5.0', @@ -34,12 +33,14 @@ ], }, install_requires=[ + 'bleach>=3.1.0', 'cryptography>=2.4', 'dash==0.41', 'dash-auth==1.3.2', 'flask-caching>=1.4.0', 'flask-talisman>=0.6.0', 'jinja2>=2.10', + 'markdown>=3.0.1', 'pandas==0.24.1', 'plotly-express>=0.1.3', 'pyarrow>=0.11.1', diff --git a/webviz_config/containers/__init__.py b/webviz_config/containers/__init__.py index 8c514ca7..4075c3eb 100644 --- a/webviz_config/containers/__init__.py +++ b/webviz_config/containers/__init__.py @@ -14,6 +14,7 @@ from ._syntax_highlighter import SyntaxHighlighter from ._table_plotter import TablePlotter from ._embed_pdf import EmbedPdf +from ._markdown import Markdown __all__ = ['ExampleContainer', 'ExampleAssets', @@ -22,7 +23,8 @@ 'DataTable', 'SyntaxHighlighter', 'TablePlotter', - 'EmbedPdf'] + 'EmbedPdf', + 'Markdown'] for entry_point in pkg_resources.iter_entry_points('webviz_config_containers'): globals()[entry_point.name] = entry_point.load() diff --git a/webviz_config/containers/_markdown.py b/webviz_config/containers/_markdown.py new file mode 100644 index 00000000..b9925dcc --- /dev/null +++ b/webviz_config/containers/_markdown.py @@ -0,0 +1,101 @@ +from pathlib import Path +import bleach +import markdown +from markdown.util import etree +from markdown.extensions import Extension +from markdown.inlinepatterns import ImageInlineProcessor, IMAGE_LINK_RE +import dash_core_components as html +from ..webviz_assets import webviz_assets + + +class _WebvizMarkdownExtension(Extension): + def __init__(self, base_path): + self.base_path = base_path + + super(_WebvizMarkdownExtension, self).__init__() + + def extendMarkdown(self, md): + md.inlinePatterns['image_link'] = \ + _MarkdownImageProcessor(IMAGE_LINK_RE, md, self.base_path) + + +class _MarkdownImageProcessor(ImageInlineProcessor): + def __init__(self, image_link_re, md, base_path): + self.base_path = base_path + + super(_MarkdownImageProcessor, self).__init__(image_link_re, md) + + def handleMatch(self, match, data): + image, start, index = super().handleMatch(match, data) + + if image is None or not image.get('title'): + return image, start, index + + src = image.get('src') + caption = image.get('title') + + if src.startswith('http'): + raise ValueError(f'Image path {src} has been given. Only images ' + 'available on the file system can be added.') + + image_path = Path(src) + if not image_path.is_absolute(): + image_path = (self.base_path / image_path).resolve() + + url = webviz_assets.add(image_path) + + image.set('src', url) + image.set('class', '_markdown_image') + + container = etree.Element('span', attrib={'style': 'display: block'}) + container.append(image) + + etree.SubElement(container, + 'span', + attrib={'class': '_markdown_image_caption'} + ).text = caption + + return container, start, index + + +class Markdown: + '''### Include Markdown + +This container renders and includes the content from a Markdown file. Images +are supported, and should in the markdown file be given as either relative +paths to the markdown file itself, or absolute paths. + +* `markdown_file`: Path to the markdown file to render and include. Either + absolute path or relative to the configuration file. +''' + + ALLOWED_TAGS = [ + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'b', 'i', 'strong', 'em', 'tt', + 'p', 'br', 'span', 'div', 'blockquote', 'code', 'hr', + 'ul', 'ol', 'li', 'dd', 'dt', 'img', 'a', 'sub', 'sup', + 'table', 'thead', 'tbody', 'tr', 'th', 'td' + ] + + ALLOWED_ATTRIBUTES = { + '*': ['id', 'class', 'style'], + 'img': ['src', 'alt', 'title'], + 'a': ['href', 'alt', 'title'] + } + + def __init__(self, markdown_file: Path): + self.html = bleach.clean( + markdown.markdown( + markdown_file.read_text(), + extensions=['tables', + 'sane_lists', + _WebvizMarkdownExtension( + base_path=markdown_file.parent + )]), + Markdown.ALLOWED_TAGS, + Markdown.ALLOWED_ATTRIBUTES + ) + + @property + def layout(self): + return html.Markdown(self.html, dangerously_allow_html=True) diff --git a/webviz_config/static/assets/webviz_config.css b/webviz_config/static/assets/webviz_config.css index 8b779324..61cd74dd 100644 --- a/webviz_config/static/assets/webviz_config.css +++ b/webviz_config/static/assets/webviz_config.css @@ -16,3 +16,19 @@ background-position: center; } + +._markdown_image { + + display: block; + margin: auto; + max-width: 90%; + max-height: 90vw; + +} + +._markdown_image_caption { + + display: block; + text-align: center; + +} diff --git a/webviz_config/webviz_assets.py b/webviz_config/webviz_assets.py index 332901cd..d1f72a86 100644 --- a/webviz_config/webviz_assets.py +++ b/webviz_config/webviz_assets.py @@ -50,6 +50,8 @@ def add(self, filename): if filename not in self._assets.values(): assigned_id = self._generate_id(path.name) self._assets[assigned_id] = filename + else: + assigned_id = {v: k for k, v in self._assets.items()}[filename] return os.path.normcase(os.path.join(self._base_folder(), assigned_id))