first commit

This commit is contained in:
CrazyJasonwell
2026-07-23 20:36:13 +08:00
commit 513b68be8c
7915 changed files with 2642530 additions and 0 deletions
@@ -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 {
new URL(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);
}
}
};
@@ -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
};
@@ -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
@@ -0,0 +1,85 @@
'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._useImg = safeProperty(opt, "useImg", "boolean", false);
this._render = safeProperty(opt, "render", "function", undefined);
this._imageFormat = safeChoice(this._imageFormat, support.imageFormats, "svg");
contract.toBeUrlString(this._entrypoint, "entrypoint must be url string.");
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
let imgTag;
if(this._render) {
imgTag = this._render(url, alt);
} else if (this._useImg) {
imgTag = langAndAlt.alt ?
`<img alt="${alt}" src="${url}" />` : `<img src="${url}" />`;
} else {
imgTag = langAndAlt.alt ?
`<embed title="${alt}" src="${url}" />` : `<embed 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
}
@@ -0,0 +1,15 @@
'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;
}
module.exports = { safeProperty, safeChoice };
@@ -0,0 +1,56 @@
'use strict';
/**
* Diagram Languages are supported by kroki.io
*/
const LANGUAGES = [
'actdiag',
'blockdiag',
'bpmn',
'bytefield',
'c4plantuml',
'dbml',
'ditaa',
'dot',
'd2',
'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)
};