@antora/asciidoc-loader
Version:
Loads AsciiDoc content into an Asciidoctor Document object (AST) for use in an Antora documentation pipeline.
263 lines (247 loc) • 8.73 kB
JavaScript
const Opal = global.Opal
const { $Antora } = require('../constants')
const DBL_COLON = '::'
const DBL_SQUARE = '[]'
const NEWLINE_RX = /\r\n?|\n/
const TAG_DIRECTIVE_RX = /\b(?:tag|(e)nd)::(\S+?)\[\](?=$|[ \r])/m
const IncludeProcessor = (() => {
const $callback = Symbol('callback')
const classDef = Opal.klass(
Opal.Antora || Opal.module(null, 'Antora', $Antora),
Opal.Asciidoctor.Extensions.IncludeProcessor,
'IncludeProcessor'
)
Opal.defn(classDef, '$initialize', function initialize (callback) {
Opal.send(this, Opal.find_super_dispatcher(this, 'initialize', initialize))
this[$callback] = callback
})
Opal.defn(classDef, '$process', function (doc, reader, target, attrs) {
if (reader.maxdepth === Opal.nil) return
const sourceCursor = reader.$cursor_at_prev_line()
if (reader.$include_depth() >= Opal.hash_get(reader.maxdepth, 'curr')) {
log('error', `maximum include depth of ${Opal.hash_get(reader.maxdepth, 'rel')} exceeded`, reader, sourceCursor)
return
}
const resolvedFile = this[$callback](doc, target, sourceCursor)
if (resolvedFile) {
let includeContents
let linenums
let tags
let startLineNum
if ((linenums = getLines(attrs))) {
;[includeContents, startLineNum] = filterLinesByLineNumbers(reader, target, resolvedFile, linenums)
} else if ((tags = getTags(attrs))) {
;[includeContents, startLineNum] = filterLinesByTags(reader, target, resolvedFile, tags, sourceCursor)
} else {
includeContents = resolvedFile.contents
startLineNum = 1
}
Opal.hash_put(attrs, 'partial-option', '')
const file = Object.assign(new String(resolvedFile.file), {
src: resolvedFile.src,
parent: { file: reader.file, lineno: reader.lineno - 1 },
})
reader.pushInclude(includeContents, file, resolvedFile.path, startLineNum, attrs)
} else {
if (attrs['$key?']('optional-option')) {
log('info', `optional include dropped because include file not found: ${target}`, reader, sourceCursor)
} else {
log('error', `target of include not found: ${target}`, reader, sourceCursor)
reader.$unshift(`Unresolved include directive in ${sourceCursor.file} - include::${target}[]`)
}
}
})
return classDef
})()
function getLines (attrs) {
if (attrs['$key?']('lines')) {
const lines = attrs['$[]']('lines')
if (lines) {
const linenums = []
let filtered
;(~lines.indexOf(',') ? lines.split(',') : lines.split(';'))
.filter((it) => it)
.forEach((linedef) => {
filtered = true
let delim
let from
if (~(delim = linedef.indexOf('..'))) {
from = linedef.substr(0, delim)
let to = linedef.substr(delim + 2)
if ((to = parseInt(to, 10) || -1) > 0) {
if ((from = parseInt(from, 10) || -1) > 0) {
for (let i = from; i <= to; i++) linenums.push(i)
}
} else if (to === -1 && (from = parseInt(from, 10) || -1) > 0) {
linenums.push(from, Infinity)
}
} else if ((from = parseInt(linedef, 10) || -1) > 0) {
linenums.push(from)
}
})
if (linenums.length) return [...new Set(linenums.sort((a, b) => a - b))]
if (filtered) return []
}
}
}
function getTags (attrs) {
if (attrs['$key?']('tag')) {
const tag = attrs['$[]']('tag')
if (tag && tag !== '!') {
return tag.charAt() === '!' ? new Map().set(tag.substr(1), false) : new Map().set(tag, true)
}
} else if (attrs['$key?']('tags')) {
const tags = attrs['$[]']('tags')
if (tags) {
const result = new Map()
let any = false
tags.split(~tags.indexOf(',') ? ',' : ';').forEach((tag) => {
if (tag && tag !== '!') {
any = true
tag.charAt() === '!' ? result.set(tag.substr(1), false) : result.set(tag, true)
}
})
if (any) return result
}
}
}
function filterLinesByLineNumbers (reader, target, file, linenums) {
let lineNum = 0
let startLineNum
let selectRest
const lines = []
file.contents.split(NEWLINE_RX).some((line) => {
lineNum++
if (selectRest || (selectRest = linenums[0] === Infinity)) {
if (!startLineNum) startLineNum = lineNum
lines.push(line)
} else {
if (linenums[0] === lineNum) {
if (!startLineNum) startLineNum = lineNum
linenums.shift()
lines.push(line)
}
if (!linenums.length) return true
}
})
return [lines, startLineNum || 1]
}
function filterLinesByTags (reader, target, file, tags, sourceCursor) {
let selectingDefault, selecting, wildcard
const globstar = tags.get('**')
const star = tags.get('*')
if (globstar === undefined) {
if (star === undefined) {
selectingDefault = selecting = !mapContainsValue(tags, true)
} else {
if ((wildcard = star) || tags.keys().next().value !== '*') {
selectingDefault = selecting = false
} else {
selectingDefault = selecting = !wildcard
}
tags.delete('*')
}
} else {
tags.delete('**')
selectingDefault = selecting = globstar
if (star === undefined) {
if (!globstar && tags.values().next().value === false) wildcard = true
} else {
tags.delete('*')
wildcard = star
}
}
const lines = []
const tagStack = []
const tagsSelected = []
let activeTag
let lineNum = 0
let startLineNum
file.contents.split(NEWLINE_RX).forEach((line) => {
lineNum++
let m
if (~line.indexOf(DBL_COLON) && ~line.indexOf(DBL_SQUARE) && (m = line.match(TAG_DIRECTIVE_RX))) {
const thisTag = m[2]
if (m[1]) {
if (thisTag === activeTag) {
tagStack.shift()
;[activeTag, selecting] = tagStack.length ? tagStack[0] : [undefined, selectingDefault]
} else if (tags.has(thisTag)) {
const idx = tagStack.findIndex(([name]) => name === thisTag)
if (~idx) {
tagStack.splice(idx, 1)
log(
'warn',
`mismatched end tag (expected '${activeTag}' but found '${thisTag}') ` +
`at line ${lineNum} of include file: ${file.file})`,
reader,
sourceCursor,
createIncludeCursor(reader, file, target, lineNum)
)
} else {
log(
'warn',
`unexpected end tag '${thisTag}' at line ${lineNum} of include file: ${file.file}`,
reader,
sourceCursor,
createIncludeCursor(reader, file, target, lineNum)
)
}
}
} else if (tags.has(thisTag)) {
if ((selecting = tags.get(thisTag))) tagsSelected.push(thisTag)
tagStack.unshift([(activeTag = thisTag), selecting, lineNum])
} else if (wildcard !== undefined) {
selecting = activeTag && !selecting ? false : wildcard
tagStack.unshift([(activeTag = thisTag), selecting, lineNum])
}
} else if (selecting) {
if (!startLineNum) startLineNum = lineNum
lines.push(line)
}
})
if (tagStack.length) {
tagStack.forEach(([tagName, _, tagLineNum]) =>
log(
'warn',
`detected unclosed tag '${tagName}' starting at line ${tagLineNum} of include file: ${file.file}`,
reader,
sourceCursor,
createIncludeCursor(reader, file, target, tagLineNum)
)
)
}
if (tagsSelected.length) tagsSelected.forEach((name) => tags.delete(name))
const missingTags = []
tags.forEach((select, name) => select && missingTags.push(name))
if (missingTags.length) {
log(
'warn',
`tag${missingTags.length > 1 ? 's' : ''} '${missingTags.join(', ')}' not found in include file: ${file.file}`,
reader,
sourceCursor,
createIncludeCursor(reader, file, target, 0)
)
}
return [lines, startLineNum || 1]
}
function createIncludeCursor (reader, { file, src }, path, lineno) {
return reader.$create_include_cursor(
Object.assign(new String(file), { src, parent: { file: reader.file, lineno: reader.lineno - 1 } }),
path,
lineno
)
}
function log (severity, message, reader, sourceCursor, includeCursor = undefined) {
const opts = includeCursor
? { source_location: sourceCursor, include_location: includeCursor }
: { source_location: sourceCursor }
reader.$logger()['$' + severity](reader.$message_with_context(message, Opal.hash(opts)))
}
function mapContainsValue (map, value) {
for (const v of map.values()) {
if (v === value) return true
}
}
module.exports = IncludeProcessor