| /* |
| * 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 |