'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var nomnoml = require('nomnoml'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var nomnoml__default = /*#__PURE__*/_interopDefaultLegacy(nomnoml); const isObject = (x) => Object.prototype.toString.call(x) === '[object Object]' && x !== null; const lightness = (rgb) => { const [r, g, b] = rgb.map((n) => n / 255); return (Math.max(r, g, b) + Math.min(r, g, b)) / 2; }; const RE_COLOR = /^(?:(#?)([a-f\d]{6}))$|^(?:(rgba?)\(((?:\s*\d+\s*)(?:,?\s*\d+\s*){2}(?:,?\s*(?:0\.)?\d+\s*)?))\)$/i; const RE_MATCH_HEX = /[a-f\d]{2}/g; const RE_MATCH_NUM = /(?:\d+\.)?\d+/g; const parseColor = (color) => { const [, maybeHex, hex, maybeRgb, rgb] = RE_COLOR.exec(color) || []; switch (maybeHex ?? maybeRgb) { case '': case '#': { return (hex.match(RE_MATCH_HEX) || []).map((n) => Number.parseInt(n, 16)); } case 'rgb': { return (rgb.match(RE_MATCH_NUM) || []).map(Number); } case 'rgba': { const [r, g, b, a] = (rgb.match(RE_MATCH_NUM) || []).map(Number); if (a < 0.9) { // low confidence in predicting "light" vs. "dark" theme // note: Chrome getComputedStyle returns a defaults of // rgba(0, 0, 0, 0) as the background "white" :/ return false; } return [r, g, b]; } default: return false; } }; const cssStyleChain = (...styles) => styles .slice() .reverse() .reduce((acc, style) => (!acc ? style : `var(${style}, ${acc})`), ''); const resolveVariant = (autothemes) => { const fromBackground = autothemes .map((theme) => { const rgb = parseColor(theme.backgroundColor); if (!rgb) { return false; } return { variant: (lightness(rgb) >= 0.5 ? 'light' : 'dark'), autotheme: theme, }; }) .filter(Boolean) .pop(); if (fromBackground) { return fromBackground; } const fromForeground = autothemes .map((theme) => { const rgb = parseColor(theme.foregroundColor); if (!rgb) { return false; } return { variant: (lightness(rgb) <= 0.5 ? 'light' : 'dark'), autotheme: theme, }; }) .filter(Boolean) .pop(); if (fromForeground) { return fromForeground; } return false; }; const getThemeStyles = (autotheme) => { if (!autotheme || !autotheme.length) { return {}; } const themeVariant = resolveVariant(autotheme); if (!themeVariant) { return {}; } const { variant, autotheme: { foregroundColor, backgroundColor }, } = themeVariant; return { stroke: cssStyleChain('--nomnoml-svg-stroke', '--theme-color', foregroundColor), fill: [ cssStyleChain('--nomnoml-svg-fill-1', variant === 'light' ? '--mono-tint2' : '--mono-shade2', backgroundColor), cssStyleChain('--nomnoml-svg-fill-2', variant === 'light' ? '--mono-tint3' : '--mono-shade3', backgroundColor), ].join('; '), }; }; const RE_MATCH_ATTRIBUTES = /(["'])(?:\\\1|.)*?\1|\S+=(["'])(?:\\\2|.)*?\2|\S+/g; const RE_MATCH_KEY_VALUE_PAIR = /([^=]+)(?:=(.+))?/; const RE_REPLACE_QUOTES_OUTER = [/^(["'])(.*)\1$/g, '$2']; const RE_REPLACE_QUOTES_INNER = [/^([^"']*?)(["'])(.*)\2$/, '$1$3']; const RE_REPLACE_QUOTES_BROKE = [/(?:(^|=)["'])/g, '$1']; const RE_REPLACE_ESCAPE_CHARS = [/\\+/g, '']; const parseAttributes = (language, constrainBy) => { const matches = language.match(RE_MATCH_ATTRIBUTES); return (matches || []) .map((match) => match .replace(...RE_REPLACE_QUOTES_OUTER) .replace(...RE_REPLACE_QUOTES_INNER) .replace(...RE_REPLACE_QUOTES_BROKE) .replace(...RE_REPLACE_ESCAPE_CHARS)) .reduce((attributes, keyValue) => { const [, key, value] = keyValue.match(RE_MATCH_KEY_VALUE_PAIR) || []; if (!constrainBy || (value && constrainBy.includes(key))) { return Object.assign(attributes, { [key]: value, }); } return attributes; }, {}); }; const createValidator = (regex) => (input) => typeof input === 'string' && regex.test(input); const RE_VALID_HTML_ATTRIBUTE = /^[a-z][a-z0-9_.:-]*$/i; const isValidHtmlAttribute = createValidator(RE_VALID_HTML_ATTRIBUTE); const RE_VALID_CSS_CLASS = /^[a-z_-][a-z0-9_-]*$/i; const isValidCssClass = createValidator(RE_VALID_CSS_CLASS); const RE_VALID_NOMNOML_DIRECTIVE = /^[a-z]+$/i; const isValidNomnomlDirective = createValidator(RE_VALID_NOMNOML_DIRECTIVE); const RESERVED = { '&': 'amp', '<': 'lt', '>': 'gt', '"': 'quot', }; const RE_RESERVED = RegExp(`[${Object.keys(RESERVED).join('')}]`, 'g'); const encodeHtmlReserved = (input) => input.replace(RE_RESERVED, (match) => `&${RESERVED[match]};`); const stringifyAttributes = (attributes) => Object.entries(attributes).reduce((acc, [key, value]) => isValidHtmlAttribute(key) ? `${acc}${acc ? ' ' : ''}${key}="${encodeHtmlReserved(value)}"` : acc, ''); const stringifyDirectives = (directives) => Object.entries(directives).reduce((acc, [key, value]) => isValidNomnomlDirective(key) ? `${acc}${acc ? '\n' : ''}#${key}: ${value}` : acc, ''); const USER_ATTRIBUTES = ['title', 'class', 'width', 'height']; // yes, yes, using RegExp to parse HTML... but it's just one tag. const RE_MATCH_SVG_TAG = /(.*?)()/i; const createSvgCreator = ({ nomnoml, config, autotheme }) => (nomlnomlStr, attributes) => { const userDirectives = isObject(config.directives) ? stringifyDirectives(config.directives) : ''; let themeDirectives = ''; if (autotheme && config.autotheme !== false) { themeDirectives = stringifyDirectives(getThemeStyles(autotheme)); } const directives = [themeDirectives, userDirectives] .filter(Boolean) .join('\n'); const separator = directives ? '\n' : ''; const svg = nomnoml.renderSvg(`${directives}${separator}${nomlnomlStr}`); const match = svg.match(RE_MATCH_SVG_TAG); if (!match) { return new Error('[docsify-nomnoml] unable to wrap svg, unexpected input'); } const [{ length: tagLength }, potentialCruft, tagOpen, tagAttributes, tagClose,] = match; const svgAttributes = parseAttributes(tagAttributes); const userAttributes = parseAttributes(attributes, USER_ATTRIBUTES); const finalAttributes = { ...svgAttributes, role: 'img', 'aria-label': userAttributes.title || 'An SVG render of a UML diagram.', class: [svgAttributes.class, 'nomnoml-svg', userAttributes.class] .filter(isValidCssClass) .join(' '), }; if (userAttributes.width) { finalAttributes.width = userAttributes.width; if (!userAttributes.height) { finalAttributes.height = 'auto'; } } if (userAttributes.height) { finalAttributes.height = userAttributes.height; if (!userAttributes.width) { finalAttributes.width = 'auto'; } } const titleElement = userAttributes.title ? `${encodeHtmlReserved(userAttributes.title)}` : ''; return [ potentialCruft, `${tagOpen} `, stringifyAttributes(finalAttributes), tagClose, tagClose === '/>' ? '' : titleElement, svg.substring(tagLength + match.index), ].join(''); }; const createRenderInterceptCreator = (renderer) => (type, interceptor) => { const existingInterceptor = renderer[type]; renderer[type] = function () { // self-acknowledging defeat: this typing is a bit janky :/ const result = interceptor.apply(this, arguments); if (result !== false) { return result; } return existingInterceptor ? existingInterceptor.apply(this, arguments) : this.origin[type].apply(this, arguments); }; }; const createPlugin$1 = (dependencies) => { const svgCreator = createSvgCreator(dependencies); return (hook, vm) => { hook.init(() => { if (!vm.config.markdown) { vm.config.markdown = {}; } if (!vm.config.markdown.renderer) { vm.config.markdown.renderer = {}; } const interceptCreator = createRenderInterceptCreator(vm.config.markdown.renderer); interceptCreator('code', function (content, type, isEscaped) { const isNomnoml = type && /^(?:nom){1,2}l\srender(?:svg)?/i.test(type); if (!content || !isNomnoml) { return false; } try { const maybeRender = svgCreator(content, type); if (maybeRender instanceof Error) { console.error(maybeRender.message, maybeRender); return false; } return maybeRender; } catch (e) { console.error('[nomnoml]', e); return false; } }); }); }; }; const createPlugin = (config) => createPlugin$1({ nomnoml: nomnoml__default['default'], config }); /* * This module was part of an inital experiment to register a plugin with docsify * locally via "npm install" but has been shelved for the moment - it is subject * to change and should not be relied on. */ exports.createPlugin = createPlugin;