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

Server Side Support #3650

Open
Yash-Singh1 opened this issue Oct 12, 2022 · 22 comments
Open

Server Side Support #3650

Yash-Singh1 opened this issue Oct 12, 2022 · 22 comments
Labels
Discussion Discussion which may lead to separate issues or PRs Status: Triage Needs to be verified, categorized, etc Type: Other Not an enhancement or a bug Type: Question

Comments

@Yash-Singh1
Copy link
Member

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

Support for server-side rendering of Mermaid.js such as Node.js

Describe the solution you'd like
A clear and concise description of what you want to happen.

This has been discussed in #559 and #1183. I looked into the source code and noticed that a browser environment is required to precompute widths/heights. Just saw this project, maybe we can use Vercel's Satori. It internally uses Facebook's Yoga, a cross-compatible flexbox implementation.

@Yash-Singh1 Yash-Singh1 added Type: Enhancement New feature or request Status: Triage Needs to be verified, categorized, etc labels Oct 12, 2022
@aloisklink
Copy link
Member

This would also greatly help the mermaid-cli project! It currently uses puppeteer to create a browser instance to render Mermaid diagrams, but that's a very very heavy program (and has caused mermaid-cli to be removed from homebrew: mermaid-js/mermaid-cli#288 (comment))

Alternative Solutions

svgdom might help as well, as it adds a bunch of SVG DOM features to Node.JS.

Issues

We'd most likely have to change the default font to an open-source font too, since the default fonts are owned by Microsoft:

fontFamily: '"trebuchet ms", verdana, arial, sans-serif;',

I'd recommend https://fonts.google.com/specimen/Atkinson+Hyperlegible as a good default, since it's designed for people with low vision, and it's available under the Open Font License. The license is very permissive, we just need to:

At a minimum you must include the copyright statement, the license notice and the license text. A mention of this information in your About box or Changelog, with a link to where the font package is from, is good practice [...]

From https://scripts.sil.org/cms/scripts/page.php?item_id=OFL-FAQ_web#2a57c6bb (Question 1.20)

@sidharthv96
Copy link
Member

This could also enable the generation of diagram previews when linking to live editor.

@knsv
Copy link
Collaborator

knsv commented Oct 14, 2022

Lots of possibilities with this one.

@slorber
Copy link

slorber commented Oct 14, 2022

We just finished the integration of Mermaid in Docusauurs (facebook/docusaurus#7490)

We would also really like to have SSR support:

  • mermaid increases the bundle size, but doc sites should rather remain light
  • rendering the SVG only on the client produces layout shifts
  • progressive enhancement: if js loads slowly (or not at all), the doc page does not display any diagrams

Important thing to consider: it would be great if the SVG output did use CSS variables so that we can do theming and support dark/light mode:

  • if colors are hardcoded into the SVG it means we would have to build one svg per theme server-side.
  • if colors are CSS vars we can just use different CSS variables based on a html[data-theme='dark'] CSS selector

@aloisklink
Copy link
Member

We just finished the integration of Mermaid in Docusauurs (facebook/docusaurus#7490)

Exciting stuff! I'm looking forwards to the next Docusaurus release then! 🚀

I do have a server-side rendering (SSR) plugin for remark called remark-mermaid-dataurl that works for docusaurus, but the way it works is by using puppeteer to render the SVGs, so it's very slow and heavy (puppeteer downloads a copy of the chromium browser on the server and it's a 450+ MiB download!)

Important thing to consider: it would be great if the SVG output did use CSS variables so that we can do theming and support dark/light mode:

It might be easier to hard-code them for a few reasons:

Docusaurus would always be able to override the Mermaid default CSS with their own custom CSS using the themeCSS option.

Although, adding server-side rendering support to Mermaid will probably take a while, so maybe some of these considerations will change over time.

@ggrossetie
Copy link
Contributor

Would you be open to integrate a mechanism to convert text to SVG paths (such as https://maker.js.org/ or https://github.com/vercel/satori)?

It would solve font cut-off issues when a diagram is generated server-side because the font is very likely not going to be rendered identically on the client side.

@DanInProgress
Copy link
Contributor

DanInProgress commented Nov 21, 2022

This is something I've been interested in for a while, I currently use mermaid with gatsby for my blog. (including maintaining a fork of a plugin to prerender diagrams with puppeteer. I may start experimenting with this over the holiday break--puppeteer is easily the most fragile part of my static site build.

I saw others successfully leverage jsdom to render d3 charts, so I think I'll probably start there

@aloisklink do you know of any obvious stumbling blocks for that approach?
edit: just saw your comment on #3491 (link) , so yeah, may have some problems...

@Mogztter that's an interesting idea, did you have any thoughts on how to address the SEO and accessibility concerns of such an approach? Charts tend to be much more vital to understanding the content of a page than other types of figures, and removing all text might cause problems with this.

@ggrossetie
Copy link
Contributor

that's an interesting idea, did you have any thoughts on how to address the SEO and accessibility concerns of such an approach? Charts tend to be much more vital to understanding the content of a page than other types of figures, and removing all text might cause problems with this.

I think we should use the title element with an aria-labelledby: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title

The <title> element provides an accessible, short-text description of any SVG container element or graphics element.

If an element can be described by visible text, it is recommended to reference that text with an aria-labelledby attribute rather than using the <title> element.

@DanInProgress
Copy link
Contributor

DanInProgress commented Nov 21, 2022

Also just to make sure I understand the issue you're describing:

when a diagram is generated server-side because the font is very likely not going to be rendered identically on the client side.

  1. A diagram is rendered as an svg on one computer, (say a server)
  2. The server decides on the spacing/size of elements using the calculated width of the text contained
  3. The svg is saved and transferred to a second computer where mermaid is not present
  4. The svg is opened and without mermaid/d3 to recalculate, the elements overlap or text is cut off

does that sum it up correctly?

@ggrossetie
Copy link
Contributor

That's correct!

The server decides on the spacing/size of elements using the calculated width of the text contained

Actually, Mermaid calculates the spacing/size of boxes based on the rendered size of the text (contained in it) using a given font and rendering engine (browser).

When you open the generated SVG generated on another machine/browser the font will be rendered differently, small differences on the letter spacing but in then end the text can exceed the box width.

@aloisklink
Copy link
Member

Would you be open to integrate a mechanism to convert text to SVG paths (such as https://maker.js.org/ or https://github.com/vercel/satori)?

It would solve font cut-off issues when a diagram is generated server-side because the font is very likely not going to be rendered identically on the client side.

It would be better to bundle the fonts somehow (e.g. if using Satori, there's the font embedding option, see https://github.com/vercel/satori#font-embedding).

The only issue is then we'd be limited to fonts with an open-source license (but that's probably a good thing! The default Mermaid font of Tahoma is owned by Microsoft, so can be hard to install on Linux. Maybe https://fonts.google.com/specimen/Atkinson+Hyperlegible is a good option).

I think we should use the <title> element with an aria-labelledby: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title

We already have a <title> element, and @weedySeaDragon has just opened a PR to add a aria-labelledby to point to it #3808! More importantly, that PR also improves the docs, so hopefully people use the accessibility features more.

Actually, Mermaid calculates the spacing/size of boxes based on the rendered size of the text (contained in it) using a given font and rendering engine (browser).

As far as I'm aware, that's correct. And since jsdom doesn't support calculating layouts, it can't fully help us. svgdom might help though, but who knows 🤷

PRs are always welcome if anybody wants to try it out, though!

When you open the generated SVG generated on another machine/browser the font will be rendered differently, small differences on the letter spacing but in then end the text can exceed the box width.

If both the server and client have the exact same font and same CSS settings, it might be that the generated SVGs are similar enough that letter spacing won't change. From my limited experience with mermaid-cli and remark-mermaid-dataurl (which use puppeteer), pre-generated SVGs look mostly identical to client-generated SVGs, as long as both computers had the same fonts installed. But I am mainly testing Ubuntu x86_64 machines using Chromium, so there's not a lot of variety there.

@taylorh140
Copy link

I was thinking of different ways to do this, and I wanted to talk about solution combos.

One mentioned here was:

  • Nodejs + svgdom + mermaid. (as of yet untested?)
    If this does end up working then it's possible to take this stack elsewhere.
  • (source language) + embedded Javascript engine + svgdom + mermaid.

I would like to compile mermaid to a wasm so it can be portable and more secure/sandboxed, and used more easily in different places.

One of the key items that Mermaid is missing is the layout engine, but the expense of the browser is quite a lot.
I saw there are browser engines, and I was contemplating something like this:

rust + servo + mermaid -> wasm
(servo is a rust browser engine.)
might be a possible solution.

This looks like a problem that would really be nice to have a lighter solution for, but also pretty involved. So it would be nice to fined a path that looks like it will have a good performance/size result in the end and be flexible for embedding.

If anyone has more or better ideas I would like to hear them.

@ghost
Copy link

ghost commented Aug 30, 2023

@taylorh140, my recommendation would be to solve the deeper problem. The reason why MermaidJS cannot be easily run server-side (outside of a browser environment) is because it doesn't generate SVG-compliant output. While leveraging browser features (i.e., HTML/CSS) may make development faster/simpler to generate complex SVG diagrams, it comes with the disadvantage that nearly every third-party SVG library available cannot handle embedded HTML. (After all, why should an SVG library be burdened with also implementing a non-trivial chunk of the HTML/CSS specification?)

@jgreywolf is looking into this problem. See issue #2485 for details.

@aloisklink
Copy link
Member

The reason why MermaidJS cannot be easily run server-side (outside of a browser environment) is because it doesn't generate SVG-compliant output.

FYI, I think these are two separate issues. Converting MermaidJS to create SVG 1.1 compliant SVGs would be easier, we just need to avoid using HTML in diagrams (I believe many diagrams already have a htmlLabel: false option to avoid using embedded HTML) and avoid using new CSS/SVG features that aren't supported in SVG 1.1.

I believe svgdom only supports SVG 1.1, so fixing #2485 might be a necessary step for getting server-side rendering working!


rust + servo + mermaid -> wasm (servo is a rust browser engine.) might be a possible solution.

This might be a struggle, see servo/servo#28070

yoga is a C++ layout engine that has a WASM version on npm, see https://www.npmjs.com/package/yoga-layout, but unfortunately it doesn't yet support text layout. (although we might be able to hack it it together, with a fixed font like https://github.com/DioxusLabs/taffy/blob/daf416438b5379bf4d8b3dd79ab20c8bc78754aa/tests/generated/mod.rs#L8-L62, since Yoga does support adding your own text measure() function: facebook/yoga#54).

@ghost
Copy link

ghost commented Aug 31, 2023

From @aloisklink 's comments:

I believe many diagrams already have a htmlLabel: false option ...

Some diagrams have a htmlLabels: false option that you can use to remove <foreignObject> and use only SVG tags

My understanding is that flowChart graph types support that option, and there's work to move the option up:

See also:

When #1431 is finished, maybe it'll work?

I don't know how many graphs support that option.

@ghost
Copy link

ghost commented Oct 5, 2023

FYI, in two days I will be locked out of my account once GitHub begins enforcing MFA. Sorry I won't be able to help any further.

@tex0l
Copy link

tex0l commented Oct 7, 2023

By the way, I decided to use Pintora instead which has fewer features, but which renders entirely on the server side.

It works fine in an Astro component:

---
import {createHash} from 'node:crypto'
import {PintoraConfig} from '@pintora/standalone';
import {render} from '@pintora/cli'

interface Cache {
    [key: string]: string;
}
export interface Props {
    config?: PintoraConfig;
    code: string;
}

const cache: Cache = {}

const generateHash = (input: string) => {
    const hash = createHash('sha256')
    hash.update(input)
    return hash.digest().toString('hex')
}


const {config, code} = Astro.props

const uniqueKey = generateHash(JSON.stringify(Astro.props));

if (cache[uniqueKey] === undefined) {
    /* Render the mermaid diagram */
    try {
        const result = await render({
            code: code,
            pintoraConfig: config,
            mimeType: 'image/svg+xml'
        });
        if (typeof result === 'string' && result !== '') {
            cache[uniqueKey] = result;
        } else if (result instanceof Buffer) throw new Error('Unexpected buffer')
    } catch (error) {
        console.error(error)
    }
}
---

{
    cache[uniqueKey] && (
    <div class="pintora">
        <Fragment set:html={cache[uniqueKey]}/>
    </div>
        )
    }

<style>
    .pintora {
        /* Necessary to avoid pixelated font rendering on macOS/Chrome, why? */
        /* Also, why this looks awful on Safari with every settings tested? */
        -webkit-font-smoothing: auto;
        -moz-osx-font-smoothing: auto;
        /* Necessary for Safari, which doesn't make the SVG full width, why? */
        display: flex;
        justify-content: center;
        width: 100%;
    }
</style>

@ghost
Copy link

ghost commented Dec 12, 2023

This really needs fixed

@titanism
Copy link

By the way, I decided to use Pintora instead which has fewer features, but which renders entirely on the server side.

It works fine in an Astro component:

---
import {createHash} from 'node:crypto'
import {PintoraConfig} from '@pintora/standalone';
import {render} from '@pintora/cli'

interface Cache {
    [key: string]: string;
}
export interface Props {
    config?: PintoraConfig;
    code: string;
}

const cache: Cache = {}

const generateHash = (input: string) => {
    const hash = createHash('sha256')
    hash.update(input)
    return hash.digest().toString('hex')
}


const {config, code} = Astro.props

const uniqueKey = generateHash(JSON.stringify(Astro.props));

if (cache[uniqueKey] === undefined) {
    /* Render the mermaid diagram */
    try {
        const result = await render({
            code: code,
            pintoraConfig: config,
            mimeType: 'image/svg+xml'
        });
        if (typeof result === 'string' && result !== '') {
            cache[uniqueKey] = result;
        } else if (result instanceof Buffer) throw new Error('Unexpected buffer')
    } catch (error) {
        console.error(error)
    }
}
---

{
    cache[uniqueKey] && (
    <div class="pintora">
        <Fragment set:html={cache[uniqueKey]}/>
    </div>
        )
    }

<style>
    .pintora {
        /* Necessary to avoid pixelated font rendering on macOS/Chrome, why? */
        /* Also, why this looks awful on Safari with every settings tested? */
        -webkit-font-smoothing: auto;
        -moz-osx-font-smoothing: auto;
        /* Necessary for Safari, which doesn't make the SVG full width, why? */
        display: flex;
        justify-content: center;
        width: 100%;
    }
</style>

Thank you for sharing Pintora; we're going to try it out! 🎉 We use Mermaid right now at Forward Email (@forwardemail) and we have had an absolute headache with it due to this issue. Definitely looking to switch.

@tex0l
Copy link

tex0l commented Dec 13, 2023

Glad I could help!

If you use Astro, I published a component: https://github.com/tex0l/astro-pintora

@titanism
Copy link

titanism commented Jan 5, 2024

👋 Hi there folks!

💬 What's this comment about?

Here at Forward Email (@forwardemail) - we tried out Mermaid (v6-latest), then we tried Pintora Standalone, then we tried Pintora CLI, then we tried Mermaid again, and then finally Mermaid CLI.

Unfortunately both Mermaid and Pintora have Content-Security-Policy ("CSP") issues if used client-side - and they also have issues with server-side rendering (or rendering in general).

Ultimately we ended up using Mermaid CLI to render the assets with retina and dark/light mode theme support. Albeit it's not pretty, it has built-in caching and works remarkably well. We wanted to share it here with the community 🎉...

🔴 Live Demo: https://forwardemail.net/blog/docs/best-quantum-safe-encrypted-email-service#how-does-it-work

🚧 How does it work?

  1. We write Markdown as usual with Mermaid @ https://github.com/forwardemail/forwardemail.net/blob/a481cf81bb3195a65683740153e5f4ab08316999/app/views/docs/best-quantum-safe-encrypted-email-service/index.md?plain=1#L81-L89
  2. Our pug has a built-in filter for Markdown to parse and rewrite blocks using markdown-it @ https://github.com/forwardemail/forwardemail.net/blob/a481cf81bb3195a65683740153e5f4ab08316999/helpers/markdown.js#L29-L64
  3. The Mermaid code is converted to an img tag inside a picture (for dark/light mode support), and all of this is inside a a tag that uses a lightbox gallery effect so when the user clicks the chart it expands to a lightbox @ https://github.com/forwardemail/forwardemail.net/blob/a481cf81bb3195a65683740153e5f4ab08316999/helpers/markdown.js#L45-L55
  4. The img tag source refers to a route on our side that is server-side rendered @ https://github.com/forwardemail/forwardemail.net/blob/a481cf81bb3195a65683740153e5f4ab08316999/routes/web/index.js#L59-L130
  5. We have built-in caching with global (process) and using Redis (via ioredis) @ https://github.com/forwardemail/forwardemail.net/blob/a481cf81bb3195a65683740153e5f4ab08316999/routes/web/index.js#L69-L93

❓ Why bother?

We were aiming to pass 100% on all tests (and we did; as far as we know we're the only email service to do this):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion Discussion which may lead to separate issues or PRs Status: Triage Needs to be verified, categorized, etc Type: Other Not an enhancement or a bug Type: Question
Projects
None yet
Development

No branches or pull requests