Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Route logs through the Asciidoctor logger #324

Merged
merged 1 commit into from
Feb 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/asciidoctor-kroki.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* global Opal */
// @ts-check
const { KrokiDiagram, KrokiClient } = require('./kroki-client.js')

Expand Down Expand Up @@ -198,6 +199,7 @@ module.exports.register = function register (registry, context = {}) {
if (typeof context.contentCatalog !== 'undefined' && typeof context.contentCatalog.addFile === 'function' && typeof context.file !== 'undefined') {
context.vfs = require('./antora-adapter.js')(context.file, context.contentCatalog, context.vfs)
}
context.logger = Opal.Asciidoctor.LoggerManager.getLogger()
const names = [
'actdiag',
'blockdiag',
Expand Down
35 changes: 19 additions & 16 deletions src/preprocess.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const path = require('path')
* @param {string} diagramDir
* @returns {string}
*/
module.exports.preprocessVegaLite = function (diagramText, context, diagramDir = '') {
module.exports.preprocessVegaLite = function (diagramText, context = {}, diagramDir = '') {
const logger = 'logger' in context && typeof context.logger !== 'undefined' ? context.logger : console
let diagramObject
try {
const JSON5 = require('json5')
Expand All @@ -26,16 +27,15 @@ ${diagramText}
if (!diagramObject || !diagramObject.data || !diagramObject.data.url) {
return diagramText
}
const vfs = context.vfs
const read = typeof vfs !== 'undefined' && typeof vfs.read === 'function' ? vfs.read : require('./node-fs.js').read
const read = 'vfs' in context && typeof context.vfs !== 'undefined' && typeof context.vfs.read === 'function' ? context.vfs.read : require('./node-fs.js').read
const data = diagramObject.data
const urlOrPath = data.url
try {
data.values = read(isLocalAndRelative(urlOrPath) ? path.join(diagramDir, urlOrPath) : urlOrPath)
} catch (e) {
if (isRemoteUrl(urlOrPath)) {
// Only warn and do not throw an error, because the data file can perhaps be found by kroki server (https://github.com/yuzutech/kroki/issues/60)
console.warn(`Skipping preprocessing of Vega-Lite view specification, because reading the remote data file '${urlOrPath}' referenced in the diagram caused an error:\n${e}`)
// Includes a remote file that cannot be found but might be resolved by the Kroki server (https://github.com/yuzutech/kroki/issues/60)
logger.info(`Skipping preprocessing of Vega-Lite view specification, because reading the remote data file '${urlOrPath}' referenced in the diagram caused an error:\n${e}`)
return diagramText
}
const message = `Preprocessing of Vega-Lite view specification failed, because reading the local data file '${urlOrPath}' referenced in the diagram caused an error:\n${e}`
Expand Down Expand Up @@ -88,10 +88,11 @@ function removePlantUmlTags (diagramText) {
* @returns {string}
*/
module.exports.preprocessPlantUML = function (diagramText, context, diagramIncludePaths = '', diagramDir = '') {
const logger = 'logger' in context ? context.logger : console
const includeOnce = []
const includeStack = []
const includePaths = diagramIncludePaths ? diagramIncludePaths.split(path.delimiter) : []
diagramText = preprocessPlantUmlIncludes(diagramText, diagramDir, includeOnce, includeStack, includePaths, context.vfs)
diagramText = preprocessPlantUmlIncludes(diagramText, diagramDir, includeOnce, includeStack, includePaths, context.vfs, logger)
return removePlantUmlTags(diagramText)
}

Expand All @@ -102,9 +103,10 @@ module.exports.preprocessPlantUML = function (diagramText, context, diagramInclu
* @param {string[]} includeStack
* @param {string[]} includePaths
* @param {any} vfs
* @param {any} logger
* @returns {string}
*/
function preprocessPlantUmlIncludes (diagramText, dirPath, includeOnce, includeStack, includePaths, vfs) {
function preprocessPlantUmlIncludes (diagramText, dirPath, includeOnce, includeStack, includePaths, vfs, logger) {
// See: http://plantuml.com/en/preprocessing
// Please note that we cannot use lookbehind for compatibility reasons with Safari: https://caniuse.com/mdn-javascript_builtins_regexp_lookbehind_assertion objects are stateful when they have the global flag set (e.g. /foo/g).
// const regExInclude = /^\s*!(include(?:_many|_once|url|sub)?)\s+((?:(?<=\\)[ ]|[^ ])+)(.*)/
Expand All @@ -124,7 +126,7 @@ function preprocessPlantUmlIncludes (diagramText, dirPath, includeOnce, includeS
const trailingContent = target.comment
const url = urlSub[0].replace(/\\ /g, ' ')
const sub = urlSub[1]
const result = readPlantUmlInclude(url, [dirPath, ...includePaths], includeStack, vfs)
const result = readPlantUmlInclude(url, [dirPath, ...includePaths], includeStack, vfs, logger)
if (result.skip) {
return line
}
Expand All @@ -147,7 +149,7 @@ function preprocessPlantUmlIncludes (diagramText, dirPath, includeOnce, includeS
text = getPlantUmlTextOrFirstBlock(text)
}
includeStack.push(result.filePath)
text = preprocessPlantUmlIncludes(text, path.dirname(result.filePath), includeOnce, includeStack, includePaths, vfs)
text = preprocessPlantUmlIncludes(text, path.dirname(result.filePath), includeOnce, includeStack, includePaths, vfs, logger)
includeStack.pop()
if (trailingContent !== '') {
return text + ' ' + trailingContent
Expand Down Expand Up @@ -207,16 +209,17 @@ function parseTarget (value) {
* @param {string[]} includePaths
* @param {string[]} includeStack
* @param {any} vfs
* @param {any} logger
* @returns {any}
*/
function readPlantUmlInclude (url, includePaths, includeStack, vfs) {
function readPlantUmlInclude (url, includePaths, includeStack, vfs, logger) {
const read = typeof vfs !== 'undefined' && typeof vfs.read === 'function' ? vfs.read : require('./node-fs.js').read
let skip = false
let text = ''
let filePath = url
if (url.startsWith('<')) {
// Only warn and do not throw an error, because the std-lib includes can perhaps be found by Kroki server
console.warn(`Skipping preprocessing of PlantUML standard library include file '${url}'`)
// Includes a standard library that cannot be resolved locally but might be resolved the by Kroki server
logger.info(`Skipping preprocessing of PlantUML standard library include '${url}'`)
skip = true
} else if (includeStack.includes(url)) {
const message = `Preprocessing of PlantUML include failed, because recursive reading already included referenced file '${url}'`
Expand All @@ -226,8 +229,8 @@ function readPlantUmlInclude (url, includePaths, includeStack, vfs) {
try {
text = read(url)
} catch (e) {
// Only warn and do not throw an error, because the data file can perhaps be found by Kroki server (https://github.com/yuzutech/kroki/issues/60)
console.warn(`Skipping preprocessing of PlantUML include, because reading the referenced remote file '${url}' caused an error:\n${e}`)
// Includes a remote file that cannot be found but might be resolved by the Kroki server (https://github.com/yuzutech/kroki/issues/60)
logger.info(`Skipping preprocessing of PlantUML include, because reading the referenced remote file '${url}' caused an error:\n${e}`)
skip = true
}
} else {
Expand All @@ -239,8 +242,8 @@ function readPlantUmlInclude (url, includePaths, includeStack, vfs) {
try {
text = read(filePath)
} catch (e) {
// Only warn and do not throw an error, because the data file can perhaps be found by Kroki server
console.warn(`Skipping preprocessing of PlantUML include, because reading the referenced local file '${filePath}' caused an error:\n${e}`)
// Includes a local file that cannot be found but might be resolved by the Kroki server
logger.info(`Skipping preprocessing of PlantUML include, because reading the referenced local file '${filePath}' caused an error:\n${e}`)
skip = true
}
}
Expand Down
19 changes: 19 additions & 0 deletions test/antora/docs/modules/ROOT/pages/topic/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,22 @@ include::example$ab-all.puml[]
----
include::example$ab.puml[]
----

== C4 Diagram with !include

[c4plantuml]
----
@startuml
!include <C4/C4_Container>

Person(personAlias, "Label", "Optional Description")
Container(containerAlias, "Label", "Technology", "Optional Description")
System(systemAlias, "Label", "Optional Description")

System_Ext(extSystemAlias, "Label", "Optional Description")

Rel(personAlias, containerAlias, "Label", "Optional Technology")

Rel_U(systemAlias, extSystemAlias, "Label", "Optional Technology")
@enduml
----
76 changes: 39 additions & 37 deletions test/preprocess.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,35 +31,24 @@ describe('Vega-Lite preprocessing', () => {
})

/**
* @param {string} diagramText
* @param {string} expectedErrorMessage
* @param {string} [diagramDir]
* @param {string} result
* @param {string} expected
* @returns {void}
*/
function expectToThrow (diagramText, expectedErrorMessage, diagramDir) {
expect(() => preprocessVegaLite(diagramText, {}, diagramDir)).to.throw(expectedErrorMessage)
}

/**
* @param {string} diagramText
* @param {string} expectedPreprocessedDiagramText
* @param {string} [diagramDir]
* @returns {void}
*/
function expectToBeEqual (diagramText, expectedPreprocessedDiagramText, diagramDir) {
expect(preprocessVegaLite(diagramText, {}, diagramDir)).to.be.equal(expectedPreprocessedDiagramText.replace(/\r\n/g, '\n'))
function expectToBeEqualIgnoreNewlines (result, expected) {
expect(result).to.equal(expected.replace(/\r\n/g, '\n'))
}

it('should throw an error for invalid JSON', () => {
expectToThrow('invalid JSON', `Preprocessing of Vega-Lite view specification failed, because of a parsing error:
expect(() => preprocessVegaLite('invalid JSON')).to.throw(`Preprocessing of Vega-Lite view specification failed, because of a parsing error:
SyntaxError: JSON5: invalid character 'i' at 1:1
The invalid view specification was:
invalid JSON`)
})

it('should return original diagramText for valid JSON but without "data.url"', () => {
const validJsonWithoutDataUrl = '{}'
expectToBeEqual(validJsonWithoutDataUrl, validJsonWithoutDataUrl)
expect(preprocessVegaLite(validJsonWithoutDataUrl)).to.equal(validJsonWithoutDataUrl)
})

it('should throw an error for unexisting local file referenced with relative path', () => {
Expand All @@ -70,7 +59,7 @@ invalid JSON`)
}`
const errorMessage = `Preprocessing of Vega-Lite view specification failed, because reading the local data file 'unexisting.csv' referenced in the diagram caused an error:
Error: ENOENT: no such file or directory, open 'unexisting.csv'`
expectToThrow(diagramText, errorMessage)
expect(() => preprocessVegaLite(diagramText)).to.throw(errorMessage)
})

it('should throw an error for unexisting file referenced with "file" protocol', () => {
Expand All @@ -81,18 +70,21 @@ Error: ENOENT: no such file or directory, open 'unexisting.csv'`
}
}`
const unexistingPath = url.fileURLToPath(unexistingFileUrl)
const errorMessage = `Preprocessing of Vega-Lite view specification failed, because reading the local data file '${unexistingFileUrl}' referenced in the diagram caused an error:
Error: ENOENT: no such file or directory, open '${unexistingPath}'`
expectToThrow(diagramText, errorMessage)
expect(() => preprocessVegaLite(diagramText)).to.throw(`Preprocessing of Vega-Lite view specification failed, because reading the local data file '${unexistingFileUrl}' referenced in the diagram caused an error:
Error: ENOENT: no such file or directory, open '${unexistingPath}'`)
})

it('should warn and return original diagramText for unexisting remote file referenced with "http" protocol, because it can perhaps be found by kroki server', () => {
it('should log and return original diagramText for unexisting remote file referenced with "http" protocol, because it can perhaps be found by kroki server', () => {
const memoryLogger = asciidoctor.MemoryLogger.create()
const diagramText = `{
"data": {
"url": "https://raw.githubusercontent.com/Mogztter/asciidoctor-kroki/master/unexisting.csv"
}
}`
expectToBeEqual(diagramText, diagramText)
expectToBeEqualIgnoreNewlines(preprocessVegaLite(diagramText, { logger: memoryLogger }), diagramText)
const logs = memoryLogger.getMessages()
expect(logs.length).to.equal(1)
expect(logs[0].message).to.includes('Skipping preprocessing of Vega-Lite view specification, because reading the remote data file \'https://raw.githubusercontent.com/Mogztter/asciidoctor-kroki/master/unexisting.csv\' referenced in the diagram caused an error:')
})

it('should return diagramText with inlined local file referenced with relative path', () => {
Expand All @@ -101,7 +93,7 @@ Error: ENOENT: no such file or directory, open '${unexistingPath}'`
"url": "${relativePath}"
}
}`
expectToBeEqual(diagramText, diagramTextWithInlinedCsvFile)
expectToBeEqualIgnoreNewlines(preprocessVegaLite(diagramText), diagramTextWithInlinedCsvFile)
})

it('should return diagramText with inlined local file referenced with relative path and base dir', () => {
Expand All @@ -110,7 +102,7 @@ Error: ENOENT: no such file or directory, open '${unexistingPath}'`
"url": "vegalite-data.csv"
}
}`
expectToBeEqual(diagramText, diagramTextWithInlinedCsvFile, 'test/fixtures/')
expectToBeEqualIgnoreNewlines(preprocessVegaLite(diagramText, {}, 'test/fixtures/'), diagramTextWithInlinedCsvFile)
})

it('should return diagramText with inlined local file referenced with absolute path', () => {
Expand All @@ -119,7 +111,7 @@ Error: ENOENT: no such file or directory, open '${unexistingPath}'`
"url": "${cwd}/${relativePath}"
}
}`
expectToBeEqual(diagramText, diagramTextWithInlinedCsvFile)
expectToBeEqualIgnoreNewlines(preprocessVegaLite(diagramText), diagramTextWithInlinedCsvFile)
})

it('should return diagramText with inlined local file referenced with absolute path and base dir (which should not be used)', () => {
Expand All @@ -128,7 +120,7 @@ Error: ENOENT: no such file or directory, open '${unexistingPath}'`
"url": "${cwd}/${relativePath}"
}
}`
expectToBeEqual(diagramText, diagramTextWithInlinedCsvFile, 'test/fixtures/')
expectToBeEqualIgnoreNewlines(preprocessVegaLite(diagramText, {}, 'test/fixtures/'), diagramTextWithInlinedCsvFile)
})

it('should return diagramText with inlined local file referenced with "file" protocol and absolute path', () => {
Expand All @@ -138,7 +130,7 @@ Error: ENOENT: no such file or directory, open '${unexistingPath}'`
"url": "${fileUrl}"
}
}`
expectToBeEqual(diagramText, diagramTextWithInlinedCsvFile)
expectToBeEqualIgnoreNewlines(preprocessVegaLite(diagramText), diagramTextWithInlinedCsvFile)
})

it('should return diagramText with inlined remote file referenced with "http" protocol', () => {
Expand All @@ -147,9 +139,7 @@ Error: ENOENT: no such file or directory, open '${unexistingPath}'`
"url": "https://raw.githubusercontent.com/Mogztter/asciidoctor-kroki/master/${relativePath}"
}
}`
// replace escaped CR/LF (which happens when CSV file was checked-out in Windows) with escaped LF
const expected = diagramTextWithInlinedCsvFile.replace(/\\r\\n/g, '\\n')
expectToBeEqual(diagramText, expected)
expectToBeEqualIgnoreNewlines(preprocessVegaLite(diagramText), diagramTextWithInlinedCsvFile)
})
})

Expand All @@ -166,26 +156,38 @@ describe('PlantUML preprocessing', () => {
expect(preprocessPlantUML(diagramTextWithoutInclude, {})).to.be.equal(diagramTextWithoutInclude)
})

it('should warn and return original diagramText for standard library file referenced with "!include <std-lib-file>", because it can perhaps be found by kroki server', () => {
it('should log and return original diagramText for standard library file referenced with "!include <std-lib-file>", because it can perhaps be found by kroki server', () => {
const memoryLogger = asciidoctor.MemoryLogger.create()
const diagramTextWithStdLibIncludeFile = `
!include <std/include.iuml>
alice -> bob`
expect(preprocessPlantUML(diagramTextWithStdLibIncludeFile, {})).to.be.equal(diagramTextWithStdLibIncludeFile)
expect(preprocessPlantUML(diagramTextWithStdLibIncludeFile, { logger: memoryLogger })).to.be.equal(diagramTextWithStdLibIncludeFile)
const logs = memoryLogger.getMessages()
expect(logs.length).to.equal(1)
expect(logs[0].message).to.equal('Skipping preprocessing of PlantUML standard library include \'<std/include.iuml>\'')
})

it('should warn and return original diagramText for unexisting local file referenced with "!include local-file-or-url", because it can perhaps be found by kroki server', () => {
it('should log and return original diagramText for unexisting local file referenced with "!include local-file-or-url", because it can perhaps be found by kroki server', () => {
const memoryLogger = asciidoctor.MemoryLogger.create()
const diagramTextWithUnexistingLocalIncludeFile = `
!include ${localUnexistingFilePath}
alice -> bob`
expect(preprocessPlantUML(diagramTextWithUnexistingLocalIncludeFile, {})).to.be.equal(diagramTextWithUnexistingLocalIncludeFile)
expect(preprocessPlantUML(diagramTextWithUnexistingLocalIncludeFile, { logger: memoryLogger })).to.be.equal(diagramTextWithUnexistingLocalIncludeFile)
const logs = memoryLogger.getMessages()
expect(logs.length).to.equal(1)
expect(logs[0].message).to.includes(`Skipping preprocessing of PlantUML include, because reading the referenced local file '${localUnexistingFilePath}' caused an error:`)
})

it('should warn and return original diagramText for unexisting remote file referenced with "!include remote-url", because it can perhaps be found by kroki server', () => {
it('should log and return original diagramText for unexisting remote file referenced with "!include remote-url", because it can perhaps be found by kroki server', () => {
const memoryLogger = asciidoctor.MemoryLogger.create()
const remoteUnexistingIncludeFilePath = `${remoteBasePath}${localUnexistingFilePath}`
const diagramTextWithUnexistingRemoteIncludeFile = `
!include ${remoteUnexistingIncludeFilePath}
alice -> bob`
expect(preprocessPlantUML(diagramTextWithUnexistingRemoteIncludeFile, {})).to.be.equal(diagramTextWithUnexistingRemoteIncludeFile)
expect(preprocessPlantUML(diagramTextWithUnexistingRemoteIncludeFile, { logger: memoryLogger })).to.be.equal(diagramTextWithUnexistingRemoteIncludeFile)
const logs = memoryLogger.getMessages()
expect(logs.length).to.equal(1)
expect(logs[0].message).to.includes(`Skipping preprocessing of PlantUML include, because reading the referenced remote file '${remoteUnexistingIncludeFilePath}' caused an error:`)
})

it('should return diagramText with inlined local file referenced with "!include local-file-or-url"', () => {
Expand Down