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 local-file-path-with-spaces`
- [x] `!include remote-file-url`
- [x] `!include <std/lib/path>`
- [x] `!include file!index`
- [x] `!include file!id`
- [x] `!includeurl url` (compatibility)
- [x] `!includesub id` (compatibility)
- [x] recursive include
- [x] recursive include loop protection
  • Loading branch information
anb0s committed May 23, 2020
1 parent 9585b96 commit 850c9ea
Show file tree
Hide file tree
Showing 16 changed files with 503 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 @@ -44,6 +44,8 @@ const processKroki = (processor, parent, attrs, diagramType, diagramText, contex
}
if (diagramType === 'vegalite') {
diagramText = require('./preprocess.js').preprocessVegaLite(diagramText, context)
} else if (diagramType === 'plantuml') {
diagramText = require('./preprocess').preprocessPlantUML(diagramText, context)
}
const blockId = attrs.id
const title = attrs.title
Expand Down
169 changes: 169 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,173 @@ ${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' || typeof vfs.exists !== 'function') {
vfs = require('./node-fs.js')
}
const includeStack = []
return preprocessPlantUmlIncludes(diagramText, '.', includeStack, vfs)
}

/**
* @param {string} diagramText
* @param {string} dirPath
* @param {string[]} includeStack
* @param {any} vfs
* @returns {string}
*/
function preprocessPlantUmlIncludes (diagramText, dirPath, includeStack, vfs) {
// see: http://plantuml.com/en/preprocessing
const RegExInclude = new RegExp('^\\s*!(include(?:url|sub)?)\\s+((?:(?<=\\\\)[ ]|[^ ])+)(.*)')
const diagramLines = diagramText.split('\n')
const diagramProcessed = diagramLines.map(line => line.replace(
RegExInclude,
(match, ...args) => {
const include = args[0].toLowerCase()
const urlSub = args[1].trim().split('!')
const url = urlSub[0].replace(/\\ /g, ' ')
const sub = urlSub[1]
const result = readPlantUmlInclude(url, dirPath, includeStack, vfs)
if (result.skip) {
return line
} else {
let text = result.text
if (sub !== undefined && sub !== null && sub !== '') {
if (include === 'includesub') {
text = getPlantUmlTextFromSub(text, sub)
} else {
const index = parseInt(sub, 10)
if (isNaN(index)) {
text = getPlantUmlTextFromId(text, sub)
} else {
text = getPlantUmlTextFromIndex(text, index)
}
}
}
includeStack.push(result.filePath)
text = preprocessPlantUmlIncludes(text, path.dirname(result.filePath), includeStack, vfs)
includeStack.pop()
return text
}
})
)
return diagramProcessed.join('\n')
}

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

/**
* @param {string} text
* @param {string} sub
* @returns {string}
*/
function getPlantUmlTextFromSub (text, sub) {
const RegEx = new RegExp(`!startsub\\s+${sub}(?:\\r\\n|\\n)([\\s\\S]*?)(?:\\r\\n|\\n)!endsub`, 'gm')
return getPlantUmlTextRegEx(text, RegEx)
}

/**
* @param {string} text
* @param {string} id
* @returns {string}
*/
function getPlantUmlTextFromId (text, id) {
const RegEx = new RegExp(`@startuml\\(id=${id}\\)(?:\\r\\n|\\n)([\\s\\S]*?)(?:\\r\\n|\\n)@enduml`, 'gm')
return getPlantUmlTextRegEx(text, RegEx)
}

/**
* @param {string} text
* @param {RegExp} RegEx
* @returns {string}
*/
function getPlantUmlTextRegEx (text, RegEx) {
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 {int} index
* @returns {string}
*/
function getPlantUmlTextFromIndex (text, index) {
const RegEx = new RegExp('@startuml(?:\\r\\n|\\n)([\\s\\S]*?)(?:\\r\\n|\\n)@enduml', 'gm')
let idx = -1
let matchedStrings = ''
let match = RegEx.exec(text)
while (match != null && idx < index) {
if (++idx === index) {
matchedStrings += match[1]
} else {
match = RegEx.exec(text)
}
}
return matchedStrings
}

/**
* @param {Error} error
* @param {any} causedBy
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/plantuml/file-include-child.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!include file-include-grand-parent.iuml
1 change: 1 addition & 0 deletions test/fixtures/plantuml/file-include-grand-parent.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!include file-include-parent.iuml
1 change: 1 addition & 0 deletions test/fixtures/plantuml/file-include-itself.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!include file-include-itself.iuml
1 change: 1 addition & 0 deletions test/fixtures/plantuml/file-include-parent.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!include file-include-child.iuml
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
9 changes: 9 additions & 0 deletions test/fixtures/plantuml/file-with-index.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@startuml
A -> A : stuff1
B -> B : stuff2
@enduml

@startuml
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 with spaces.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
3 changes: 3 additions & 0 deletions test/fixtures/plantuml/style with spaces.iuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
!include style\ general\ with\ spaces.iuml
!include style-note.iuml
!include style-sequence.iuml
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

0 comments on commit 850c9ea

Please sign in to comment.