Skip to content

Commit

Permalink
add preprocess support for PlantUML "!include"
Browse files Browse the repository at this point in the history
Fixes #49

see: http://plantuml.com/en/preprocessing

implement and test:

- [x] `!include local-file-path`
- [x] `!include remote-file-url`
- [x] `!include <std/lib/path>`
- [ ] `!include file!index`
- [x] `!include file!id`
- [x] `!includeurl url` (compatibility)
- [x] `!includesub id` (compatibility)
- [x] recursive include
- [ ] recursive include loop protection
  • Loading branch information
anb0s committed May 22, 2020
1 parent 1275cfa commit e0337a1
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/asciidoctor-kroki.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const processKroki = (processor, parent, attrs, diagramType, diagramText, contex
}
if (diagramType === 'vegalite') {
diagramText = require('./preprocess').preprocessVegaLite(diagramText, context)
} else if (diagramType === 'plantuml') {
diagramText = require('./preprocess').preprocessPlantUML(diagramText, context)
}
const blockId = attrs.id
const title = attrs.title
Expand Down
135 changes: 135 additions & 0 deletions src/preprocess.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const path = require('path')

// @ts-check
/**
* @param {string} diagramText
Expand Down Expand Up @@ -56,6 +58,139 @@ ${diagramText}
return JSON.stringify(diagramObject)
}

// @ts-check
/**
* @param {string} diagramText
* @param {any} context
* @returns {string}
*/
module.exports.preprocessPlantUML = function (diagramText, context) {
let vfs = context.vfs
if (typeof vfs === 'undefined' || typeof vfs.read !== 'function') {
vfs = require('./node-fs')
}
return preprocessPlantUmlIncludes(diagramText, '.', vfs)
}

/**
* @param {string} diagramText
* @param {string} dirname
* @param {any} vfs
* @returns {string}
*/
function preprocessPlantUmlIncludes (diagramText, dirname, vfs) {
// see: http://plantuml.com/en/preprocessing
// ^\\s*!include(?:url|sub)?\\s+(?<path>(?:(?<=\\\\)[ ]|[^ ])+)(.*)
const RegExInclude = /^\s*!(include(?:url|sub)?)\s+(.+?)(?:!(\w+))?$/i
// if (!RegExInclude.test(diagramText)) {
// return diagramText
// }
const diagramLines = diagramText.split('\n')
const diagramProcessed = diagramLines.map(line => line.replace(
RegExInclude,
(match, ...args) => {
const include = args[0].toLowerCase()
const url = args[1].trim()
const sub = args[2]
const result = readPlantUmlInclude(url, dirname, vfs)
// console.warn(`include= '${include}'`)
// console.warn(`url= '${url}'`)
// console.warn(`sub= '${sub}'`)
// console.warn(`result= '${result}'`)
if (result.skip) {
return line
} else {
let text = result.text
if (sub !== undefined && sub !== null && sub !== '') {
if (include === 'includesub') {
text = getPlantUmlIncludeSub(result.text, sub)
} else {
text = getPlantUmlIncludeId(result.text, sub)
}
}
return preprocessPlantUmlIncludes(text, path.dirname(url), vfs)
}
})
)
return diagramProcessed.join('\n')
}

/**
* @param {string} url
* @param {string} dirname
* @param {any} vfs
* @returns {any}
*/
function readPlantUmlInclude (url, dirname, vfs) {
let skip = false
let text
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}'`)
skip = true
} else {
try {
text = vfs.read(url)
} catch (e) {
if (isRemoteUrl(url)) {
// 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}`)
skip = true
} else {
try {
text = vfs.read(path.join(dirname, url))
} catch (e2) {
const message = `Preprocessing of PlantUML include failed, because reading the referenced local file '${url}' caused an error:\n${e}`
throw addCauseToError(new Error(message), e)
}
}
}
}
return { skip: skip, text: text }
}

/**
* @param {string} text
* @param {string} sub
* @returns {string}
*/
function getPlantUmlIncludeSub (text, sub) {
const RegEx = new RegExp(`!startsub\\s+${sub}(?:\\r\\n|\\n)([\\s\\S]*?)(?:\\r\\n|\\n)!endsub`, 'gm')
console.warn(`text='${text}'\nsub='${sub}'\n`)
let matchedStrings = ''
let match = RegEx.exec(text)
if (match != null) {
matchedStrings += match[1]
match = RegEx.exec(text)
while (match != null) {
matchedStrings += '\n' + match[1]
match = RegEx.exec(text)
}
}
return matchedStrings
}

/**
* @param {string} text
* @param {string} id
* @returns {string}
*/
function getPlantUmlIncludeId (text, id) {
const RegEx = new RegExp(`@startuml\\(id=${id}\\)(?:\\r\\n|\\n)([\\s\\S]*?)(?:\\r\\n|\\n)@enduml`, 'gm')
console.warn(`text='${text}'\nid='${id}'\n`)
let matchedStrings = ''
let match = RegEx.exec(text)
if (match != null) {
matchedStrings += match[1]
match = RegEx.exec(text)
while (match != null) {
matchedStrings += '\n' + match[1]
match = RegEx.exec(text)
}
}
return matchedStrings
}

/**
* @param {Error} error
* @param {any} causedBy
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/plantuml/file-with-id.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@startuml(id=MY_OWN_ID1)
A -> A : stuff1
B -> B : stuff2
@enduml

@startuml(id=MY_OWN_ID2)
C -> C : stuff3
D -> D : stuff4
@enduml
12 changes: 12 additions & 0 deletions test/fixtures/plantuml/file-with-subs.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@startuml
A -> A : stuff1
!startsub BASIC
B -> B : stuff2
B -> B : stuff2.1
!endsub
C -> C : stuff3
!startsub BASIC
D -> D : stuff4
D -> D : stuff4.1
!endsub
@enduml
4 changes: 4 additions & 0 deletions test/fixtures/plantuml/style-general.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
skinparam DefaultFontName "Neucha"
skinparam BackgroundColor transparent
skinparam Shadowing false
skinparam Handwritten true
5 changes: 5 additions & 0 deletions test/fixtures/plantuml/style-note.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
skinparam Note {
BorderColor #303030
BackgroundColor #CEEEFE
FontSize 12
}
13 changes: 13 additions & 0 deletions test/fixtures/plantuml/style-sequence.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
skinparam Sequence {
TitleFontSize 12
TitleFontColor #606060
ArrowColor #303030
DividerBackgroundColor #EEEEEE
GroupBackgroundColor #EEEEEE
LifeLineBackgroundColor white
LifeLineBorderColor #303030
ParticipantBackgroundColor #FEFEFE
ParticipantBorderColor #303030
BoxLineColor #303030
BoxBackgroundColor #DDDDDD
}
3 changes: 3 additions & 0 deletions test/fixtures/plantuml/style.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
!include style-general.iuml
!include style-note.iuml
!include style-sequence.iuml
178 changes: 178 additions & 0 deletions test/preprocess.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,181 @@ Error: ENOENT: no such file or directory, open 'unexisting.csv'`
expect(preprocessVegaLite(referencedRemoteCsvFile, {})).to.be.equal(inlinedRemoteCsvFile)
})
})

const { preprocessPlantUML } = require('../src/preprocess.js')

describe('preprocessPlantUML', () => {
// TODO: change after merge to upstream project
const remoteBasePath = 'https://raw.githubusercontent.com/anb0s/asciidoctor-kroki/plantuml-include/'
// const remoteBasePath = 'https://raw.githubusercontent.com/Mogztter/asciidoctor-kroki/master/'
const localUnexistingFilePath = 'test/fixtures/plantuml/unexisting.iuml'
const localExistingFilePath = 'test/fixtures/plantuml/style-general.iuml'

it('should return original diagramText without "!include ..."', () => {
const diagramTextWithoutInclude = `
@startuml
alice -> bob
@enduml`
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', () => {
const diagramTextWithStdLibIncludeFile = `
@startuml
!include <std/include.iuml>
alice -> bob
@enduml`
expect(preprocessPlantUML(diagramTextWithStdLibIncludeFile, {})).to.be.equal(diagramTextWithStdLibIncludeFile)
})

it('should throw an error for unexisting local file referenced with "!include local-file-or-url"', () => {
const diagramTextWithUnexistingLoicalIncludeFile = `
@startuml
!include ${localUnexistingFilePath}
alice -> bob
@enduml`
const errorMessage = `Preprocessing of PlantUML include failed, because reading the referenced local file '${localUnexistingFilePath}' caused an error:
Error: ENOENT: no such file or directory, open '${localUnexistingFilePath}'`
expect(() => preprocessPlantUML(diagramTextWithUnexistingLoicalIncludeFile, {})).to.throw(errorMessage)
})

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', () => {
const remoteUnexistingIncludeFilePath = `${remoteBasePath}${localUnexistingFilePath}`
const diagramTextWithUnexistingRemoteIncludeFile = `
@startuml
!include ${remoteUnexistingIncludeFilePath}
alice -> bob
@enduml`
expect(preprocessPlantUML(diagramTextWithUnexistingRemoteIncludeFile, {})).to.be.equal(diagramTextWithUnexistingRemoteIncludeFile)
})

it('should return diagramText with inlined local file referenced with "!include local-file-or-url"', () => {
const diagramTextWithExistingLocalIncludeFile = `
@startuml
!include ${localExistingFilePath}
alice -> bob
@enduml`
const includedText = fs.readFileSync(`${localExistingFilePath}`, 'utf8')
const diagramTextWithIncludedText = `
@startuml
${includedText}
alice -> bob
@enduml`
expect(preprocessPlantUML(diagramTextWithExistingLocalIncludeFile, {})).to.be.equal(diagramTextWithIncludedText)
})

it('should return diagramText with inlined remote file referenced with "!include remote-url"', () => {
const remoteIncludeFilePath = `${remoteBasePath}${localExistingFilePath}`
const diagramTextWithExistingRemoteIncludeFile = `
@startuml
!include ${remoteIncludeFilePath}
alice -> bob
@enduml`.replace(/\r\n/g, '\n')
const includedText = fs.readFileSync(`${localExistingFilePath}`, 'utf8').replace(/\r\n/g, '\n')
const diagramTextWithIncludedText = `
@startuml
${includedText}
alice -> bob
@enduml`
expect(preprocessPlantUML(diagramTextWithExistingRemoteIncludeFile, {})).to.be.equal(diagramTextWithIncludedText)
})

it('should return diagramText with inlined remote file referenced with "!includeurl remote-url"', () => {
const remoteIncludeFilePath = `${remoteBasePath}${localExistingFilePath}`
const diagramTextWithExistingRemoteIncludeFile = `
@startuml
!includeurl ${remoteIncludeFilePath}
alice -> bob
@enduml`.replace(/\r\n/g, '\n')
const includedText = fs.readFileSync(`${localExistingFilePath}`, 'utf8').replace(/\r\n/g, '\n')
const diagramTextWithIncludedText = `
@startuml
${includedText}
alice -> bob
@enduml`
expect(preprocessPlantUML(diagramTextWithExistingRemoteIncludeFile, {})).to.be.equal(diagramTextWithIncludedText)
})

it('should return diagramText with inlined multiple local files referenced with "!include local-file-or-url"', () => {
const localExistingFilePath1 = 'test/fixtures/plantuml/style-note.iuml'
const localExistingFilePath2 = 'test/fixtures/plantuml/style-sequence.iuml'
const diagramTextWithExistingLocalIncludeFiles = `
@startuml
!include ${localExistingFilePath}
!include ${localExistingFilePath1}
!include ${localExistingFilePath2}
alice -> bob
@enduml`
const includedText = fs.readFileSync(`${localExistingFilePath}`, 'utf8')
const includedText1 = fs.readFileSync(`${localExistingFilePath1}`, 'utf8')
const includedText2 = fs.readFileSync(`${localExistingFilePath2}`, 'utf8')
const diagramTextWithIncludedText = `
@startuml
${includedText}
${includedText1}
${includedText2}
alice -> bob
@enduml`
expect(preprocessPlantUML(diagramTextWithExistingLocalIncludeFiles, {})).to.be.equal(diagramTextWithIncludedText)
})

it('should return diagramText with inlined recursive local files referenced with "!include local-file-or-url"', () => {
const localExistingFilePath0 = 'test/fixtures/plantuml/style.iuml'
const localExistingFilePath1 = 'test/fixtures/plantuml/style-note.iuml'
const localExistingFilePath2 = 'test/fixtures/plantuml/style-sequence.iuml'
const diagramTextWithExistingRecursiveLocalIncludeFile = `
@startuml
!include ${localExistingFilePath0}
alice -> bob
@enduml`
const includedText = fs.readFileSync(`${localExistingFilePath}`, 'utf8')
const includedText1 = fs.readFileSync(`${localExistingFilePath1}`, 'utf8')
const includedText2 = fs.readFileSync(`${localExistingFilePath2}`, 'utf8')
const diagramTextWithIncludedText = `
@startuml
${includedText}
${includedText1}
${includedText2}
alice -> bob
@enduml`
expect(preprocessPlantUML(diagramTextWithExistingRecursiveLocalIncludeFile, {})).to.be.equal(diagramTextWithIncludedText)
})

it('should return diagramText with inlined local file referenced with "!includesub local-file!sub-name"', () => {
const localExistingFilePathWithSubs = 'test/fixtures/plantuml/file-with-subs.puml!BASIC'
const diagramTextWithExistingIncludeFileWithSubs = `
@startuml
!includesub ${localExistingFilePathWithSubs}
alice -> bob
@enduml`
const diagramTextWithIncludedText = `
@startuml
B -> B : stuff2
B -> B : stuff2.1
D -> D : stuff4
D -> D : stuff4.1
alice -> bob
@enduml`
expect(preprocessPlantUML(diagramTextWithExistingIncludeFileWithSubs, {}).replace(/\r\n/g, '\n')).to.be.equal(diagramTextWithIncludedText)
})

it('should return diagramText with inlined local file referenced with "!include local-file!id"', () => {
const localExistingFilePathWithID1 = 'test/fixtures/plantuml/file-with-id.puml!MY_OWN_ID1'
const localExistingFilePathWithID2 = 'test/fixtures/plantuml/file-with-id.puml!MY_OWN_ID2'
const diagramTextWithExistingIncludeFileWithID = `
@startuml
!include ${localExistingFilePathWithID1}
!include ${localExistingFilePathWithID2}
alice -> bob
@enduml`
const diagramTextWithIncludedText = `
@startuml
A -> A : stuff1
B -> B : stuff2
C -> C : stuff3
D -> D : stuff4
alice -> bob
@enduml`
expect(preprocessPlantUML(diagramTextWithExistingIncludeFileWithID, {}).replace(/\r\n/g, '\n')).to.be.equal(diagramTextWithIncludedText)
})
})

0 comments on commit e0337a1

Please sign in to comment.