meta-engine
Version:
A language agnostic directive system and preprocessor.
456 lines (315 loc) • 15.9 kB
text/coffeescript
path = require 'path'
class MetaEngine
optionMap: null
contentProvider: null
=
encoding: 'utf8'
prefix: '@'
postfix: ''
indentCharacter: ' '
linebreakCharacter: '\n'
contentProvider: null
insertComments: false
commentPrefix: '<!-- '
commentPostfix: ' -->'
presetRegions: {}
constructor: (optionMap = {})->
= optionMap = optionMap
if optionMap.contentProvider
= optionMap.contentProvider
setContentProvider: (contentProvider)->
= contentProvider
__validateContentProvider: (contentProvider)->
if not ('getContent' of contentProvider) or not (typeof contentProvider.getContent is 'function')
throw new Error 'contentProvider does not implement getContent'
if not ('setContent' of contentProvider) or not (typeof contentProvider.setContent is 'function')
throw new Error 'contentProvider does not implement setContent'
if not ('getContentSync' of contentProvider) or not (typeof contentProvider.getContentSync is 'function')
throw new Error 'contentProvider does not implement getContentSync'
if not ('setContentSync' of contentProvider) or not (typeof contentProvider.setContentSync is 'function')
throw new Error 'contentProvider does not implement setContentSync'
return contentProvider
__processOptionMap: (optionMap)->
if not optionMap or not (typeof optionMap is 'object')
optionMap = {}
if not ('encoding' of optionMap) or not (typeof optionMap.encoding is 'string')
optionMap.encoding = MetaEngine.defaults.encoding
if not ('prefix' of optionMap) or not (typeof optionMap.prefix is 'string')
optionMap.prefix = MetaEngine.defaults.prefix
if not ('postfix' of optionMap) or not (typeof optionMap.postfix is 'string')
optionMap.postfix = MetaEngine.defaults.postfix
if not ('indentCharacter' of optionMap) or not (typeof optionMap.indentCharacter is 'string')
optionMap.indentCharacter = MetaEngine.defaults.indentCharacter
if not ('linebreakCharacter' of optionMap) or not (typeof optionMap.linebreakCharacter is 'string')
optionMap.linebreakCharacter = MetaEngine.defaults.linebreakCharacter
if not ('contentProvider' of optionMap) or not (typeof optionMap.contentProvider is 'object')
optionMap.contentProvider = MetaEngine.defaults.contentProvider
if not ('insertComments' of optionMap) or not (typeof optionMap.insertComments is 'boolean')
optionMap.insertComments = MetaEngine.defaults.insertComments
if not ('commentPrefix' of optionMap) or not (typeof optionMap.commentPrefix is 'string')
optionMap.commentPrefix = MetaEngine.defaults.commentPrefix
if not ('commentPostfix' of optionMap) or not (typeof optionMap.commentPostfix is 'string')
optionMap.commentPostfix = MetaEngine.defaults.commentPostfix
if not ('presetRegions' of optionMap) or not (typeof optionMap.presetRegions is 'object')
optionMap.presetRegions = MetaEngine.defaults.presetRegions
return optionMap
__ensureContentProvider: ->
unless
throw new Error "contentProvider is not assigned"
# ---------------------------------------------------------- Actual Processing
__extractCommonOptions: (tagString, otherParameterList, resourcePath, content, tagStartIndex, linebreakCharacter, indentCharacter)->
# [ tagLineStartIndex, indentLevel ]
if tagStartIndex is 0
indentLevel = 0
tagLineStartIndex = 0
else
caseContent = content.slice 0, tagStartIndex
lastLineEnd = caseContent.lastIndexOf linebreakCharacter
tagLineStartIndex = lastLineEnd + 1 # NOTE: Also covers the scenario when lastLineEnd is -1
indentationString = content.slice tagLineStartIndex, tagStartIndex
indentLevel = 0
while (indentationString.indexOf indentCharacter, (indentCharacter.length * indentLevel)) > -1
indentLevel += 1
# [ tagLineEndIndex, tag ]
tagLineEndIndex = (content.indexOf linebreakCharacter, tagStartIndex)
if tagLineEndIndex is -1
tagLineEndIndex = content.length - 1
tag = (content.slice tagStartIndex, tagLineEndIndex)
# [ primaryQuotedString ]
indexQuote1 = tag.indexOf '"', tagString.length
if indexQuote1 is -1
throw new Error "Expected Double Quote in \"#{resourcePath}\""
indexQuote2 = tag.indexOf '"', indexQuote1 + 1
if indexQuote2 is -1
throw new Error "Expected Double Quote in \"#{resourcePath}\""
primaryQuotedString = name = tag.slice indexQuote1 + 1, indexQuote2
# [ otherParameterMap ]
otherParameterMap = {}
for parameter in otherParameterList
if (tag.indexOf parameter, indexQuote2 + 1) > -1
otherParameterMap[parameter] = true
else
otherParameterMap[parameter] = false
return [ tagLineStartIndex, indentLevel, tagLineEndIndex, tag, primaryQuotedString, otherParameterMap ]
__extractNextIncludeTag: (resourcePath, content, offset)->
tagString = .prefix + 'include' + .postfix
{ linebreakCharacter, indentCharacter } =
tagStartIndex = content.indexOf tagString, offset
return [ -1, null ] if tagStartIndex is -1
res = tagString, ['isolated'], resourcePath, content, tagStartIndex, linebreakCharacter, indentCharacter
return [ tagStartIndex, res ]
__insertIncludeTagContents: ( resourcePath, subResourcePath, subContent, content, tagLineStartIndex, tagLineEndIndex ) ->
# insert comments
if .insertComments
beginComments = .commentPrefix + "start of inclusion from \"#{subResourcePath}\" into \"#{resourcePath}\"" + .commentPostfix
endComments = .commentPrefix + "end of inclusion \"#{subResourcePath}\"" + .commentPostfix
subContent = beginComments + subContent + endComments
# replace
left = content.slice 0, tagLineStartIndex
middle = subContent
right = content.slice tagLineEndIndex, content.length
content = left + middle + right
return content
__processIncludeTag: (resourcePath, content, cbfn, offset = 0)->
[ tagStartIndex, res ] = resourcePath, content, offset
if tagStartIndex is -1
return cbfn null, content
[ tagLineStartIndex, indentLevel, tagLineEndIndex, tag, primaryQuotedString, otherParameterMap ] = res
offset = tagStartIndex + 1
subResourcePath = path.join (path.dirname resourcePath), primaryQuotedString
# extract subContent (recursive)
subResourcePath, otherParameterMap.isolated, true, (err, subContent)=>
return cbfn err if err
content = resourcePath, subResourcePath, subContent, content, tagLineStartIndex, tagLineEndIndex
resourcePath, content, cbfn, offset
__processSyncIncludeTag: (resourcePath, content)->
offset = 0
loop
[ tagStartIndex, res ] = resourcePath, content, offset
break if tagStartIndex is -1
[ tagLineStartIndex, indentLevel, tagLineEndIndex, tag, primaryQuotedString, otherParameterMap ] = res
offset = tagStartIndex + 1
subResourcePath = path.join (path.dirname resourcePath), primaryQuotedString
# extract subContent (recursive)
subContent = subResourcePath, otherParameterMap.isolated
content = resourcePath, subResourcePath, subContent, content, tagLineStartIndex, tagLineEndIndex
return content
__processRegionTag: (resourcePath, content)->
linebreakCharacter = .linebreakCharacter
indentCharacter = .indentCharacter
regionMap = {}
offset = 0
while (tagStartIndex = content.indexOf '@region', offset) > -1
offset = tagStartIndex + 1
# figure out indent level
if tagStartIndex is 0
indentLevel = 0
tagLineStartIndex = 0
else
caseContent = content.slice 0, tagStartIndex
lastLineEnd = caseContent.lastIndexOf linebreakCharacter
tagLineStartIndex = lastLineEnd + 1 # NOTE: Also covers the scenario when lastLineEnd is -1
indentationString = content.slice tagLineStartIndex, tagStartIndex
indentLevel = 0
while (indentationString.indexOf indentCharacter, (indentCharacter.length * indentLevel)) > -1
indentLevel += 1
# extract tag
tagLineEndIndex = (content.indexOf linebreakCharacter, offset)
if tagLineEndIndex is -1
tagLineEndIndex = content.length - 1
tag = (content.slice tagStartIndex, tagLineEndIndex)
# extract name
indexQuote1 = tag.indexOf '"', '@region'.length
if indexQuote1 is -1
throw new Error "Expected Double Quote in \"#{resourcePath}\""
indexQuote2 = tag.indexOf '"', indexQuote1 + 1
if indexQuote2 is -1
throw new Error "Expected Double Quote in \"#{resourcePath}\""
name = tag.slice indexQuote1 + 1, indexQuote2
# extract other parameters
if ((tag.slice indexQuote2 + 1, tag.length).indexOf 'indented') > -1
indented = true
else
indented = false
# check for duplication
if name of regionMap
err = new Error "Duplicate Region \"#{name}\" in \"#{resourcePath}\""
throw err
# locate the region
localOffset = offset
endOfBlockString = linebreakCharacter + (indentCharacter for i in [0...indentLevel]).join ''
inBlockString = linebreakCharacter + (indentCharacter for i in [0...(indentLevel + 1)]).join ''
loop
endOfBlockStringIndex = content.indexOf endOfBlockString, localOffset
inBlockStringIndex = content.indexOf inBlockString, localOffset
break if endOfBlockStringIndex is -1
break unless endOfBlockStringIndex is inBlockStringIndex
localOffset = endOfBlockStringIndex + 1
endOfBlockStringIndex = content.length if endOfBlockStringIndex is -1
# extract the region
regionContent = (content.slice (tagLineEndIndex+1), endOfBlockStringIndex)
regionContent = (linebreakCharacter + regionContent).split inBlockString
regionContent = (regionContent.join endOfBlockString).slice 1
# replace
left = content.slice 0, tagLineStartIndex
middle = ''
right = content.slice (endOfBlockStringIndex + 1), content.length
content = left + middle + right
# add
regionMap[name] = {
resourcePath: resourcePath,
regionContent: regionContent
indented: indented
indentLevel: indentLevel
}
return [ regionMap, content ]
__processUseTag: (resourcePath, content, regionMap)->
linebreakCharacter = .linebreakCharacter
indentCharacter = .indentCharacter
offset = 0
while (tagStartIndex = content.indexOf '@use', offset) > -1
offset = tagStartIndex + 1
# figure out indent level
if tagStartIndex is 0
indentLevel = 0
tagLineStartIndex = 0
else
caseContent = content.slice 0, tagStartIndex
lastLineEnd = caseContent.lastIndexOf linebreakCharacter
tagLineStartIndex = lastLineEnd + 1 # NOTE: Also covers the scenario when lastLineEnd is -1
indentationString = content.slice tagLineStartIndex, tagStartIndex
indentLevel = 0
while (indentationString.indexOf indentCharacter, (indentCharacter.length * indentLevel)) > -1
indentLevel += 1
# extract tag
tagLineEndIndex = (content.indexOf linebreakCharacter, offset)
if tagLineEndIndex is -1
tagLineEndIndex = content.length - 1
tag = (content.slice tagStartIndex, tagLineEndIndex)
# extract name
indexQuote1 = tag.indexOf '"', '@use'.length
if indexQuote1 is -1
throw new Error "Expected Double Quote in \"#{resourcePath}\""
indexQuote2 = tag.indexOf '"', indexQuote1 + 1
if indexQuote2 is -1
throw new Error "Expected Double Quote in \"#{resourcePath}\""
name = tag.slice indexQuote1 + 1, indexQuote2
# extract other parameters
if ((tag.slice indexQuote2 + 1, tag.length).indexOf 'as-is') > -1
asIs = true
else
asIs = false
# extract other parameters
if ((tag.slice indexQuote2 + 1, tag.length).indexOf 'match-indent') > -1
matchIndent = true
if asIs
throw new Error "match-indent and as-is may not be used in the same @use tag in \"#{resourcePath}\""
else
matchIndent = false
# check for duplication
unless name of regionMap
throw new Error "Unknown Region \"#{name}\" in \"#{resourcePath}\""
region = regionMap[name]
regionContent = region.regionContent
# insert comments
if .insertComments
fillerString = (indentCharacter for i in [0...indentLevel]).join ''
beginComments = .commentPrefix + "start of use of \"#{name}\"" + .commentPostfix
endComments = .commentPrefix + "end of use" + .commentPostfix
regionContent = beginComments + '\n' + regionContent + '\n' + endComments
# decide whether to match indentation
matchIndent = if matchIndent or (region.indented and not asIs) then true else false
# match indent
if matchIndent
fillerString = (indentCharacter for i in [0...indentLevel]).join ''
# regionContent = region.regionContent
regionContent = regionContent.split linebreakCharacter
regionContent = fillerString + regionContent.join linebreakCharacter + fillerString
# replace
left = content.slice 0, tagLineStartIndex
middle = regionContent
right = content.slice tagLineEndIndex, content.length
content = left + middle + right
return content
__trim: (content)->
content = content.replace /^\s*/g, ''
content = content.replace /\s*$/g, ''
return content
__processSync: (resourcePath, isolated = false, trim = true)->
content = .getContentSync resourcePath
content = resourcePath, content
if isolated
[ regionMap, content ] = resourcePath, content
if .presetRegions
for name, region of .presetRegions
regionMap[name] = region
content = resourcePath, content, regionMap
content = content if trim
return content
__process: (resourcePath, isolated = false, trim = true, cbfn)->
.getContent resourcePath, (err, content)=>
return cbfn err if err
resourcePath, content, (err, content)=>
return cbfn err if err
if isolated
try
[ regionMap, content ] = resourcePath, content
if .presetRegions
for name, region of .presetRegions
regionMap[name] = region
content = resourcePath, content, regionMap
content = content if trim
catch ex
return cbfn ex
return cbfn null, content
processSync: (resourcePath)->
content = resourcePath, true
.setContentSync resourcePath, content
process: (resourcePath, cbfn)->
resourcePath, true, true, (err, content)=>
return cbfn err if err
.setContent resourcePath, content, (err)=>
return cbfn err
= MetaEngine