Skip to content

Commit

Permalink
Merge pull request #115 from raghur/generate-inline-svg
Browse files Browse the repository at this point in the history
Generate inline svg
  • Loading branch information
raghur authored Dec 25, 2023
2 parents 4d63fe5 + a8f9d3b commit 912d283
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 46 deletions.
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// #! /usr/bin/env node
const pandoc = require('pandoc-filter')
const process = require('process')
const utils = require('./lib')
const utils = require('./lib/filter')
const tmp = require('tmp')
const fs = require('fs')

Expand Down
27 changes: 16 additions & 11 deletions lib/index.js → lib/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ const pandoc = require('pandoc-filter')
const exec = require('child_process').execSync
const sanfile = require('sanitize-filename')
const prefix = 'diagram'
const cmd = externalTool('mmdc', process.env)
const imgur = externalTool('imgur', process.env)
let counter = 0
const folder = process.cwd()
const tmp = require('tmp')
Expand Down Expand Up @@ -109,10 +107,16 @@ function mermaid (type, value, _format, _meta) {
exec(fullCmd)
// console.log(oldPath, newPath);
let data
const imageClasses = options.imageClass ? [options.imageClass] : []
if (options.loc === 'inline') {
if (options.format === 'svg') {
data = fs.readFileSync(savePath, 'utf8')
newPath = 'data:image/svg+xml;base64,' + Buffer.from(data).toString('base64')
const svg = fs.readFileSync(savePath, 'utf8')
// does not use default theme - picks the forest theme in the test.md
return pandoc.RawBlock('html', svg)
// return pandoc.Div(
// [id, imageClasses, []],
// [pandoc.RawBlock('html', svg)]
// )
} else if (options.format === 'pdf') {
newPath = savePath
} else {
Expand All @@ -128,13 +132,7 @@ function mermaid (type, value, _format, _meta) {
mv(savePath, newPath)
}

let fig = ''

if (options.caption !== '') {
fig = 'fig:'
}

const imageClasses = options.imageClass ? [options.imageClass] : []
const fig = id.startsWith('fig:') ? 'fig:' : ''

return pandoc.Para([
pandoc.Image(
Expand All @@ -148,12 +146,19 @@ function mv (from, to) {
const readStream = fs.createReadStream(from)
const writeStream = fs.createWriteStream(to)

const parent = path.dirname(to)
if (!fs.existsSync(parent)) {
fs.mkdirSync(parent, { recursive: true })
}

readStream.on('close', () => {
fs.unlinkSync(from)
})
readStream.pipe(writeStream)
}

const cmd = externalTool('mmdc', process.env)
const imgur = externalTool('imgur', process.env)
exports.externalTool = externalTool
exports.mermaid = mermaid
exports.getOptions = getOptions
133 changes: 133 additions & 0 deletions lib/mermaid.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
const pandoc = require('pandoc-filter')
/* global test jest describe expect beforeEach */
describe('mermaid', () => {
let fs, exec, filter
beforeEach(() => {
jest.mock('fs')
jest.mock('process', () => ({
env: {}
}))
jest.mock('tmp', () => ({
fileSync: jest.fn().mockReturnValue({
name: 'tmpfile'
})
}))
jest.mock('child_process', () => ({
execSync: jest.fn()
}))
fs = require('fs')
exec = require('child_process').execSync
fs.existsSync = jest.fn().mockReturnValue(true)
filter = require('./filter')

fs.writeFileSync = jest.fn()
fs.readFileSync = jest.fn().mockReturnValue('graph TD;\nA-->B;')
jest.clearAllMocks()
})
test('returns null for non code block', () => {
const type = 'Paragraph'
const value = []
const format = ''
const meta = {}

expect(filter.mermaid(type, value, format, meta)).toBeNull()
})

test('returns null if no mermaid class', () => {
const type = 'CodeBlock'
const value = [['id', ['other']], 'graph TD;\nA-->B;']
const format = ''
const meta = {}

expect(filter.mermaid(type, value, format, meta)).toBeNull()
})

test('renders with default options', () => {
filter.mermaid('CodeBlock', [['id', ['mermaid']], 'x'])

expect(exec).toHaveBeenCalled()
const cmd = exec.mock.lastCall[0]
console.log('cmd', cmd)
expect(cmd).toContain('mmdc')
expect(cmd).toContain('-s 1')
expect(cmd).toContain('-f')
expect(cmd).toContain('-i "tmpfile"')
expect(cmd).toContain('-t default')
expect(cmd).toContain('-w 800')
expect(cmd).toContain('-b white')
expect(cmd).toContain('-o "tmpfile.png"')
})

test('returns RawBlock for svg inline', () => {
const block = filter.mermaid('CodeBlock', [['id', ['mermaid'], [['format', 'svg']]], 'x'])

expect(block).toEqual(expect.objectContaining({
t: 'RawBlock',
c: ['html', 'graph TD;\nA-->B;']
}))
expect(exec).toHaveBeenCalled()
const cmd = exec.mock.lastCall[0]
console.log('cmd', cmd)
expect(cmd).toContain('mmdc')
expect(cmd).toContain('-s 1')
expect(cmd).toContain('-f')
expect(cmd).toContain('-i "tmpfile"')
expect(cmd).toContain('-t default')
expect(cmd).toContain('-w 800')
expect(cmd).toContain('-b white')
expect(cmd).toContain('-o "tmpfile.svg"')
})

test('sets title as fig: if id starts with fig:', () => {
const para = filter.mermaid('CodeBlock',
[['fig:someref',
['mermaid']], 'x'])

expect(para).toEqual(expect.objectContaining({
t: 'Para',
c: [
{
t: 'Image',
c: [
['fig:someref', [], []],
[pandoc.Str('')],
[expect.any(String), 'fig:']
]
}
]
}))
})

test('sets alt from caption attribute', () => {
const para = filter.mermaid('CodeBlock',
[['id',
['mermaid'],
[['caption', 'a caption']]], 'x'])

expect(para).toEqual(expect.objectContaining({
t: 'Para',
c: [
{
t: 'Image',
c: [
['id', [], []],
[pandoc.Str('a caption')],
[expect.any(String), '']
]
}
]
}))

expect(exec).toHaveBeenCalled()
const cmd = exec.mock.lastCall[0]
console.log('cmd', cmd)
expect(cmd).toContain('mmdc')
expect(cmd).toContain('-s 1')
expect(cmd).toContain('-f')
expect(cmd).toContain('-i "tmpfile"')
expect(cmd).toContain('-t default')
expect(cmd).toContain('-w 800')
expect(cmd).toContain('-b white')
expect(cmd).toContain('-o "tmpfile.png"')
})
})
39 changes: 15 additions & 24 deletions index.test.js → lib/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
/* global test describe it expect fail */
const utils = require('./lib')
const utils = require('./filter')

describe('external tool lookup', () => {
function findTool (name, env) {
return utils.externalTool(name, env, () => fail(`expected to find utility ${name}`))
function findTool (name, env, onNotFound) {
return utils.externalTool(name, env, function () {
if (onNotFound) onNotFound()
else fail(`expected to find utility ${name}`)
})
}
it('should find mmdc tool', () => {
findTool('mmdc')
})
it('should throw for non existent tool', () => {
expect(() => findTool('someName', {}, () => {
throw new Error('expected to fail')
})).toThrow()
})
it('should find imgur tool', () => {
findTool('imgur')
})
Expand All @@ -16,32 +25,14 @@ describe('external tool lookup', () => {
expect(path).toEqual('"/usr/bin/ls"')
})
it('should only override where env key matches', () => {
const path = findTool('imgur', { MERMAID_FILTER_CMD_MMDC: '/usr/bin/ls' })
const path = findTool('imgur', {
MERMAID_FILTER_CMD_MMDC: '/usr/bin/ls'
})
expect(path).not.toEqual('"/usr/bin/ls"')
})
})
})

describe('mermaid', () => {
test('returns null for non code block', () => {
const type = 'Paragraph'
const value = []
const format = ''
const meta = {}

expect(utils.mermaid(type, value, format, meta)).toBeNull()
})

test('returns null if no mermaid class', () => {
const type = 'CodeBlock'
const value = [['id', ['other']], 'graph TD;\nA-->B;']
const format = ''
const meta = {}

expect(utils.mermaid(type, value, format, meta)).toBeNull()
})
})

describe('getOptions', () => {
test('sets default options', () => {
const options = utils.getOptions()
Expand Down
43 changes: 37 additions & 6 deletions test.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,61 @@
this is a markdown file
## basic png with width override
with a code block


```{.mermaid width=100 format=png loc=img}
```{.mermaid format=png loc=img}
sequenceDiagram
Note right of John: png, folder img
Alice->>John: Hello John, how are you?
John-->>Alice: Great!
```

```{.mermaid width=100 format=png loc=img}
## Nested folder

```{.mermaid format=png loc=img/child alt="should show up"}
sequenceDiagram
Note right of John: png, folder img/child
Alice->>John: Hello John, how are you?
John-->>Alice: Great!
```
## fig ref
```{.mermaid #fig:ref caption="Caption" format=png loc=img/child alt="should have id of fig:ref"}
If an id starts with `fig:`, then `title` attribute is set to `fig:`
```{.mermaid width=100 format=svg }
sequenceDiagram
Note right of John: png with id as attr
Alice->>John: Hello John, how are you?
John-->>Alice: Great!
```

With theme specified
---------------------

```{.mermaid width=100 format=svg theme=forest}
**The following two diagram themes don't work because mermaid generates the svg with the same id and the last set of styles applied wins.**

```{.mermaid format=svg }
---
config:
theme: dark
deterministicIds: true
deterministicIdSeed: first
title: first
---
sequenceDiagram
Note right of John: SVG output with dark theme
Alice->>John: Hello John, how are you?
John-->>Alice: Great!
```

```{.mermaid #item2 width=100 format=svg }
---
config:
theme: forest
deterministicIds: true
deterministicIdSeed: second
title: first
---
sequenceDiagram
Note right of John: SVG output with forest theme
Alice->>John: Hello John, how are you?
John-->>Alice: Great!
```
Expand Down
10 changes: 6 additions & 4 deletions test.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#! /bin/bash
TEST_OUT=test-output
mkdir -p $TEST_OUT
pandoc -t json test.md > $TEST_OUT/test.json
cat $TEST_OUT/test.json | node index.js > $TEST_OUT/transformed.json
pandoc -f json $TEST_OUT/transformed.json -t html > $TEST_OUT/transformed.html
xdg-open $TEST_OUT/transformed.html
pushd $TEST_OUT
pandoc -t json ../test.md > test.json
cat test.json | node ../index.js > transformed.json
pandoc -f json transformed.json -t html > transformed.html
xdg-open transformed.html
popd

0 comments on commit 912d283

Please sign in to comment.