opendocx-node
Version:
OpenDocx document assembly for Node.js
145 lines (132 loc) • 5.73 kB
JavaScript
'use strict'
const { Scope, Engine, IndirectVirtual } = require('yatte')
const uuidv4 = require('uuid/v4')
const XmlDataBuilder = require('./xmlbuilder')
const loadTemplateModule = require('./load-template-module')
class XmlAssembler {
constructor (scope) {
this.indirects = []
this.missing = {}
this.errors = []
this.contextStack = null
if (scope) {
this.contextStack = Scope.pushObject(scope, this.contextStack)
}
this.xmlStack = new XmlDataBuilder()
}
assembleXml (templateJsFile, joinstr = '') {
try {
const extractedLogic = loadTemplateModule(templateJsFile)
extractedLogic.evaluate(this.contextStack, null, this)
return this.xmlStack.toString(joinstr)
} catch (e) {
this.errors.push(e.message)
}
}
beginObject (ident, objContext) {
if (objContext !== this.contextStack && typeof objContext === 'number') { // top-level object
this.contextStack = Scope.pushListItem(objContext, this.contextStack)
this.xmlStack.pushObject(ident) // no need to push top-level object on XML stack
}
}
endObject () {
this.contextStack = Scope.pop(this.contextStack)
this.xmlStack.popObject()
}
define (ident, expr) {
if (Scope.empty(this.contextStack)) {
throw new Error('internal error: Cannot define a member on an empty context stack')
}
const frame = this.contextStack
if (frame.frameType === Scope.LIST) {
throw new Error(`Evaluation error: cannot retrieve a member '${expr}' (${
ident}) from a LIST; found a LIST when expecting a SINGLE object`)
// error message & handling on this needs work, but it happens when a list context (which is expected to
// represent an array of objects) actually contains an array OF ARRAYS of objects (nested instead of flat).
// OpenDocx attempts to define/look up property 'ident' from an item in the outer list, and it fails
// with this error because the item on which the lookup is being performed is, itself, another list.
}
const evaluator = Engine.compileExpr(expr) // these are cached so this should be fast
let value = frame.evaluate(evaluator) // we need to make sure this is memoized to avoid unnecessary re-evaluation
if (value === null || typeof value === 'undefined') {
this.missing[expr] = true
value = this.missingValuePlaceholder(expr, evaluator.ast)
} else if (typeof value === 'object') {
if (value instanceof IndirectVirtual) {
value = this.indirectSub(value)
} else if (value.errors || value.missing) {
// value is a yatte EvaluationResult, probably because of nested template evaluation
if (value.missing && value.missing.length > 0) {
value.missing.forEach((expr) => { this.missing[expr] = true })
}
if (value.errors && value.errors.length > 0) {
value.errors.forEach((errmsg) => { this.errors.push(errmsg) })
}
value = value.valueOf() // get the actual evaluated value
}
}
if (value === '') {
this.xmlStack.set(ident, undefined)
} else if (typeof value === 'object') { // define should only be used to output simple scalar values into XML
this.xmlStack.set(ident, value.toString()) // probably bad input; convert to a string representation
} else {
this.xmlStack.set(ident, value)
}
}
indirectSub (indirect) {
if (indirect.contentType !== 'text') { // docx, markdown, etc... substitute special placeholder
// see if this indirect has already been encountered/added
let existing = this.indirects.find(ex => Object.keys(indirect).every(propName => {
const indPropVal = indirect[propName]
return (indPropVal && (indPropVal instanceof Scope))
? indPropVal.valueEqualTo(ex[propName])
: indPropVal === ex[propName]
}))
if (!existing) {
existing = { ...indirect, id: uuidv4() }
this.indirects.push(existing)
}
let uri = `oxpt://DocumentAssembler/insert/${existing.id}`
if (indirect.KeepSections) {
uri += '?KeepSections=true'
}
return uri
}
// else plain text... just evaluate it
return indirect.toString()
}
beginCondition (ident, expr) {
if (Scope.empty(this.contextStack)) {
throw new Error('internal error: Cannot define a condition on an empty context stack')
}
const frame = this.contextStack
if (frame.frameType !== Scope.OBJECT && frame.frameType !== Scope.PRIMITIVE) {
throw new Error(`Internal error: cannot define a condition on a ${frame.frameType} context`)
}
const evaluator = Engine.compileExpr(expr) // these are cached so this should be fast
const value = frame.evaluate(evaluator) // this ought to be memoized to avoid unnecessary re-evaluation
const bValue = Scope.isTruthy(value)
this.xmlStack.set(ident, bValue)
return bValue
}
beginList (ident, expr) {
const frame = this.contextStack
const evaluator = Engine.compileExpr(expr) // these are cached so this should be fast
const iterable = frame.evaluate(evaluator) // this ought to be memoized to avoid unnecessary re-evaluation
this.contextStack = Scope.pushList(iterable || [], this.contextStack)
const indices = this.contextStack.indices
this.xmlStack.pushList(ident)
return indices
}
endList () {
this.xmlStack.popList()
this.contextStack = Scope.pop(this.contextStack)
}
missingValuePlaceholder (expr, ast) {
if (ast && ast.type === Engine.AST.AngularFilterExpression) {
return this.missingValuePlaceholder(Engine.serializeAST(ast.input))
}
return '[' + expr + ']'
}
}
module.exports = XmlAssembler