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

Add support for lazyLoadedDiagrams (mermaid v9.2.0) #420

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/compile-mermaid.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
DOCKERFILE: DockerfileBuild
IMAGENAME: mermaid-cli
DOCKER_IO_REPOSITORY: minlag/mermaid-cli
INPUT_DATA: test-positive
INPUT_DATA: test/__fixtures__/test-positive
steps:
- uses: actions/checkout@v3
with:
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
DOCKERFILE: Dockerfile
IMAGENAME: mermaid-cli
DOCKER_IO_REPOSITORY: minlag/mermaid-cli
INPUT_DATA: test/__fixtures__/test-positive

steps:
- uses: actions/checkout@v3
Expand Down Expand Up @@ -95,8 +96,8 @@ jobs:

- name: Post deployment tests
run: |
for i in $(ls test-positive/*.mmd); do docker run -v $(pwd):/data ${{env.DOCKER_IO_REPOSITORY}}:${{env.RELEASE_VERSION}} -i /data/$i; done
for i in $(ls test-positive/*.mmd); do cat $i | docker run -i -v $(pwd):/data ${{env.DOCKER_IO_REPOSITORY}}:${{env.RELEASE_VERSION}} -o /data/$i-stdin.svg; done
for i in $(ls $INPUT_DATA/*.mmd); do docker run -v $(pwd):/data ${{env.DOCKER_IO_REPOSITORY}}:${{env.RELEASE_VERSION}} -i /data/$i; done
for i in $(ls $INPUT_DATA/*.mmd); do cat $i | docker run -i -v $(pwd):/data ${{env.DOCKER_IO_REPOSITORY}}:${{env.RELEASE_VERSION}} -o /data/$i-stdin.svg; done

- name: Commit new version to the repository
run: |
Expand All @@ -109,4 +110,4 @@ jobs:
- uses: actions/upload-artifact@v3.1.1
with:
name: output
path: ./test-positive
path: ${{ env.INPUT_DATA }}
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ fontawesome/
.DS_Store
.npmrc
yarn-error.log
test-positive/*.out.md
test/__fixtures/*/*.out.md
test-output/

24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,38 @@ mmdc -i input.mmd -o output.svg
mmdc -i input.mmd -o output.png -t dark -b transparent
```

### Using a custom mermaidConfig file

A custom mermaid config JSON file can be loaded in using the `-c`/`--configFile` option:

```sh
mmdc -i test/__fixtures__/test-mindmap/mindmap.mmd --configFile test/__fixtures__/test-mindmap/config.json -o mindmap.png
```

This can be used to enable custom diagrams.

For example, for mermaid-mindmap support, you can pass in the following:

```json
{
"lazyLoadedDiagrams": [
"https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-mindmap@9.2.0/dist/mermaid-mindmap-detector.esm.mjs"
]
}
```

### Animating an SVG file with custom CSS

The `--cssFile` option can be used to inline some custom CSS.

Please see [./test-positive/flowchart1.css](test-positive/flowchart1.css) for an example of a CSS file that has animations.
Please see [./test/__fixtures__/test-positive/flowchart1.css](test/__fixtures__/test-positive/flowchart1.css) for an example of a CSS file that has animations.

**Warning**: If you want to override `mermaid`'s [`themeCSS`](https://mermaid-js.github.io/mermaid/#/Setup?id=theme), we recommend instead adding `{"themeCSS": "..."})` to your mermaid `--configFile`. You may also need to use [`!important`](https://developer.mozilla.org/en-US/docs/Web/CSS/important) to override mermiad's `themeCSS`.

**Warning**: Inline CSS files may be blocked by your browser, depending on the [HTTP Content-Security-Policy header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) of the website that hosts your SVG.

```sh
mmdc --input test-positive/flowchart1.mmd --cssFile test-positive/flowchart1.css -o docs/animated-flowchart.svg
mmdc --input test/__fixtures__/test-positive/flowchart1.mmd --cssFile test/__fixtures__/test-positive/flowchart1.css -o docs/animated-flowchart.svg
```

<details>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
"devDependencies": {
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
"mermaid": "^9.1.2",
"mermaid": "^9.2.0",
"jest": "^29.0.1",
"standard": "^17.0.0",
"yarn-upgrade-all": "^0.7.0"
Expand Down
10 changes: 6 additions & 4 deletions run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,18 @@ done
cat $INPUT_DATA/flowchart1.mmd | docker run --rm -i -v $(pwd):/data $IMAGETAG -o /data/$INPUT_DATA/flowchart1-stdin.png -w 800

# Test if mmdc crashes on Markdown files containing no mermaid charts
OUTPUT=$(docker run --rm -v $(pwd):/data $IMAGETAG -i /data/test-positive/no-charts.md)
OUTPUT=$(docker run --rm -v $(pwd):/data $IMAGETAG -i /data/$INPUT_DATA/no-charts.md)
EXPECTED_OUTPUT="No mermaid charts found in Markdown input"
[ "$OUTPUT" = "$EXPECTED_OUTPUT" ] || echo "Expected output to be '$EXPECTED_OUTPUT', got '$OUTPUT'"

# Test if mmdc does not replace <br> with <br/>
outputFileName="graph-with-br.svg"
docker run --rm -v $(pwd):/data $IMAGETAG \
-i /data/test-positive/graph-with-br.mmd \
-i "/data/$INPUT_DATA/graph-with-br.mmd" \
--width 800 \
--configFile "/data/$config_noUseMaxWidth"
if grep -q "<br>" "./test-positive/graph-with-br.mmd.svg"; then
--configFile "/data/$config_noUseMaxWidth" \
-o "$outputFileName"
if grep -q "<br>" "$outputFileName"; then
echo "<br> has not been replaced with <br/>";
exit 1;
fi
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,12 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
await page.$eval('body', (body, backgroundColor) => {
body.style.background = backgroundColor
}, backgroundColor)
const metadata = await page.$eval('#container', (container, definition, mermaidConfig, myCSS, backgroundColor) => {
const metadata = await page.$eval('#container', async (container, definition, mermaidConfig, myCSS, backgroundColor) => {
container.textContent = definition
window.mermaid.initialize(mermaidConfig)
// should throw an error if mmd diagram is invalid
try {
window.mermaid.initThrowsErrors(undefined, container)
await window.mermaid.initThrowsErrorsAsync(undefined, container)
} catch (error) {
if (error instanceof Error) {
// mermaid-js doesn't currently throws JS Errors, but let's leave this
Expand Down
37 changes: 37 additions & 0 deletions test/__fixtures__/test-mindmap/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"lazyLoadedDiagrams": [
"https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-mindmap@9.2.0/dist/mermaid-mindmap-detector.esm.mjs"
],
"//comment": "Our CI/Percy tests use convert-svg-to-png, which needs explicit width, not useMaxWidth",
"//comment2": "Not recommended normally, since browsers may render SVGs slightly differently",
"flowchart": {
"useMaxWidth": false
},
"sequence": {
"useMaxWidth": false
},
"gantt": {
"useMaxWidth": false
},
"journey": {
"useMaxWidth": false
},
"class": {
"useMaxWidth": false
},
"state": {
"useMaxWidth": false
},
"er": {
"useMaxWidth": false
},
"pie": {
"useMaxWidth": false
},
"requirement": {
"useMaxWidth": false
},
"c4": {
"useMaxWidth": false
}
}
14 changes: 14 additions & 0 deletions test/__fixtures__/test-mindmap/mindmap.mmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mindmap
root
child1((Circle))
grandchild 1
grandchild 2
child2(Round rectangle)
grandchild 3
grandchild 4
child3[Square]
grandchild 5
::icon(mdi mdi-fire)
gc6((grand<br/>child 6))
::icon(mdi mdi-fire)
gc7((grand<br/>grand<br/>child 8))
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
75 changes: 46 additions & 29 deletions src-test/test.js → test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { expect, beforeAll, afterAll, describe, test } from '@jest/globals'
import { run, renderMermaid, parseMMD } from '../src/index.js'
import puppeteer from 'puppeteer'

const workflows = ['test-positive', 'test-negative']
const workflows = ['test/__fixtures__/test-positive', 'test/__fixtures__/test-negative', 'test/__fixtures__/test-mindmap']
const out = 'test-output'

/**
Expand Down Expand Up @@ -148,9 +148,9 @@ describe('mermaid-cli', () => {

test('should error on mmdc failure', async () => {
// should work with default puppeteerConfigFile
await compileDiagram('test-positive', 'sequence.mmd', 'svg')
await compileDiagram('test/__fixtures__/test-positive', 'sequence.mmd', 'svg')
await expect(
compileDiagram('test-positive', 'sequence.mmd', 'svg', { puppeteerConfigFile: '../test-negative/puppeteerTimeoutConfig.json' })
compileDiagram('test/__fixtures__/test-positive', 'sequence.mmd', 'svg', { puppeteerConfigFile: '../test-negative/puppeteerTimeoutConfig.json' })
).rejects.toThrow('TimeoutError: Timed out after 1 ms')
}, timeout)

Expand All @@ -160,25 +160,25 @@ describe('mermaid-cli', () => {

test('should error on mermaid syntax error', async () => {
await expect(
compileDiagram('test-negative', 'invalid.expect-error.mmd', 'svg')
compileDiagram('test/__fixtures__/test-negative', 'invalid.expect-error.mmd', 'svg')
).rejects.toThrow('Parse error on line 2')
}, timeout)

test('should have 3 trailing spaces after ``` in test-positive/mermaid.md for case 9.', async () => {
// test if test case 9. for the next test is in required state
const data = await fs.readFile('test-positive/mermaid.md', { encoding: 'utf8' })
const data = await fs.readFile('test/__fixtures__/test-positive/mermaid.md', { encoding: 'utf8' })
const regex = /9\.\s+Should still find mermaid code even with trailing spaces after the(.+)do not delete the trailing spaces after the/sg
const matches = data.match(regex)
await expect(matches.length).toBeGreaterThan(0)
await expect(matches[0].includes('``` ')).toBeTruthy()
}, timeout)

test('should write multiple SVGs for default .md input by default', async () => {
const expectedOutputFiles = [1, 2, 3, 8, 9].map((i) => join('test-positive', `mermaid.md-${i}.svg`))
const expectedOutputFiles = [1, 2, 3, 8, 9].map((i) => join('test/__fixtures__/test-positive', `mermaid.md-${i}.svg`))
// delete any files from previous test (fs.rm added in Node v14.14.0)
await Promise.all(expectedOutputFiles.map((file) => fs.rm(file, { force: true })))

await promisify(execFile)('node', ['src/cli.js', '-i', 'test-positive/mermaid.md'])
await promisify(execFile)('node', ['src/cli.js', '-i', 'test/__fixtures__/test-positive/mermaid.md'])

// files should exist, and they should be SVGs
await Promise.all(expectedOutputFiles.map(async (file) => {
Expand All @@ -187,72 +187,72 @@ describe('mermaid-cli', () => {
}, timeout)

test('the .png extension should be added to .md files', async () => {
const expectedOutputFiles = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => join('test-positive', `mermaid.md-${i}.png`))
const expectedOutputFiles = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => join('test/__fixtures__/test-positive', `mermaid.md-${i}.png`))
await Promise.all(expectedOutputFiles.map((file) => fs.rm(file, { force: true })))
await promisify(execFile)('node', ['src/cli.js', '-e', 'png', '-i', 'test-positive/mermaid.md'])
await promisify(execFile)('node', ['src/cli.js', '-e', 'png', '-i', 'test/__fixtures__/test-positive/mermaid.md'])

await Promise.all(expectedOutputFiles.map(async (file) => {
expectBytesAreFormat(await fs.readFile(file), 'png')
}))
}, timeout)

test('the .svg extension should be added to .md files', async () => {
const expectedOutputFiles = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => join('test-positive', `mermaid.md-${i}.svg`))
const expectedOutputFiles = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => join('test/__fixtures__/test-positive', `mermaid.md-${i}.svg`))
await Promise.all(expectedOutputFiles.map((file) => fs.rm(file, { force: true })))
await promisify(execFile)('node', ['src/cli.js', '-e', 'svg', '-i', 'test-positive/mermaid.md'])
await promisify(execFile)('node', ['src/cli.js', '-e', 'svg', '-i', 'test/__fixtures__/test-positive/mermaid.md'])

await Promise.all(expectedOutputFiles.map(async (file) => {
expectBytesAreFormat(await fs.readFile(file), 'svg')
}))
}, timeout)

test('the .pdf extension should be added to .md files', async () => {
const expectedOutputFiles = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => join('test-positive', `mermaid.md-${i}.pdf`))
const expectedOutputFiles = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => join('test/__fixtures__/test-positive', `mermaid.md-${i}.pdf`))
await Promise.all(expectedOutputFiles.map((file) => fs.rm(file, { force: true })))
await promisify(execFile)('node', ['src/cli.js', '-e', 'pdf', '-i', 'test-positive/mermaid.md'])
await promisify(execFile)('node', ['src/cli.js', '-e', 'pdf', '-i', 'test/__fixtures__/test-positive/mermaid.md'])

await Promise.all(expectedOutputFiles.map(async (file) => {
expectBytesAreFormat(await fs.readFile(file), 'pdf')
}))
}, timeout)

test('the extension .pdf should be added for .mmd file', async () => {
const expectedOutputFile = 'test-positive/flowchart1.mmd.pdf'
const expectedOutputFile = 'test/__fixtures__/test-positive/flowchart1.mmd.pdf'
await fs.rm(expectedOutputFile, { force: true })
await promisify(execFile)('node', ['src/cli.js', '-e', 'pdf', '-i', 'test-positive/flowchart1.mmd'])
await promisify(execFile)('node', ['src/cli.js', '-e', 'pdf', '-i', 'test/__fixtures__/test-positive/flowchart1.mmd'])

expectBytesAreFormat(await fs.readFile(expectedOutputFile), 'pdf')
}, timeout)

test('the extension .svg should be added for .mmd file', async () => {
const expectedOutputFile = 'test-positive/flowchart1.mmd.svg'
const expectedOutputFile = 'test/__fixtures__/test-positive/flowchart1.mmd.svg'
await fs.rm(expectedOutputFile, { force: true })
await promisify(execFile)('node', ['src/cli.js', '-e', 'svg', '-i', 'test-positive/flowchart1.mmd'])
await promisify(execFile)('node', ['src/cli.js', '-e', 'svg', '-i', 'test/__fixtures__/test-positive/flowchart1.mmd'])

expectBytesAreFormat(await fs.readFile(expectedOutputFile), 'svg')
}, timeout)

test('the extension .png should be added for .mmd file', async () => {
const expectedOutputFile = 'test-positive/flowchart1.mmd.png'
const expectedOutputFile = 'test/__fixtures__/test-positive/flowchart1.mmd.png'
await fs.rm(expectedOutputFile, { force: true })
await promisify(execFile)('node', ['src/cli.js', '-e', 'png', '-i', 'test-positive/flowchart1.mmd'])
await promisify(execFile)('node', ['src/cli.js', '-e', 'png', '-i', 'test/__fixtures__/test-positive/flowchart1.mmd'])

expectBytesAreFormat(await fs.readFile(expectedOutputFile), 'png')
}, timeout)

test.concurrent.each(['svg', 'png', 'pdf'])('should set red background to %s', async (format) => {
await promisify(execFile)('node', [
'src/cli.js', '-i', 'test-positive/flowchart1.mmd', '-o', `test-output/flowchart1-red-background.${format}`,
'src/cli.js', '-i', 'test/__fixtures__/test-positive/flowchart1.mmd', '-o', `test-output/flowchart1-red-background.${format}`,
'--backgroundColor', 'red'
])
}, timeout)

test.concurrent.each(['svg', 'png', 'pdf'])('should add css to %s', async (format) => {
await promisify(execFile)('node', [
'src/cli.js', '-i', 'test-positive/flowchart1.mmd', '-o', `test-output/flowchart1-with-css.${format}`,
'src/cli.js', '-i', 'test/__fixtures__/test-positive/flowchart1.mmd', '-o', `test-output/flowchart1-with-css.${format}`,
// we want to add an SVG file to git, so make sure it's always the same
'--configFile', 'test-positive/config-deterministic.json',
'--cssFile', 'test-positive/flowchart1.css'
'--configFile', 'test/__fixtures__/test-positive/config-deterministic.json',
'--cssFile', 'test/__fixtures__/test-positive/flowchart1.css'
])

if (format === 'svg') {
Expand All @@ -276,7 +276,7 @@ describe("NodeJS API (import ... from '@mermaid-js/mermaid-cli')", () => {
)

await run(
'test-positive/mermaid.md', expectedOutputMd, { quiet: true, outputFormat: 'svg' }
'test/__fixtures__/test-positive/mermaid.md', expectedOutputMd, { quiet: true, outputFormat: 'svg' }
)

const markdownFile = await fs.readFile(expectedOutputMd, { encoding: 'utf8' })
Expand All @@ -303,7 +303,7 @@ describe("NodeJS API (import ... from '@mermaid-js/mermaid-cli')", () => {
)

await run(
'test-positive/mermaid.md', expectedOutputMd, { quiet: true, outputFormat: 'png' }
'test/__fixtures__/test-positive/mermaid.md', expectedOutputMd, { quiet: true, outputFormat: 'png' }
)

const markdownFile = await fs.readFile(expectedOutputMd, { encoding: 'utf8' })
Expand All @@ -330,7 +330,7 @@ describe("NodeJS API (import ... from '@mermaid-js/mermaid-cli')", () => {
const expectedOutput = `test-output/flowchart1-run-output-test.${format}`
await fs.rm(expectedOutput, { force: true })
await run(
'test-positive/flowchart1.mmd',
'test/__fixtures__/test-positive/flowchart1.mmd',
expectedOutput,
{ quiet: true, outputFormat: format }
)
Expand All @@ -351,7 +351,8 @@ describe("NodeJS API (import ... from '@mermaid-js/mermaid-cli')", () => {
const shouldError = /expect-error/.test(file)
test.concurrent.each(formats)(`${shouldError ? 'should fail' : 'should compile'} ${file} to format %s`, async (format) => {
const result = file.replace(/\.(?:mmd|md)$/, `-run.${format}`)
const promise = run(join(workflow, file), join(out, result), { quiet: true })
const mermaidConfig = JSON.parse(await fs.readFile(join(workflow, 'config.json'), { encoding: 'utf-8' }))
const promise = run(join(workflow, file), join(out, result), { quiet: true, parseMMDOptions: { mermaidConfig } })
if (shouldError) {
await expect(promise).rejects.toThrow()
} else {
Expand All @@ -374,7 +375,7 @@ describe("NodeJS API (import ... from '@mermaid-js/mermaid-cli')", () => {
const invalidMMDInput = 'this is not a valid mermaid file'
expect(
parseMMD(browser, invalidMMDInput, 'svg')
).rejects.toThrow('Parse error')
).rejects.toThrow('Error: No diagram type detected')
})

describe.each(workflows)('testing workflow %s', (workflow) => {
Expand All @@ -388,7 +389,8 @@ describe("NodeJS API (import ... from '@mermaid-js/mermaid-cli')", () => {
const shouldError = /expect-error/.test(file)
test.concurrent.each(formats)(`${shouldError ? 'should fail' : 'should compile'} ${file} to format %s`, async (format) => {
const mmd = await fs.readFile(join(workflow, file), { encoding: 'utf8' })
const promise = parseMMD(browser, mmd, format)
const mermaidConfig = JSON.parse(await fs.readFile(join(workflow, 'config.json'), { encoding: 'utf-8' }))
const promise = parseMMD(browser, mmd, format, { mermaidConfig })
if (shouldError) {
await expect(promise).rejects.toThrow()
} else {
Expand All @@ -407,5 +409,20 @@ describe("NodeJS API (import ... from '@mermaid-js/mermaid-cli')", () => {
expect(result).toMatchObject({ title: 'Hi', desc: 'World' })
expectBytesAreFormat(result.data, 'svg')
})

test('should support lazyLoadedDiagrams (mermaid-mindmap) (requires internet)', async () => {
const mindmapMMD = await fs.readFile('test/__fixtures__/test-mindmap/mindmap.mmd', { encoding: 'utf8' })
// TODO: remove this test if we ever support mermaid-mindmap by default
await expect(renderMermaid(browser, mindmapMMD, 'svg')).rejects.toThrow('No diagram type detected for text: mindmap')

const mermaidConfig = {
lazyLoadedDiagrams: [
'https://cdn.jsdelivr.net/npm/@mermaid-js/mermaid-mindmap@9.2.0/dist/mermaid-mindmap-detector.esm.mjs'
]
}
const { data } = await renderMermaid(browser, mindmapMMD, 'svg', { mermaidConfig })
expect(data).toBeInstanceOf(Buffer)
expectBytesAreFormat(data, 'svg')
}, timeout)
})
})
Loading