Ensure marp can render slides.

We bundle markdown-it-kroki, which can add mermaid diagrams to marp slides using https://kroki.io.
This commit is contained in:
Jonathan Pallant (Ferrous Systems) 2023-03-22 10:28:51 +00:00
parent 04fc983107
commit a4ee38540d
No known key found for this signature in database
27 changed files with 1411 additions and 0 deletions

View file

@ -0,0 +1,11 @@
# Down the Stack Slides
To read as an mdbook:
1. `cargo install mdbook`
2. `mdbook serve`
To read as a slide-show:
1. Download [marp-cli](https://github.com/marp-team/marp-cli/releases) and unpack into your `$PATH`
2. `marp -c marp.config.js -I . -s`

View file

@ -0,0 +1,6 @@
module.exports = {
inputDir: './slides',
engine: ({ marp }) => marp.use(require('@kazumatu981/markdown-it-kroki'), {
entrypoint: "https://kroki.io",
})
}

13
down-the-stack-book/node_modules/.package-lock.json generated vendored Normal file
View file

@ -0,0 +1,13 @@
{
"name": "down-the-stack-book",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@kazumatu981/markdown-it-kroki": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kazumatu981/markdown-it-kroki/-/markdown-it-kroki-1.1.1.tgz",
"integrity": "sha512-LDYl+mV2WogLQ5r4olxovm+gphL/MNGfWZ1M1woBO/YhFnfwdn5EAUu9zF/KoVZzytJPq0RNfyeDtMkv+GJihg==",
"dev": true
}
}
}

View file

@ -0,0 +1,17 @@
# CHANGELOG
## v1.1.1
* fix readme, see [this issue](https://github.com/kazumatu981/markdown-it-kroki/issues/1)
## v1.1.0
* Obsolated Option `marpAutoScaling` and detect automatically wether it is nessesury or not.
## v1.0.1
release on npm
## v1.0.0
create new

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Kazuyoshi Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,157 @@
# markdown-it-kroki
> This library was designed to embed [Kori.io](https://kroki.io/) diagram into [Marp](https://marp.app/) Slide-deks!!
This library is a pugin for markdown-it to embed figure is created by textual syntax.
To use this package, You can embed **Software Diagram** (like uml) is written by **code** in Marp Slides-deck.
See marp sample code.
## Sample
---
marp: true
---
## plantuml
```plantuml[platuml image]
@startuml
left to right direction
actor Guest as g
package Professional {
actor Chef as c
actor "Food Critic" as fc
}
package Restaurant {
usecase "Eat Food" as UC1
usecase "Pay for Food" as UC2
usecase "Drink" as UC3
usecase "Review" as UC4
}
fc --> UC4
g --> UC1
g --> UC2
g --> UC3
@enduml
```
![plantuml-sample](img/plantuml-sample.png)
If you want to write daigram, you write Diagram Language (like [plantuml](https://plantuml.com/), [mermaid.js](https://mermaid-js.github.io/mermaid/#/)) with in fenced code block.
## How to install
You can install `npm install` command like bellow.
```bash
npm install -D @kazumatu981/markdown-it-kroki
```
## How to use
Here is the how to use `markdow-it-kroki`.
This section introduce how to create Marp slides-deck project,
and introduce how to create Marp slides-deck server.
You can find deltail info in [here](https://marp.app/),
and you can learn about marp plugin eco-system, [here](https://marpit.marp.app/usage?id=extend-marpit-by-plugins).
### **[1st step]** Create Slides-deck project
First, for create slides-deck, you have to prepair to **Marp Project** directory.
So First, Create slides-deck project, and init npm package.
```bash
mkdir myslides
cd myslides
npm init -y
```
Secondary, Build Marp Environment.
Install [@marp-team/marp-cli](https://github.com/marp-team/marp-cli).
```bash
npm install -D @marp-team/marp-cli
```
> Off-course you can install as **global package** (like `npm install -g @marp-team/marp-cli`), or **run at-once** (like `npx`).
### **[2nd step]** Download this project and install
```bash
git clone https://github.com/kazumatu981/markdown-it-kroki.git
cd myslides
npm install -D path/to/markdown-it-kroki
```
### **[3rd step]** Create `marp.config.js`.
Here is the configuration file for **Marp**.
```javascript
module.exports = {
inputDir: './slides',
engine: ({ marp }) => marp.use(require('@kazumatu981/markdown-it-kroki'), {
entrypoint: "https://kroki.io",
})
}
```
### **[4th step]** Create your slides
On `slies` directory. you create slides-deck. like this.
---
marp: true
---
## mermaid
```mermaid[mermaid image]
flowchart TD
Start --> Stop
```
### **[5th step]** run server
Run marp server.
```bash
marp -s -c marp.config.js
```
## Detail
### Syntax of Markdown
#### Diagram Language
You have to write diagram language by [fenced code block](https://spec.commonmark.org/0.30/#fenced-code-blocks) syntax, start with **triple back quot** and after that the language.
```plantuml
This package depends on kroki.io.
If you want to know which is **supported diagram language**,
you will see in [Kroki.io official web site (https://kroki.io/)](https://kroki.io/).
#### Alt Text
You can write Alt-text attribute to IMG tag in HTML.
Write in `square blacket` after **Diagram Language**.
```mermaid [check your network config..]
### Options of `constructor`
| property-name | type | mean | defaul value |
| ---------------- | -------- | ------------------------------------------------------ | ------------------------- |
| `entrypoint` | `string` | The entry point for Kroki server. | `'https://kroki.io'` |
| `containerClass` | `string` | class name of container (`DIV`-tag `class` attribute). | `'kroki-image-container'` |
| `imageFormat` | `string` | image format of diagram. see [here](https://kroki.io/) | `'svg'` |

View file

@ -0,0 +1,8 @@
module.exports = {
inputDir: './',
engine: ({ marp }) => marp.use(require('../index'), {
entrypoint: "https://kroki.io",
marpAutoScaling: true
})
}

View file

@ -0,0 +1,59 @@
---
marp: true
---
# @kazumatu981/markdown-it-kroki
## Marp Sample
---
## plantuml
```plantuml[platuml image]
@startuml
left to right direction
actor Guest as g
package Professional {
actor Chef as c
actor "Food Critic" as fc
}
package Restaurant {
usecase "Eat Food" as UC1
usecase "Pay for Food" as UC2
usecase "Drink" as UC3
usecase "Review" as UC4
}
fc --> UC4
g --> UC1
g --> UC2
g --> UC3
@enduml
```
---
## mermaid
```mermaid[mermaid image]
graph TD
A[ Anyone ] -->|Can help | B( Go to github.com/yuzutech/kroki )
B --> C{ How to contribute? }
C --> D[ Reporting bugs ]
C --> E[ Sharing ideas ]
C --> F[ Advocating ]
```
---
## normal code
```JavaScript
function testFunc(test) {
let sum = 0;
for(let x = 1; x<=test; x++) {
sum += x;
}
return sum;
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,6 @@
const { MarkdownItKrokiCore } = require('./lib/plugin-core');
module.exports = (md, opt) => {
const plugin = new MarkdownItKrokiCore(md);
plugin.setOptions(opt).use();
};

View file

@ -0,0 +1,37 @@
'use strict';
module.exports = {
/**
* contract `test` to be non-empty string.
* @param {string} test test string
* @param {string} msg message on exception
*/
toNonEmptyString: function (test, msg) {
if (typeof test !== 'string') throw new Error(msg);
if (test === ''
|| test === null
|| test === undefined) throw new Error(msg);
},
/**
* contract `test` to be true.
* @param {boolean} test test boolean.
* @param {sting} msg massage on excetion.
*/
toTrue: function (test, msg) {
if (typeof test !== 'boolean') throw new Error(msg);
if (!test) throw new Error(msg);
},
toBeUrlString: function (test, msg) {
this.toNonEmptyString(test, msg);
try {
require('url').parse(test);
} catch {
throw new Error(msg);
}
},
toBeClassName: function (test, msg) {
if (!/^[A-Za-z0-9]+(-?[A-Za-z0-9]+)*$/.exec(test)) {
throw new Error(msg);
}
}
};

View file

@ -0,0 +1,27 @@
'use strict';
const { deflateSync } = require('zlib');
const contract = require('./contract');
const support = require('./support');
function encode(diagram) {
return deflateSync(diagram, { level: 9 }).toString('base64url');
}
function generateUrl(entrypoint, lang, imgType, diagram) {
contract.toNonEmptyString(entrypoint, '\'entrypoint\' must be non-empty string.');
contract.toNonEmptyString(lang, '\'lang\' must be non-empty string.');
contract.toNonEmptyString(imgType, '\'imgType\' must be non-empty string.');
contract.toNonEmptyString(diagram, '\'diagram\' must be non-empty string.');
contract.toTrue(support.languageSupports(lang), 'Not Supported Diagram Language.');
contract.toTrue(support.imageFormatSupports(imgType), 'Not Supported Image Type.');
const api = `${lang}/${imgType}/${encode(diagram)}`;
return entrypoint.endsWith('/') ?
`${entrypoint}${api}` : `${entrypoint}/${api}`;
}
module.exports = {
encode, generateUrl
};

View file

@ -0,0 +1,52 @@
const { deflateSync } = require('zlib')
const krokiLangs = [
'actdiag',
'blockdiag',
'bpmn',
'bytefield',
'c4plantuml',
'ditaa',
'dot',
'erd',
'excalidraw',
'graphviz',
'mermaid',
'nomnoml',
'nwdiag',
'packetdiag',
'pikchr',
'plantuml',
'rackdiag',
'seqdiag',
'svgbob',
'umlet',
'vega',
'vegalite',
'wavedrom',
]
const entrypoint = 'https://kroki.io/'
const marpKrokiPlugin = (md) => {
const { fence } = md.renderer.rules
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const info = md.utils.unescapeAll(tokens[idx].info).trim()
if (info) {
const [lang] = info.split(/(\s+)/g)
if (krokiLangs.includes(lang)) {
const data = deflateSync(tokens[idx].content).toString('base64url')
// <marp-auto-scaling> is working only with Marp Core v3
return `<p><marp-auto-scaling data-downscale-only><img src="${entrypoint}${lang}/svg/${data}"/></marp-auto-scaling></p>`
}
}
return fence.call(self, tokens, idx, options, env, self)
}
}
module.exports = marpKrokiPlugin

View file

@ -0,0 +1,74 @@
'use strict';
const support = require('./support');
const contract = require('./contract');
const { safeProperty, safeChoice } = require('./safe-property');
const diagramEncoder = require('./diagram-encoder');
class MarkdownItKrokiCore {
constructor(md) {
this._md = md;
}
setOptions(opt) {
this._entrypoint = safeProperty(opt, "entrypoint", "string", 'https://kroki.io');
this._containerClass = safeProperty(opt, "containerClass", "string", "kroki-image-container");
this._imageFormat = safeProperty(opt, "imageFormat", "string", "svg");
this._imageFormat = safeChoice(this._imageFormat, support.imageFormats, "svg");
this._entrypoint = require('url').parse(this._entrypoint).href;
contract.toBeClassName(this._containerClass, "containerClass must be className.");
return this;
}
use() {
// if _md has `marpit` property then use <marp-auto-scaling> tag
this._marpAutoScaling = this._md['marpit'] !== undefined;
this._defaultFence = this._md.renderer.rules.fence;
this._md.renderer.rules.fence
= (tokens, idx, options, env, self) => this.krokiFencePlugin(tokens, idx, options, env, self);
}
static readLanguageAndAltText(info) {
if (!info) return { language: '', alt: '' };
const trimed = info.trim();
const langFound = /[\s|\[]/.exec(trimed);
const altFound = /\[.*?\]/.exec(trimed);
return {
language: langFound ?
trimed.substring(0, langFound.index) : trimed,
alt: altFound ?
altFound[0].replace('[', '').replace(']', '') : ''
};
}
buildEmbedHTML(langAndAlt, diagramCode) {
// alt build url
const url = diagramEncoder.generateUrl(
this._entrypoint, langAndAlt.language, this._imageFormat, diagramCode);
// sanitize alt
const alt = langAndAlt.alt ?
this._md.utils.escapeHtml(langAndAlt.alt) : undefined;
// build img tag
const imgTag = langAndAlt.alt ?
`<img alt="${alt}" src="${url}" />` : `<img src="${url}" />`;
// build container contents
const containerContents = this._marpAutoScaling ?
`<marp-auto-scaling data-downscale-only>${imgTag}</marp-auto-scaling>` : imgTag;
// build embed HTML
return `<p class="${this._containerClass}">${containerContents}</p>`;
}
krokiFencePlugin(tokens, idx, options, env, self) {
const info = this._md.utils.unescapeAll(tokens[idx].info)
const langAndAlt = MarkdownItKrokiCore.readLanguageAndAltText(info);
return support.languageSupports(langAndAlt.language) ?
this.buildEmbedHTML(langAndAlt, tokens[idx].content) :
this._defaultFence.call(self, tokens, idx, options, env, self);
}
}
module.exports = {
MarkdownItKrokiCore
}

View file

@ -0,0 +1,18 @@
'use strict';
function safeProperty(test, name, type, defaultValue) {
if (test == null || test == undefined) return defaultValue;
if (typeof test[name] !== type) return defaultValue;
if (typeof test[name] === "string" && test[name] === '') return defaultValue;
return test[name];
}
function safeChoice(test, candidates, defaultValue) {
return candidates.includes(test) ?
test : defaultValue;
}
function safeUrl(test) {
}
module.exports = { safeProperty, safeChoice };

View file

@ -0,0 +1,54 @@
'use strict';
/**
* Diagram Languages are supported by kroki.io
*/
const LANGUAGES = [
'actdiag',
'blockdiag',
'bpmn',
'bytefield',
'c4plantuml',
'ditaa',
'dot',
'erd',
'excalidraw',
'graphviz',
'mermaid',
'nomnoml',
'nwdiag',
'packetdiag',
'pikchr',
'plantuml',
'rackdiag',
'seqdiag',
'svgbob',
'umlet',
'vega',
'vegalite',
'wavedrom',
];
/**
* Image formats are supported by kroki.io
*/
const IMG_FORMATS = [
'png', 'svg', 'jpeg', 'pdf', 'base64'
];
module.exports = {
lnaguages: LANGUAGES,
imageFormats: IMG_FORMATS,
/**
* test whether `lang` is supported diagram language by kroki.io
* @param {string} lang target language
* @returns is supported
*/
languageSupports: (lang) => LANGUAGES.includes(lang),
/**
* test whether `format` is supported image format by kroki.io
* @param {string} format name of image format like 'png', 'svg', ... etc
* @returns is supported
*/
imageFormatSupports: (format) => IMG_FORMATS.includes(format)
};

View file

@ -0,0 +1,35 @@
{
"name": "@kazumatu981/markdown-it-kroki",
"version": "1.1.1",
"description": "markdown-it kroki plugin.",
"main": "index.js",
"scripts": {
"test": "mocha tests/**/*.test.js",
"demo": "marp -s -c demo/marp.config.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/kazumatu981/markdown-it-kroki.git"
},
"keywords": [
"markdown-it",
"markdown-it-plugin",
"kroki",
"marp",
"markdown"
],
"author": "kazumatu981",
"license": "MIT",
"bugs": {
"url": "https://github.com/kazumatu981/markdown-it-kroki/issues"
},
"homepage": "https://github.com/kazumatu981/markdown-it-kroki#readme",
"devDependencies": {
"@marp-team/marp-cli": "^2.2.0",
"chai": "^4.3.6",
"jsdom": "^20.0.1",
"markdown-it": "^13.0.1",
"mocha": "^10.1.0",
"nyc": "^15.1.0"
}
}

View file

@ -0,0 +1,94 @@
const MarkdownIt = require('markdown-it');
const { expect } = require('chai');
const { JSDOM } = require('jsdom');
const MarkdownItKroki = require('../../index');
describe('# [Security-test] anti-injecttion for syntax.', () => {
describe("## for alt", () => {
it('* escape double quote', () => {
const expected = 'this is a "test comment" test';
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io",
marpAutoScaling: true,
containerClass: "the-container"
});
const result = md.render(
'```graphviz [this is a "test comment" test]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
const dom = new JSDOM(result);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
const actual = imgTag.getAttribute('alt');
expect(actual).to.be.equal(expected);
})
})
});
describe('# [Security-test] anti-injecttion for option.', () => {
describe("## for entrypoint", () => {
it('* deny invalid URL', () => {
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io\"> <script src=\"xxxx.js",
marpAutoScaling: true,
containerClass: "the-container"
});
const html = md.render(
'```graphviz [this is a test]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
const dom = new JSDOM(html);
const scriptTag = dom.window.document.getElementsByTagName("script");
expect(scriptTag.length).to.equal(0);
})
})
describe("## for containerClass", () => {
it('* throw when containerClass are not alpha, digit, or \"-\"', () => {
const testFunction = () => {
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io",
marpAutoScaling: true,
containerClass: "<container>"
});
const result = md.render(
'```graphviz [this is a test]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
};
expect(testFunction).to.throw();
})
})
describe("## for imageFormat", () => {
it('* ignore invalid imageFormat', () => {
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io",
marpAutoScaling: true,
containerClass: "the-container",
imageFormat: "<injected>"
});
const html = md.render(
'```graphviz [this is a test]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
const actual = imgTag.getAttribute('src');
expect(actual).to.includes('/svg/');
})
})
});

View file

@ -0,0 +1,96 @@
const { expect } = require('chai');
const MarkdownIt = require('markdown-it');
const MarkdownItKroki = require('../../index');
const { JSDOM } = require('jsdom');
const testData = [
// graphviz
{
langname: 'graphviz',
data: '```graphviz svg\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
}
];
describe('# [total test] Test pulugin can Render DOM', () => {
for (const test of testData) {
describe(`## Test for ${test.langname}`, () => {
describe('### no option call test', () => {
it('* Not to Throw on no options', () => {
const testFunction = () => {
// render !
const md = new MarkdownIt();
md.use(MarkdownItKroki);
const _ = md.render(test.data);
};
expect(testFunction).to.not.throw();
});
it('* root DOM item is \'p\' on no option', () => {
// render !
const md = new MarkdownIt();
md.use(MarkdownItKroki);
const result = md.render(test.data);
// find p-tag
const dom = new JSDOM(result);
const document = dom.window.document;
const ptags = document.getElementsByTagName("p");
// test p-tag is only one
expect(ptags.length).to.be.equal(1);
// test root item is p
const thePtag = ptags[0];
expect(thePtag.isSameNode(document.body.firstChild)).to.true;
// test embeded default container class
expect(thePtag.getAttribute('class')).to.be.equal('kroki-image-container');
});
it('* has img tag and source is created by this library.', () => {
// render !
const md = new MarkdownIt();
md.use(MarkdownItKroki);
const result = md.render(test.data);
// find p-tag
const dom = new JSDOM(result);
const document = dom.window.document;
const imgTags = document.getElementsByTagName("img");
// test img-tag is only one
expect(imgTags.length).to.be.equal(1);
const imgTag = imgTags[0];
expect(imgTag.getAttribute('src')).not.to.empty;
})
});
})
}
})
describe('[total test] Can Render', () => {
it('* option constainerClass is embeded.', () => {
const md = new MarkdownIt();
md.use(MarkdownItKroki, {
entrypoint: "https://kroki.io",
marpAutoScaling: true,
containerClass: "the-container"
});
var result = md.render(
'```graphviz [praphviz-image]\r\n' +
'digraph G {Hello->World}\r\n' +
'```\r\n'
);
const dom = new JSDOM(result);
const document = dom.window.document;
const ptags = document.getElementsByTagName("p");
expect(ptags.length).to.be.equal(1);
const thePtag = ptags[0];
expect(thePtag.isSameNode(document.body.firstChild)).to.true;
expect(thePtag.getAttribute('class')).to.be.equal('the-container');
});
});

View file

@ -0,0 +1,35 @@
const expect = require('chai').expect;
const contract = require('../../lib/contract');
describe('# [unit test]: contract.js', () => {
describe('## toNonEmptyString()', () => {
['abc', ' ', ' abc '].forEach((test) => {
it(`* normal cases. { testcase: ${test} }`, () => {
expect(() => {
contract.toNonEmptyString(test);
}).not.to.throw();
})
});
['', null, undefined, 123, 0.123, true, { test: 123 }].forEach((test) => {
it(`* abnormal cases. { testcase: ${test} }`, () => {
expect(() => {
contract.toNonEmptyString(test);
}).to.throw();
})
});
});
describe('## toTrue', () => {
it('* normal case. {test case: true}', () => {
expect(() => {
contract.toTrue(true);
}).not.to.throw();
});
['', null, undefined, 123, 0.123, false, { test: 123 }].forEach((test) => {
it(`* abnormal cases. { testcase: ${test} }`, () => {
expect(() => {
contract.toTrue(test);
}).to.throw();
})
});
});
})

View file

@ -0,0 +1,75 @@
const expect = require('chai').expect;
const { inflateSync } = require('zlib');
const { encode, generateUrl } = require('../../lib/diagram-encoder');
describe('# [unit-test]: diagram-encoder.js', () => {
describe('## [function]: encode()', () => {
it('* encoded data is able to decode.', () => {
const testFunc = () => {
const expected = '@startuml\nBob -> Alice : hello\n@enduml';
const encoded = encode(expected);
const deflated = Buffer.from(encoded, "base64url");
const actual = inflateSync(deflated).toString();
expect(actual).to.be.equal(expected);
}
expect(testFunc).not.to.Throw();
});
});
describe('## [function]: generateUrl()', () => {
it('* must start format like <entry-point>/<lang>/<format>/', () => {
const actual = generateUrl('https://kroki.io', 'graphviz', 'svg', 'digraph G {Hello->World}');
const expected = 'https://kroki.io/graphviz/svg/';
expect(actual).to.be.a('string');
expect(actual.startsWith(expected)).to.be.true;
});
it('* must endwith <encoded>', () => {
const actual = generateUrl('https://kroki.io', 'graphviz', 'svg', 'digraph G {Hello->World}');
const expected = encode('digraph G {Hello->World}');
expect(actual).to.be.a('string');
expect(actual.endsWith(expected)).to.be.true;
});
[1, '', null, undefined].forEach(test => {
it(`* [exception] throws when entry-point is, non-string object, empty string, null or undefined. Test: ${test}`, () => {
const testFunction = () => {
let _ = generateUrl(test, 'graphviz', 'svg', 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
it(`* [exception] throws when lang is, non-string object, empty string, null or undefined. Test: ${test}`, () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', test, 'svg', 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
it(`* [exception] throws when imgType is, non-string object, empty string, null or undefined. Test: ${test}`, () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', 'graphviz', test, 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
it(`* [exception] throws when diagram is, non-string object, empty string, null or undefined. Test: ${test}`, () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', 'graphviz', 'svg', diagram);
}
expect(testFunction).throw();
});
});
it('* [exception] throws when lang is unsupported lang', () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', 'graphviz123', 'svg', 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
it('* [exception] throws when imgType is unsupported imgType', () => {
const testFunction = () => {
let _ = generateUrl('https://kroki.io', 'graphviz', 'svg123', 'digraph G {Hello->World}');
}
expect(testFunction).throw();
});
});
});

View file

@ -0,0 +1,146 @@
const md = require('markdown-it');
const expect = require('chai').expect;
const { JSDOM } = require('jsdom');
const { MarkdownItKrokiCore } = require('../../lib/plugin-core');
const { encode } = require('../../lib/diagram-encoder');
describe('# [unit-test] plugin-core.js', () => {
describe('## method: buuildEmbedHTML', () => {
describe('### langAndAlt.language', () => {
[null, undefined, ''].forEach((test) => {
it(`* when langAndAlt.language is null or empty, throws error. testcase:${test}`, () => {
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
const testFunc = () => {
plugin.use();
const _ = plugin.buildEmbedHTML(
{ language: test, alt: '' }, diagramCode);
};
expect(testFunc).to.throw();
});
});
it('* language embeded in to url', () => {
const test = 'plantuml';
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
// build embed HTML
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
plugin.use();
const html = plugin.buildEmbedHTML({ language: test, alt: '' }, diagramCode);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
// get url attribute
const url = imgTag.getAttribute('src');
expect(/\/plantuml\//.test(url)).to.true;
});
});
describe('### langAndAlt.alt', () => {
[null, undefined, ''].forEach((test) => {
it(`* when langAndAlt.alt is null or empty, no alt attribute. testcase:${test}`, () => {
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
// prepair
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
plugin.use();
// render
const html = plugin.buildEmbedHTML(
{ language: 'plantuml', alt: test }, diagramCode);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
expect(imgTag.hasAttribute('alt')).to.false;
});
});
it('* embeded altText', () => {
const expected = "this is test Text";
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
// prepair
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
plugin.use();
// render
const html = plugin.buildEmbedHTML(
{ language: 'plantuml', alt: expected }, diagramCode);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
expect(imgTag.getAttribute('alt')).to.equal(expected);
});
});
describe('### diagramCode', () => {
[null, undefined, ''].forEach((test) => {
it(`* when diagramCode is null or empty, throws error. testcase:${test}`, () => {
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
const testFunc = () => {
plugin.use();
const _ = plugin.buildEmbedHTML(
{ language: 'plantuml', alt: '' }, test);
};
expect(testFunc).to.throw();
});
});
it('* encoded diagram must be embed to url on <img src=\'....\' ', () => {
const test = '@startuml\nBob -> Alice : hello\n @enduml';
const expected = encode(test);
// build embed HTML
const plugin = new MarkdownItKrokiCore(new md()).setOptions();
plugin.use();
const html = plugin.buildEmbedHTML({ language: 'plantuml', alt: '' }, test);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
// get url attribute
const url = imgTag.getAttribute('src');
expect(url.endsWith(expected)).to.true;
});
it('* <img> is surounded by <marp-auto-scaling> on to be used form marp-it', () => {
const test = '@startuml\nBob -> Alice : hello\n @enduml';
const markdownIt = new md()
markdownIt['marpit'] = { someObject: 'is implemented' };
// build embed HTML
const plugin = new MarkdownItKrokiCore(markdownIt).setOptions();
plugin.use();
const html = plugin.buildEmbedHTML({ language: 'plantuml', alt: '' }, test);
// parse dom
const dom = new JSDOM(html);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
const marpAutoScaling = dom.window.document.getElementsByTagName("marp-auto-scaling")[0];
expect(imgTag.isSameNode(marpAutoScaling.firstChild)).to.be.true;
});
it('* <img> is surounded by <marp-auto-scaling> on not to be used form marp-it', () => {
const test = '@startuml\nBob -> Alice : hello\n @enduml';
const markdownIt = new md()
// build embed HTML
const plugin = new MarkdownItKrokiCore(markdownIt).setOptions();
plugin.use();
const html = plugin.buildEmbedHTML({ language: 'plantuml', alt: '' }, test);
// parse dom
const dom = new JSDOM(html);
const marpAutoScaling = dom.window.document.getElementsByTagName("marp-auto-scaling");
expect(marpAutoScaling.length).to.equal(0);
})
});
});
});

View file

@ -0,0 +1,178 @@
const md = require('markdown-it');
const expect = require('chai').expect;
const { JSDOM } = require('jsdom');
const { MarkdownItKrokiCore } = require('../../lib/plugin-core');
describe('# [unit-test] plugin-core.js', () => {
describe('## method: setOptions() must be work', () => {
function buildHtmlForTest(options) {
const test = 'plantuml';
const diagramCode = '@startuml\nBob -> Alice : hello\n @enduml';
// build embed HTML
const plugin = new MarkdownItKrokiCore(new md()).setOptions(options);
plugin.use();
return plugin.buildEmbedHTML({ language: test }, diagramCode);
}
describe('### entrypoint', () => {
function expectEntryPointToEmbed(htmlString, expected) {
if (!expected) expected = 'https://kroki.io';
// parse dom
const dom = new JSDOM(htmlString);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
// get url attribute
const url = imgTag.getAttribute('src');
expect(url.startsWith(expected)).to.true;
}
it('* no options', () => {
const html = buildHtmlForTest();
expectEntryPointToEmbed(html);
});
it('* option is null', () => {
const html = buildHtmlForTest({ entrypoint: null });
expectEntryPointToEmbed(html);
});
it('* option is undefined', () => {
const html = buildHtmlForTest({ entrypoint: undefined });
expectEntryPointToEmbed(html);
});
it('* option is \'\'', () => {
const html = buildHtmlForTest({ entrypoint: '' });
expectEntryPointToEmbed(html);
});
it('* option is 1', () => {
const html = buildHtmlForTest({ entrypoint: 1 });
expectEntryPointToEmbed(html);
});
it('* option is true', () => {
const html = buildHtmlForTest({ entrypoint: true });
expectEntryPointToEmbed(html);
});
it('* option is \'https://localhost:8080\'', () => {
const html = buildHtmlForTest({
entrypoint: 'https://localhost:8080'
});
expectEntryPointToEmbed(html, 'https://localhost:8080');
});
});
describe.skip('### marpAutoScaling', () => {
function expectMarpAutoScalingToEmbed(htmlString, expected) {
// parse dom
const dom = new JSDOM(htmlString);
const tags = dom.window.document.getElementsByTagName("marp-auto-scaling");
if (expected) {
expect(tags).not.to.empty;
} else {
expect(tags).to.empty;
}
}
it('* no options', () => {
const html = buildHtmlForTest();
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is null', () => {
const html = buildHtmlForTest({ marpAutoScaling: null });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is undefined', () => {
const html = buildHtmlForTest({ marpAutoScaling: undefined });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is \'\'', () => {
const html = buildHtmlForTest({ marpAutoScaling: '' });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is 1', () => {
const html = buildHtmlForTest({ marpAutoScaling: 1 });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is \'test\'', () => {
const html = buildHtmlForTest({ marpAutoScaling: 'test' });
expectMarpAutoScalingToEmbed(html, true);
});
it('* option is false', () => {
const html = buildHtmlForTest({ marpAutoScaling: false });
expectMarpAutoScalingToEmbed(html, false);
});
})
describe('### containerClass', () => {
function expectContainerClassToEmbed(htmlString, className) {
// parse dom
const dom = new JSDOM(htmlString);
const pTag = dom.window.document.getElementsByTagName("p")[0];
const actualClassName = pTag.getAttribute('class');
expect(actualClassName).to.equal(className);
}
it('* no options', () => {
const html = buildHtmlForTest();
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is null', () => {
const html = buildHtmlForTest({ containerClass: null });
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is undefined', () => {
const html = buildHtmlForTest({ containerClass: undefined });
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is \'\'', () => {
const html = buildHtmlForTest({ containerClass: '' });
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is 1', () => {
const html = buildHtmlForTest({ containerClass: 1 });
expectContainerClassToEmbed(html, 'kroki-image-container');
});
it('* option is \'containerClass\'', () => {
const html = buildHtmlForTest({ containerClass: 'containerClass' });
expectContainerClassToEmbed(html, 'containerClass');
});
});
describe('### imageFormat', () => {
function expectImageFormatToEmbed(htmlString, expected) {
// parse dom
const dom = new JSDOM(htmlString);
const imgTag = dom.window.document.getElementsByTagName("img")[0];
// get url attribute
const url = imgTag.getAttribute('src');
expect(url).to.includes('/' + expected + '/');
}
it('* no options', () => {
const html = buildHtmlForTest();
expectImageFormatToEmbed(html, 'svg');
});
it('* option is null', () => {
const html = buildHtmlForTest({ imageFormat: null });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is undefined', () => {
const html = buildHtmlForTest({ imageFormat: undefined });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is \'\'', () => {
const html = buildHtmlForTest({ imageFormat: '' });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is 1', () => {
const html = buildHtmlForTest({ imageFormat: 1 });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is \'test\'', () => {
const html = buildHtmlForTest({ imageFormat: 'test' });
expectImageFormatToEmbed(html, 'svg');
});
it('* option is \'png\'', () => {
const html = buildHtmlForTest({ imageFormat: 'png' });
expectImageFormatToEmbed(html, 'png');
});
});
});
});

View file

@ -0,0 +1,49 @@
const expect = require('chai').expect;
const { MarkdownItKrokiCore } = require('../../lib/plugin-core');
describe('# [unit-test] plugin-core.js', () => {
describe('## static method: readLanguageAndAltText() - language', () => {
[
{ test: null, expected: '' },
{ test: undefined, expected: '' },
{ test: '', expected: '' },
{ test: ' ', expected: '' },
{ test: 'plantuml', expected: 'plantuml' },
{ test: ' plantuml', expected: 'plantuml' },
{ test: 'plantuml ', expected: 'plantuml' },
{ test: 'plantuml +++', expected: 'plantuml' },
{ test: 'html+md', expected: 'html+md' },
{ test: 'graphviz[]', expected: 'graphviz' },
{ test: 'graphviz[test]', expected: 'graphviz' },
{ test: 'graphviz [test test]', expected: 'graphviz' },
].forEach(testCase => {
it(`### Can read diagramLanguage. in case \'${testCase.test}\'`, () => {
const actual = MarkdownItKrokiCore.readLanguageAndAltText(testCase.test);
const expected = testCase.expected;
expect(actual.language).to.be.equal(expected);
})
});
});
describe('## static method: readLanguageAndAltText() - alt', () => {
[
{ test: null, expected: '' },
{ test: undefined, expected: '' },
{ test: '', expected: '' },
{ test: ' ', expected: '' },
{ test: 'plantuml', expected: '' },
{ test: ' plantuml', expected: '' },
{ test: 'plantuml ', expected: '' },
{ test: 'plantuml +++', expected: '' },
{ test: 'html+md', expected: '' },
{ test: 'graphviz[]', expected: '' },
{ test: 'graphviz[test]', expected: 'test' },
{ test: 'graphviz [test test]', expected: 'test test' },
].forEach(testCase => {
it(`### Can read diagramLanguage. in case \'${testCase.test}\'`, () => {
const actual = MarkdownItKrokiCore.readLanguageAndAltText(testCase.test);
const expected = testCase.expected;
expect(actual.alt).to.be.equal(expected);
})
});
});
});

View file

@ -0,0 +1,120 @@
const { expect } = require('chai');
const { safeProperty } = require('../../lib/safe-property');
describe('# [unit-test]: safe-property.js', () => {
[
{
testCaseDescription: "standard test - string",
testCase: {
test: {
property1: "hello"
},
name: "property1",
type: "string",
defaultValue: undefined
},
expected: "hello"
},
{
testCaseDescription: "standard test - boolean",
testCase: {
test: {
property1: true
},
name: "property1",
type: "boolean",
defaultValue: undefined
},
expected: true
},
{
testCaseDescription: "standard test - boolean on null",
testCase: {
test: {
property1: null
},
name: "property1",
type: "boolean",
defaultValue: false
},
expected: false
},
{
testCaseDescription: "on null",
testCase: {
test: {
property1: null
},
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on empty string",
testCase: {
test: {
property1: ''
},
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on undefined",
testCase: {
test: {
property1: undefined
},
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on not mutch type",
testCase: {
test: {
property1: 1
},
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on object is null",
testCase: {
test: null,
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
{
testCaseDescription: "on object is undefined",
testCase: {
test: undefined,
name: "property1",
type: "string",
defaultValue: "hello"
},
expected: "hello"
},
].forEach((testItem) => {
it(`* ${testItem.testCaseDescription}`, () => {
const actual = safeProperty(
testItem.testCase.test,
testItem.testCase.name,
testItem.testCase.type,
testItem.testCase.defaultValue);
expect(actual).to.equal(testItem.expected);
});
})
})

18
down-the-stack-book/package-lock.json generated Normal file
View file

@ -0,0 +1,18 @@
{
"name": "down-the-stack-book",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@kazumatu981/markdown-it-kroki": "^1.1.1"
}
},
"node_modules/@kazumatu981/markdown-it-kroki": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kazumatu981/markdown-it-kroki/-/markdown-it-kroki-1.1.1.tgz",
"integrity": "sha512-LDYl+mV2WogLQ5r4olxovm+gphL/MNGfWZ1M1woBO/YhFnfwdn5EAUu9zF/KoVZzytJPq0RNfyeDtMkv+GJihg==",
"dev": true
}
}
}

View file

@ -0,0 +1,5 @@
{
"devDependencies": {
"@kazumatu981/markdown-it-kroki": "^1.1.1"
}
}