@opendevise/antora-swagger-ui-extension
Version:
An Antora extension that integrates the Swagger UI (interactive API explorer) into an Antora site through AsciiDoc syntax extensions.
100 lines (92 loc) • 4.27 kB
JavaScript
const fs = require('node:fs')
const { inspect } = require('node:util')
const { posix: path } = require('node:path')
const toProc = require('./util/to-proc')
const yaml = require('js-yaml')
const INSPECT_OPTIONS = { depth: Infinity, maxArrayLength: Infinity, maxStringLength: Infinity, compact: false }
const SPEC_FAMILIES = ['attachment', 'example', 'partial']
function register (registry, context) {
if (!(registry && context)) return // NOTE only works as a scoped extension
registry.$groups().$store('swagger-ui/block-macro', toProc(createExtensionGroup(context)))
return registry
}
function createExtensionGroup ({ contentCatalog, file }) {
return function () {
this.blockMacro(function () {
this.named('swagger-ui')
this.process((parent, target, attrs) => {
let specFile, topicPath
if (/^https?:[/]{2}/.test(target)) {
const specUrl = target
const { hostname, pathname } = new URL(specUrl)
const extname = path.extname(pathname)
const pathnameWithoutExtname = extname ? pathname.slice(0, -extname.length) : pathname
const relativeWithoutExtname = hostname.replace(/\./g, '-') + pathnameWithoutExtname.replace(/[/.]/g, '-')
specFile = {
src: {
component: file.src.component,
version: file.src.version,
module: file.src.module,
family: 'attachment',
relative: relativeWithoutExtname + extname,
stem: relativeWithoutExtname,
url: specUrl,
},
}
topicPath = ''
} else if (!(specFile = contentCatalog.resolveResource(target, file.src, undefined, SPEC_FAMILIES))) {
const message = `OpenAPI specification to visualize could not be resolved: ${target}`
return log(parent.getDocument(), 'warn', message)
} else if ((topicPath = path.dirname(specFile.src.relative)) && ~topicPath.indexOf(path.sep + '_')) {
topicPath = topicPath
.split(path.sep)
.filter((it) => !it.startsWith('_'))
.join(path.sep)
}
const autoId = `swagger-ui-${parent.getDocument().counter('swagger-ui-number')}`
const id = attrs.id || autoId
const docExpansion = { full: 'full', list: 'list', none: 'none' }[attrs.docExpansion] || 'list'
const specLoaderSrc = {
component: specFile.src.component,
version: specFile.src.version,
module: specFile.src.module,
family: 'attachment',
relative: path.join(topicPath, specFile.src.stem.replace(/^_+/, '') + '-spec-loader.js'),
}
let specLoaderFile = contentCatalog.getById(specLoaderSrc)
if (!specLoaderFile) {
const { source: specLoaderFn } = require('./get-spec-loader')
let specSource
if (!(specSource = specFile.src.url)) {
try {
specSource = parseContents(specFile)
} catch (ex) {
specSource = {}
const message = `OpenAPI specification to visualize could not be parsed: ${target}\n${ex.message}`
log(parent.getDocument(), 'error', message)
}
}
specLoaderFile = contentCatalog.addFile({
contents: Buffer.from(`${specLoaderFn}(${inspect(specSource, INSPECT_OPTIONS)})\n`),
path: path.join('modules', specLoaderSrc.module, specLoaderSrc.family, specLoaderSrc.relative),
src: specLoaderSrc,
})
}
const specLoaderInfo = { url: specLoaderFile.pub.url, config: { target: id, 'doc-expansion': docExpansion } }
;(file.asciidoc.attributes['page-openapi-spec-loaders'] ||= []).push(specLoaderInfo)
return this.createBlock(parent, 'pass', `<div id="${id}"></div>`)
})
})
}
}
function parseContents (file) {
return file.mediaType === 'text/yaml'
? yaml.load(file.contents, { schema: yaml.JSON_SCHEMA, json: true })
: JSON.parse(file.contents)
}
function log (doc, level, message) {
const cursor = doc.getReader().$cursor_at_mark()
doc.getLogger()[level](doc.createLogMessage(message, { source_location: cursor }))
}
module.exports = { register, createExtensionGroup }