Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mermaid in markdown #278

Closed
5 tasks
falkoschindler opened this issue Jan 21, 2023 Discussed in #277 · 9 comments
Closed
5 tasks

Mermaid in markdown #278

falkoschindler opened this issue Jan 21, 2023 Discussed in #277 · 9 comments
Assignees
Labels
enhancement New feature or request
Milestone

Comments

@falkoschindler
Copy link
Contributor

falkoschindler commented Jan 21, 2023

Discussed in #277

Originally posted by SebastienDorgan January 21, 2023
Hi,
I am am really new to nicegui and I try to figure out how to make mermaid work with ui.markdown elements.
Here is what I tried:

async def content() -> None:
    mermaid_header =  """\
    <style>
    .mermaid-pre {
        visibility: hidden;
    }
    </style>
    """
    ui.add_head_html(mermaid_header)
    mermaid_footer = """\
        <script type="module" defer>
        import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.esm.min.mjs';
        mermaid.initialize({
            securityLevel: 'loose',
            startOnLoad: true
        });
        let observer = new MutationObserver(mutations => {
            for(let mutation of mutations) {
            mutation.target.style.visibility = "visible";
            }
        });
        document.querySelectorAll("pre.mermaid-pre div.mermaid").forEach(item => {
            observer.observe(item, { 
            attributes: true, 
            attributeFilter: ['data-processed'] });
        });
        </script>
        """
    ui.add_body_html(mermaid_footer)
    ui.label("Streams")
    content = """\
    ```mermaid
    graph TD;
        A-->B;
        A-->C;
        B-->D;
        C-->D;
    ```
    """
    ui.markdown(content , extras=['fenced-code-blocks', 'mermaid'])

unfortunately the result is not what I expected to be:
image
Can you help me?


We identified a bug in markdown2, but found a workaround. So we could extend ui.markdown to support Mermaid.

  • update to markdown2 version 2.4.7
  • let ui.markdown insert additional head and body HTML blocks (only once per client) if "mermaid" extra is used
  • additional ui.mermaid element
  • example(s) for API reference
  • pytest?
@falkoschindler falkoschindler self-assigned this Jan 21, 2023
@falkoschindler falkoschindler added the enhancement New feature or request label Jan 21, 2023
@falkoschindler falkoschindler added this to the v1.1.4 milestone Jan 21, 2023
@falkoschindler
Copy link
Contributor Author

I noticed that the workaround of adding the "mermaid" class ourselves won't work in general since we can't know which pre tag is Mermaid and which is not.

But markdown2 already has a solution which only needs to get released to PyPI. So we'll wait until this fix is out.

trentm/python-markdown2#495

@falkoschindler falkoschindler added waiting Can not be fixed right now and removed waiting Can not be fixed right now labels Jan 22, 2023
@falkoschindler
Copy link
Contributor Author

Ok, markdown2 has been updated and seams to be working. But the integration turns out to be a bit trickier than expected.

When adding the body HTML via add_body_html, the Mermaid graph is not rendered. But if I move the body HTML block further down in index.html, the rendering works. @SebastienDorgan Did you do something special, maybe to postpone the mermaid initialization?

When changing the markdown content, the new graph is not rendered. So far I couldn't find a way to trigger the rendering again. @SebastienDorgan Do you have an idea how we could do that?

I pushed my current implementation to the "mermaid" branch: https://github.com/zauberzeug/nicegui/tree/mermaid

@SebastienDorgan
Copy link

I did nothing special, here is the code that worked 2 days ago with nicegui master and markdown2 master.

from typing import Any, Dict, List

from nicegui import ui

from shunter.model import ProcessSpec, SinkSpec, SourceSpec
from shunter.repository import Document
from shunter.service.application import ApplicationServiceApi


async def content(service: ApplicationServiceApi) -> None:
    mermaid_header = """\
    <style>
    .mermaid-pre {
        visibility: hidden;
    }
    </style>
    """
    ui.add_head_html(mermaid_header)
    mermaid_footer = """\
        <script type="module" defer>
        import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.esm.min.mjs';
        mermaid.initialize({
            securityLevel: 'loose',
            startOnLoad: true
        });
        let observer = new MutationObserver(mutations => {
            for(let mutation of mutations) {
            mutation.target.style.visibility = "visible";
            }
        });
        document.querySelectorAll("pre.mermaid-pre div.mermaid").forEach(item => {
            observer.observe(item, { 
            attributes: true, 
            attributeFilter: ['data-processed'] });
        });
        </script>
        """
    ui.add_body_html(mermaid_footer)
    with ui.left_drawer().style():
        ui.tree(await load_appplication_tree(service), label_key="id", on_select=lambda e: ui.notify(e.value))
    with ui.card().classes("rounded w-full").style("height: 91vh"):
        ui.label("Streams")
        content = """\
        ```mermaid
        graph TD;
            A-->B;
            A-->C;
            B-->D;
            C-->D;
        ```
        """
        graph = ui.markdown(content, extras=["fenced-code-blocks", "mermaid"])
        print(graph)


async def load_appplication_tree(service: ApplicationServiceApi) -> List[Dict[str, Any]]:
    def to_child(doc: Document[ProcessSpec] | Document[SourceSpec] | Document[SinkSpec]):
        return {"id": f"{doc.content.name}:{doc.content.version}"}

    process_children = list(map(to_child, service.get_all_processes()))
    source_children = list(map(to_child, service.get_all_sources()))
    sink_children = list(map(to_child, service.get_all_sinks()))
    return [
        {"id": "Processes", "children": process_children},
        {"id": "Sources", "children": source_children},
        {"id": "Sinks", "children": sink_children},
    ]

Today, with nicegui master, markdown2 2.4.7 it doesn't work. Unfortunately I did not track the commit tags, I don't use this code anymore.

@SebastienDorgan
Copy link

Hi, I don't know where you are in your reflections but I came up with a solution that I think is generic enough but may seem overkill because I do server side rendering. Maybe server side rendering could be useful to implement other functionalities?
I first generate the html with markdow2 as suggested in the wiki. I render the html using chrome headless via selenium. Finally I extract the body from the rendered html to put it in a ui.html element.

The code for the web rendering:

from lxml import etree
from selenium import webdriver
from selenium import webdriver 
from selenium.webdriver.chrome.options import Options
class Renderer:
    def __init__(self) -> None:
        chrome_options = Options()
        chrome_options.add_argument("--disable-extensions")
        chrome_options.add_argument("--disable-gpu")
        chrome_options.add_argument("--no-sandbox") # linux only
        chrome_options.add_argument("--headless")
        # chrome_options.headless = True # also works

        self.driver = webdriver.Chrome(options=chrome_options)
        
    def render_to_div(self, web_content: str) -> str:
        self.driver.get("data:text/html;charset=utf-8," + web_content)
        full_page = self.driver.page_source
        root = etree.XML(full_page, parser=None)
        scripts = root.findall("./body/script")
        body = root.find("body")
        if body is not None:
            for script in scripts:
                body.remove(script)
            body.tag = "div"
            return etree.tostring(body).decode("utf-8")
        raise ValueError("Invalid HTML content, body tag not found")
    
    def render(self, web_content: str) -> str:
        self.driver.get("data:text/html;charset=utf-8," + web_content)
        return self.driver.page_source

The working example (I replaced ``` by ~~~ for the formatting):

from multiprocessing.connection import Client
from shunter.ui import web
from nicegui import ui, Client
import markdown2


web_renderer = web.Renderer()


html_header = """
<html>
  <head>
    <style>
    .mermaid-pre {
        visibility: hidden;
    }
    </style>
  </head>
  <body>"""
  
mermaid_footer = """
<script type="module" defer>
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.esm.min.mjs';
  mermaid.initialize({
    securityLevel: 'loose',
    startOnLoad: true
  });
  let observer = new MutationObserver(mutations => {
    for(let mutation of mutations) {
      mutation.target.style.visibility = "visible";
    }
  });
  document.querySelectorAll("pre.mermaid-pre div.mermaid").forEach(item => {
    observer.observe(item, { 
      attributes: true, 
      attributeFilter: ['data-processed'] });
  });
</script>
"""

content = """\

# Title
~~~mermaid
graph TD;
    A-->B;
    A-->C;
    B-->D;
    C-->D;
~~~
"""


@ui.page("/")
async def raw_html(client: Client):
  with ui.column().classes("w-full 95vh"):
    html = markdown2.markdown(html_header+content+mermaid_footer, extras=["fenced-code-blocks", "mermaid"])
    div_html = web_renderer.render_to_div(html)
    print(div_html)
    ui.html(div_html)

ui.run(show=False)

@falkoschindler
Copy link
Contributor Author

@SebastienDorgan Very interesting! I thought about server-side rendering as well, but started with a standalone ui.mermaid element instead. This turned out to be pretty easy to implement. Maybe I can extend it to the more general ui.markdown.
You're right, selenium sounds like overkill. Do you think performance is ok? I'll have a closer look into your code tomorrow.

@SebastienDorgan
Copy link

The driver is slow to start, but once done the rendering is fast

@falkoschindler
Copy link
Contributor Author

falkoschindler commented Feb 3, 2023

I managed to implement both a ui.mermaid element as well as Mermaid support for ui.markdown. It can be used like this:

Mermaid:

me = ui.mermaid('graph LR; A-->B;')
ui.button('Add', on_click=lambda: ui.mermaid('graph LR; X-->Y;'))
ui.button('Update', on_click=lambda: me.set_content('graph LR; C-->D;'))

Markdown:

ma = ui.markdown('''
Mermaid:

```mermaid
graph LR;
    A-->B;
```
''', extras=['mermaid', 'fenced-code-blocks'])

ui.button('Add', on_click=lambda: ui.markdown('''
More Mermaid:

```mermaid
graph LR;
    X-->Y;
```
''', extras=['mermaid', 'fenced-code-blocks']))

ui.button('Update', on_click=lambda: ma.set_content('''
This has changed:

```mermaid
graph LR;
    C-->D;
```
'''))

The mermaid branch is now ready for review, testing, merge, ...

falkoschindler added a commit that referenced this issue Feb 3, 2023
@rodja
Copy link
Member

rodja commented Feb 5, 2023

In my test Mermaid inside Markdown is still not rendering. Even after clearing the browser cache....

@rodja rodja self-assigned this Feb 5, 2023
@rodja
Copy link
Member

rodja commented Feb 6, 2023

Ah, I had not updated markdown2. After running python3 -m pip install markdown2==2.4.7 it works as expected. The branch has already an updated pyproject.toml so all is good. I've merged the branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants