UNPKG

@campbell-bartlett/asciidoctor-plantuml

Version:
185 lines (172 loc) 6.48 kB
const plantumlEncoder = require('plantuml-encoder') /** * Convert an (Opal) Hash to JSON. * @private */ const fromHash = function (hash) { const object = {} const data = hash.$$smap for (let key in data) { object[key] = data[key] } return object } function UnsupportedFormat (message) { this.name = 'UnsupportedFormat' this.message = message this.stack = (new Error()).stack } // eslint-disable-next-line new-parens UnsupportedFormat.prototype = new Error function UndefinedPlantumlServer (message) { this.name = 'UndefinedPlantumlServer' this.message = message this.stack = (new Error()).stack } // eslint-disable-next-line new-parens UndefinedPlantumlServer.prototype = new Error function createImageSrc (doc, text, target, format, vfs) { const serverUrl = doc.getAttribute('plantuml-server-url') const shouldFetch = doc.isAttribute('plantuml-fetch-diagram') let diagramUrl = `${serverUrl}/${format}/${plantumlEncoder.encode(text)}` if (shouldFetch) { diagramUrl = require('./fetch').save(diagramUrl, doc, target, format, vfs) } return diagramUrl } function processPlantuml (processor, parent, attrs, diagramType, diagramText, context) { const doc = parent.getDocument() // If "subs" attribute is specified, substitute accordingly. // Be careful not to specify "specialcharacters" or your diagram code won't be valid anymore! const subs = attrs.subs if (subs) { diagramText = parent.$apply_subs(diagramText, parent.$resolve_subs(subs), true) } if (!/^@start([a-z]+)\n[\s\S]*\n@end\1$/.test(diagramText)) { if (diagramType === 'plantuml') { diagramText = '@startuml\n' + diagramText + '\n@enduml' } else if (diagramType === 'ditaa') { diagramText = '@startditaa\n' + diagramText + '\n@endditaa' } else if (diagramType === 'graphviz') { diagramText = '@startdot\n' + diagramText + '\n@enddot' } } const serverUrl = doc.getAttribute('plantuml-server-url') const role = attrs.role const blockId = attrs.id const title = attrs.title if (serverUrl) { const target = attrs.target const format = attrs.format || 'png' if (format === 'png' || format === 'svg') { const imageUrl = createImageSrc(doc, diagramText, target, format, context.vfs) const blockAttrs = { role: role ? `${role} plantuml` : 'plantuml', target: imageUrl, alt: target || 'diagram', title } if (blockId) blockAttrs.id = blockId return processor.createImageBlock(parent, blockAttrs) } else { throw new UnsupportedFormat(`Format '${format}' is unsupported. Only 'png' and 'svg' are supported by the PlantUML server`) } } else { throw new UndefinedPlantumlServer('PlantUML server URL is undefined. Please use the :plantuml-server-url: attribute to configure it.') } } function plantumlBlock (context) { return function () { const self = this self.onContext(['listing', 'literal']) self.positionalAttributes(['target', 'format']) self.process((parent, reader, attrs) => { if (typeof attrs === 'object' && '$$smap' in attrs) { attrs = fromHash(attrs) } const diagramType = this.name.toString() const role = attrs.role let diagramText = reader.getString() try { return processPlantuml(this, parent, attrs, diagramType, diagramText, context) } catch (e) { if (e.name === 'UnsupportedFormat' || e.name === 'UndefinedPlantumlServer') { console.warn(`Skipping ${diagramType} block. ${e.message}`) attrs.role = role ? `${role} plantuml-error` : 'plantuml-error' return this.createBlock(parent, attrs['cloaked-context'], diagramText, attrs) } throw e } }) } } function plantumlBlockMacro (name, context) { return function () { const self = this self.named(name) self.process((parent, target, attrs) => { if (typeof attrs === 'object' && '$$smap' in attrs) { attrs = fromHash(attrs) } let vfs = context.vfs if (typeof vfs === 'undefined' || typeof vfs.read !== 'function') { vfs = require('./node-fs') } const role = attrs.role const diagramType = name target = parent.$apply_subs(target) let diagramText = vfs.read(target) try { return processPlantuml(this, parent, attrs, diagramType, diagramText, context) } catch (e) { if (e.name === 'UnsupportedFormat' || e.name === 'UndefinedPlantumlServer') { console.warn(`Skipping ${diagramType} block macro. ${e.message}`) attrs.role = role ? `${role} plantuml-error` : 'plantuml-error' return this.createBlock(parent, 'paragraph', `${e.message} - ${diagramType}::${target}[]`, attrs) } throw e } }) } } const antoraAdapter = (file, contentCatalog) => ({ add: (image) => { const { component, version, module } = file.src contentCatalog.addFile({ contents: image.contents, src: { component, version, module, family: 'image', mediaType: image.mediaType, basename: image.basename, relative: image.basename } }) } }) module.exports.register = function register (registry, context = {}) { // patch context in case of Antora if (typeof context.contentCatalog !== 'undefined' && typeof context.contentCatalog.addFile !== 'undefined' && typeof context.contentCatalog.addFile === 'function' && typeof context.file !== 'undefined') { context.vfs = antoraAdapter(context.file, context.contentCatalog) } if (typeof registry.register === 'function') { registry.register(function () { this.block('plantuml', plantumlBlock(context)) this.block('ditaa', plantumlBlock(context)) this.block('graphviz', plantumlBlock(context)) this.blockMacro(plantumlBlockMacro('plantuml', context)) this.blockMacro(plantumlBlockMacro('ditaa', context)) this.blockMacro(plantumlBlockMacro('graphviz', context)) }) } else if (typeof registry.block === 'function') { registry.block('plantuml', plantumlBlock(context)) registry.block('ditaa', plantumlBlock(context)) registry.block('graphviz', plantumlBlock(context)) registry.blockMacro(plantumlBlockMacro('plantuml', context)) registry.blockMacro(plantumlBlockMacro('ditaa', context)) registry.blockMacro(plantumlBlockMacro('graphviz', context)) } return registry }