blob: bfee71a8f10c7d087a99d07529bb930c31c0e094 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
"use strict"
/*
* This is an Asciidoctor.js extension to process `apiref` inline macro generated by `log4j-docgen`.
* The logic here is adapted from the `log4j-docgen-asciidoctor-extension`, in particular, `TypeLookup` and `ApirefMacro` classes.
*/
const fs = require("fs")
const { posix: path } = require("path")
const { XMLParser, XMLBuilder, XMLValidator} = require("fast-xml-parser")
const Handlebars = require("handlebars");
// Register a `replaceAll()` helper to Handlebars.
// This will be used while converting artifact information (`groupId`, `artifactId`, etc.) to a target link
// See its usage in `antora-playbook.yaml`.
Handlebars.registerHelper('replaceAll', function(input, from, to) {
const output = input.replaceAll(from, to)
return new Handlebars.SafeString(output)
});
function register (registry, context) {
const { config: { attributes } } = context
function getStringAttribute(key, defaultValue) {
var value = attributes[key]
if (value === undefined || (value = value.trim()).length == 0) {
if (defaultValue === null) {
throw new Error(`blank or missing attribute: \`key\``)
} else {
return defaultValue
}
}
return value
}
function attributeName(key) {
const attributeName = "log4j-docgen-" + key
if (attributeName.match(/.*[^a-z0-9-]+.*/)) {
throw new Error(`Found invalid attribute name: \`${attributeName}\`.
\`node.getDocument().getAttributes()\` lower cases all attribute names and replaces symbols with dashes.
Hence, you should use kebab-case attribute names.`)
}
return attributeName
}
/**
* JSON paths that should be parsed into an array by `fast-xml-parser`
*/
const descriptorXmlArrayJPaths = [
"pluginSet.plugins.plugin",
"pluginSet.plugins.plugin.supertypes.supertype",
"pluginSet.abstractTypes.abstractType",
"pluginSet.scalars.scalar"
]
/**
* XML parser for parsing descriptor XML files
*/
const descriptorXmlParser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "",
isArray: (name, jPath, isLeafNode, isAttribute) => {
return descriptorXmlArrayJPaths.indexOf(jPath) !== -1
}
})
/**
* Collects the list of `.xml`-suffixed file paths by walking the directory pointed by the `log4j-docgen-descriptor-directory` attribute.
*/
function loadDescriptorPaths() {
const directory = getStringAttribute(attributeName("descriptor-directory"), null)
const filePaths = []
fs.readdirSync(directory, {withFileTypes: true, recursive: true}).forEach(entry => {
if (entry.isFile() && !entry.name.startsWith(".") && entry.name.endsWith(".xml")) {
const filePath = path.resolve(entry.parentPath, entry.name)
filePaths.push(filePath)
}
})
filePaths.sort()
return filePaths
}
/**
* Parses the given descriptor XML file.
*/
function loadDescriptor(filePath) {
const xml = fs.readFileSync(filePath, {encoding: "UTF-8"})
const { pluginSet: pluginSet } = descriptorXmlParser.parse(xml)
return pluginSet
}
/**
* Consolidates all scalar, abstract type, and plugin information into a single dictionary keyed by the class name.
*/
function mergeDescriptors(pluginSets, sourcedTypeByClassName) {
pluginSets.forEach(pluginSet => {
["scalar", "abstractType", "plugin"].forEach(singularFieldName => {
const pluralFieldName = singularFieldName + "s"
if (pluralFieldName in pluginSet) {
pluginSet[pluralFieldName][singularFieldName].forEach(type => {
sourcedTypeByClassName[type.className] = {
groupId: pluginSet.groupId,
artifactId: pluginSet.artifactId,
version: pluginSet.version,
type: type
}
})
}
})
})
}
/**
* Enriches the given `sourcedTypeByClassName` with `supertypes` extracted from the given `pluginSets`.
*/
function populateTypeHierarchy(pluginSets, sourcedTypeByClassName) {
pluginSets.forEach(pluginSet => {
if ("plugins" in pluginSet) {
pluginSet["plugins"]["plugin"].forEach(plugin => {
if ("supertypes" in plugin) {
plugin["supertypes"]["supertype"].forEach(superTypeClassName => {
if (!(superTypeClassName in sourcedTypeByClassName)) {
sourcedTypeByClassName[superTypeClassName] = {
groupId: pluginSet.groupId,
artifactId: pluginSet.artifactId,
version: pluginSet.version,
type: {className: superTypeClassName}
}
}
})
}
})
}
})
}
/**
* Removes entries from the given `sourcedTypeByClassName` object whose key matches with the `log4j-docgen-type-filter-exclude-pattern` attribute.
*/
function filterTypes(sourcedTypeByClassName) {
const excludePattern = getStringAttribute(attributeName("type-filter-exclude-pattern"), null)
Object.keys(sourcedTypeByClassName).forEach(className => {
const excluded = className.match(excludePattern)
if (excluded) {
delete sourcedTypeByClassName[className]
}
})
}
function loadDescriptors() {
const filePaths = loadDescriptorPaths()
const pluginSets = filePaths.map(loadDescriptor)
const sourcedTypeByClassName = {}
mergeDescriptors(pluginSets, sourcedTypeByClassName)
populateTypeHierarchy(pluginSets, sourcedTypeByClassName)
filterTypes(sourcedTypeByClassName)
return sourcedTypeByClassName
}
function createInlineApirefMacro({ file }) {
const sourcedTypeByClassName = loadDescriptors()
const typeTargetTemplateSource = getStringAttribute(attributeName("type-target-template"), null)
const typeTargetTemplate = Handlebars.compile(typeTargetTemplateSource)
return function () {
this.process((parent, target, attributes) => {
const methodSplitterIndex = target.indexOf("#")
const methodProvided = methodSplitterIndex > 0
const className = methodProvided ? target.substr(0, methodSplitterIndex) : target
const label = "$positional" in attributes ? attributes.$positional.join(" ") : null
// If the type is provided in descriptors
const sourcedType = sourcedTypeByClassName[className]
if (sourcedType) {
const extendedAttributes = {
type: "xref",
target: typeTargetTemplate({sourcedType: sourcedType}),
attributes
}
const effectiveLabel = label ? label : className.substr(className.lastIndexOf(".") + 1)
return this.createInline(parent, "anchor", effectiveLabel, extendedAttributes)
}
// Otherwise we don't know the link
const text = label ? `<em>${label}</em>` : `<code>${target}</code>`
return this.createInline(parent, "quoted", text, attributes)
})
}
}
registry.inlineMacro('apiref', createInlineApirefMacro(context))
}
module.exports.register = register