Explorar el Código

Use Mermaid API directly to fix nested diagram inside `ui.dialog` inside `ui.markdown` (#4722)

This PR is extremely similar in nature to #4692, in that it fixes: 
- broken text as in
https://github.com/zauberzeug/nicegui/pull/4170#issuecomment-2865321544
- showing the mermaid diagrams source code for about half a second
whenever a page is refreshed containing the markdown component using
mermaid, as in
https://github.com/zauberzeug/nicegui/pull/4170#issue-2767685163

by, again invoking the Mermaid API exactly as prescribed at
https://mermaid.js.org/config/usage.html#api-usage

Notable differences compared to past attempt at #4170:

- Uses `mermaid.render` not `mermaid.run`, more easy API integration
- `this.mermaid.initialize({ startOnLoad: false });` should not be
awaited, according to the Mermaid docs.
- Since the SVG is added by me because I control the API usage, I always
make the text hidden no-matter-what in the CSS, and add the SVG in
another class so that it shows up. No need stuff like
`.mermaid[data-processed="true"]``

Notable differences compared to #4692:

- Stores the last rendered Mermaid diagram, since the markdown is
somehow updated when the `ui.dialog` finishes the animation, and
otherwise we'd render twice and there'd be Layout Shift.

There are still tasks to do:

- [x] Address multiple-mermaid situation (I'd imagine this
implementation breaking, especially with the caching of rendered
diagrams).

But the mermaid integration with markdown isn't perfect. There are still
some tasks on my wishlist. I hope to address some of them some day soon:

- Cannot influence the mermaid initialize parameters so as to enable
click events like
https://nicegui.io/documentation/mermaid#handle_click_events
- Cannot listen for mermaid error messages like
https://nicegui.io/documentation/mermaid#handle_errors

---

Test Script (updated to include multi-mermaid scenario):

<details>
<summary> It's a bit long but it works </summary>

```py
from nicegui import ui
import random


def generate_random_graph():
    nodes = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
    two_random_nodes = random.sample(nodes, 2)
    # Generate a random graph with 1 edge
    edges = [f"{two_random_nodes[0]} --> {two_random_nodes[1]}"]
    return f"graph TD; {' '.join(edges)}"


@ui.page('/mermaid_inside_markdown')
def mermaid_inside_markdown():
    md = ui.markdown('''
    - Mermaid inside markdown

    ```mermaid
    graph TD;
        A-->B;
        A-->C;
        B-->D;
        C-->D;
    ```
''', extras=['mermaid'])

# mermaid inside markdown inside ui.dialog


@ui.page('/mermaid_inside_markdown_dialog')
def mermaid_inside_markdown_dialog():
    with ui.dialog(value=True).props('transition-duration=1500'), ui.card():
        my_markdown = ui.markdown('''
        - Mermaid inside markdown inside dialog        

        ```mermaid
        graph TD;
            A-->B;
            A-->C;
            B-->D;
            C-->D;
        ```
        ''', extras=['mermaid'])

        def change_to_random_markdown_with_mermaid():
            my_markdown.set_content(f'''
            - Mermaid inside markdown inside dialog        

            ```mermaid
            {generate_random_graph()}
            ```
            ''')

        def change_to_markdown_with_errorneous_mermaid():
            my_markdown.set_content('''
            - Mermaid inside markdown inside dialog        

            ```mermaid
            graph TD;
                A-->B;
                A->C;
            ```
            ''')

        def change_to_markdown_with_many_mermaid():
            my_markdown.set_content(f'''
            - Mermaid inside markdown inside dialog        

            ```mermaid
            {generate_random_graph()}
            ```
            ```mermaid
            {generate_random_graph()}
            ```
            ```mermaid
            {generate_random_graph()}
            ```
            ''')
        ui.button('Change to random graph', on_click=change_to_random_markdown_with_mermaid)
        ui.button('Change to erroneous graph', on_click=change_to_markdown_with_errorneous_mermaid)
        ui.button('Change to many graphs', on_click=change_to_markdown_with_many_mermaid)

# many meriaid inside markdown inside dialog


@ui.page('/mermaid_inside_markdown_dialog_many')
def mermaid_inside_markdown_dialog_many():
    with ui.dialog(value=True).props('transition-duration=1500'), ui.card():
        my_markdown = ui.markdown(f'''
        - Mermaid inside markdown inside dialog        

        ```mermaid
        {generate_random_graph()}
        ```
        ```mermaid
        {generate_random_graph()}
        ```
        ```mermaid
        {generate_random_graph()}
        ```
        ''', extras=['mermaid'])


ui.link('mermaid inside markdown', '/mermaid_inside_markdown')
ui.link('mermaid inside markdown dialog', '/mermaid_inside_markdown_dialog')
ui.link('mermaid inside markdown dialog many', '/mermaid_inside_markdown_dialog_many')

ui.run(show=False, port=9191)

```

</details>

<img width="253" alt="{7C71FC62-1331-43CC-900D-5682586111C3}"
src="https://github.com/user-attachments/assets/3a2191f9-fe0c-4439-a279-12e6a2562792"
/>

<img width="271" alt="{28A96891-BF42-43B3-8027-D0C7E6CDD3C2}"
src="https://github.com/user-attachments/assets/34c3afd5-528b-4177-bd1b-30a54c612736"
/>

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Evan Chan hace 1 semana
padre
commit
35eac2444f
Se han modificado 3 ficheros con 35 adiciones y 8 borrados
  1. 27 1
      nicegui/elements/markdown.js
  2. 3 0
      nicegui/static/nicegui.css
  3. 5 7
      tests/test_markdown.py

+ 27 - 1
nicegui/elements/markdown.js

@@ -7,12 +7,14 @@ export default {
     await loadResource(window.path_prefix + this.codehilite_css_url);
     await loadResource(window.path_prefix + this.codehilite_css_url);
     if (this.use_mermaid) {
     if (this.use_mermaid) {
       this.mermaid = (await import("mermaid")).default;
       this.mermaid = (await import("mermaid")).default;
+      this.mermaid.initialize({ startOnLoad: false });
       this.renderMermaid();
       this.renderMermaid();
     }
     }
   },
   },
   data() {
   data() {
     return {
     return {
       mermaid: null,
       mermaid: null,
+      diagrams: {},
     };
     };
   },
   },
   updated() {
   updated() {
@@ -20,9 +22,33 @@ export default {
   },
   },
   methods: {
   methods: {
     renderMermaid() {
     renderMermaid() {
+      // render new diagrams
+      const usedKeys = new Set();
       this.$el.querySelectorAll(".mermaid-pre").forEach(async (pre, i) => {
       this.$el.querySelectorAll(".mermaid-pre").forEach(async (pre, i) => {
-        await this.mermaid.run({ nodes: [pre.children[0]] });
+        const key = pre.children[0].innerText + "\n" + i;
+        usedKeys.add(key);
+        if (!this.diagrams[key]) {
+          try {
+            this.diagrams[key] = await this.mermaid.render(this.$el.id + "_mermaid_" + i, pre.children[0].innerText);
+          } catch (error) {
+            this.diagrams[key] = await this.mermaid.render(this.$el.id + "_mermaid_" + i, "error");
+            console.error(error);
+          }
+        }
+        const svgElement = document.createElement("div");
+        svgElement.classList.add("mermaid-svg");
+        svgElement.innerHTML = this.diagrams[key].svg;
+        this.diagrams[key].bindFunctions?.(svgElement);
+        pre.querySelectorAll(".mermaid-svg").forEach((svg) => svg.remove());
+        pre.appendChild(svgElement);
       });
       });
+
+      // prune cached diagrams that are not used anymore
+      for (const key in this.diagrams) {
+        if (!usedKeys.has(key)) {
+          delete this.diagrams[key];
+        }
+      }
     },
     },
   },
   },
   props: {
   props: {

+ 3 - 0
nicegui/static/nicegui.css

@@ -193,6 +193,9 @@
 .nicegui-markdown .codehilite pre {
 .nicegui-markdown .codehilite pre {
   margin: 0.5rem 0;
   margin: 0.5rem 0;
 }
 }
+.nicegui-markdown .mermaid-pre > .mermaid {
+  display: none;
+}
 
 
 /* other NiceGUI elements */
 /* other NiceGUI elements */
 .nicegui-grid {
 .nicegui-grid {

+ 5 - 7
tests/test_markdown.py

@@ -31,9 +31,8 @@ def test_markdown_with_mermaid(screen: Screen):
     screen.open('/')
     screen.open('/')
     screen.wait(0.5)  # wait for Mermaid to render
     screen.wait(0.5)  # wait for Mermaid to render
     screen.should_contain('Mermaid')
     screen.should_contain('Mermaid')
-    assert screen.find_by_tag('svg').get_attribute('id').startswith('mermaid-')
-    node_a = screen.selenium.find_element(By.XPATH, '//span[p[contains(text(), "Node_A")]]')
-    assert node_a.get_attribute('class') == 'nodeLabel'
+    assert screen.find_by_tag('svg').get_attribute('id') == f'{m.html_id}_mermaid_0'
+    assert screen.selenium.find_element(By.XPATH, '//span[p[contains(text(), "Node_A")]]').is_displayed()
 
 
     m.set_content('''
     m.set_content('''
         New:
         New:
@@ -44,8 +43,7 @@ def test_markdown_with_mermaid(screen: Screen):
         ```
         ```
     ''')
     ''')
     screen.should_contain('New')
     screen.should_contain('New')
-    node_c = screen.selenium.find_element(By.XPATH, '//span[p[contains(text(), "Node_C")]]')
-    assert node_c.get_attribute('class') == 'nodeLabel'
+    assert screen.selenium.find_element(By.XPATH, '//span[p[contains(text(), "Node_C")]]').is_displayed()
     screen.should_not_contain('Node_A')
     screen.should_not_contain('Node_A')
 
 
 
 
@@ -59,8 +57,8 @@ def test_markdown_with_mermaid_on_demand(screen: Screen):
 
 
     screen.open('/')
     screen.open('/')
     screen.click('Create Mermaid')
     screen.click('Create Mermaid')
-    screen.should_contain('Node_A')
-    screen.should_contain('Node_B')
+    assert screen.selenium.find_element(By.XPATH, '//span[p[contains(text(), "Node_A")]]').is_displayed()
+    assert screen.selenium.find_element(By.XPATH, '//span[p[contains(text(), "Node_B")]]').is_displayed()
 
 
 
 
 def test_strip_indentation(screen: Screen):
 def test_strip_indentation(screen: Screen):