//
|
// officegen: basic common code
|
//
|
// Please refer to README.md for this module's documentations.
|
//
|
// Copyright (c) 2013 Ziv Barber;
|
//
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// of this software and associated documentation files (the "Software"), to deal
|
// in the Software without restriction, including without limitation the rights
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
// copies of the Software, and to permit persons to whom the Software is
|
// furnished to do so, subject to the following conditions:
|
//
|
// The above copyright notice and this permission notice shall be included in all
|
// copies or substantial portions of the Software.
|
//
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
// SOFTWARE.
|
//
|
|
;(function() {
|
var sys = require('util')
|
var events = require('events')
|
|
require('stream').Transform || require('readable-stream/transform')
|
|
// Used by generate:
|
var archiver = require('archiver')
|
var fs = require('fs')
|
var PassThrough =
|
require('stream').PassThrough || require('readable-stream/passthrough')
|
|
// Global data shared by all the officegen objects:
|
|
var officegenGlobals = {} // Our internal global objects.
|
|
officegenGlobals.settings = {}
|
officegenGlobals.types = {}
|
officegenGlobals.docPrototypes = {}
|
officegenGlobals.resParserTypes = {}
|
|
/**
|
* The constructor of the office generator object.
|
* <br /><br />
|
* This constructor function is been called by makegen().
|
*
|
* <h3><b>The options:</b></h3>
|
*
|
* The configuration options effecting the operation of the officegen object. Some of them can be only been
|
* declared on the 'options' object passed to the constructor object and the rest can be configured by either
|
* a property with the same name or by special function.
|
*
|
* <h3><b>List of options:</b></h3>
|
*
|
* <ul>
|
* <li>'type' - the type of generator to create. Possible options: either 'pptx', 'docx' or 'xlsx'.</li>
|
* <li>'creator' - the name of the document's author. The default is 'officegen'.</li>
|
* <li>'onend' - callback that been fired after finishing to create the zip stream.</li>
|
* <li>'onerr' - callback that been fired on error.</li>
|
* </ul>
|
*
|
* @param {object} options List of configuration options (see in the description of this function).
|
* @constructor
|
* @name officegen
|
*/
|
var officegen = function(options) {
|
if (this instanceof officegen === false) {
|
return new officegen(options)
|
} // Endif.
|
|
events.EventEmitter.call(this)
|
// Transform.call(this, {objectMode : true})
|
|
// Internal events for plugins - NOT for the user:
|
// event 'beforeGen'
|
// event 'afterGen'
|
// event 'clearDoc'
|
|
var genobj = this // Can be accessed by all the functions been declared inside the officegen object.
|
|
/**
|
* For all the private data of each officegen instance that we don't want the user of officegen to access it.
|
* Each officegen object has it's own copy of the private object so changes been done to the private object of one officegen document will not effect other objects.
|
* <br /><br />
|
* List of data members:
|
* <ul>
|
* <li>features (object) - ???.</li>
|
* <ul>
|
* <li>type</li>
|
* <li>outputType (string) - The type of the container to hold all the resources.</li>
|
* </ul>
|
* <li>pages</li>
|
* <li>resources</li>
|
* <li>type</li>
|
* <li>plugs</li>
|
* <li>length</li>
|
* </ul>
|
* @namespace officegen#private
|
*/
|
var privateData = {}
|
|
// Features been configured by the type selector and you can't change them:
|
privateData.features = {}
|
privateData.features.type = {}
|
privateData.features.outputType = 'zip'
|
// privateData.features.page_name
|
|
// Resources and "pages" of the document:
|
privateData.pages = [] // Information about all the pages to create.
|
privateData.resources = [] // List of all the resources to create inside the zip.
|
|
// Extra data needed by the document and it's specific to either to "document prototype" (like MS-office) or a document type (like pptx):
|
privateData.type = {}
|
|
/**
|
* Combine the given options and the default values.
|
* <br /><br />
|
*
|
* This function creating the real options object.
|
*
|
* @param {object} options The options to configure.
|
* @param {object} source The default configuration.
|
*/
|
function setOptions(object, source) {
|
object = object || {}
|
|
var objectTypes = {
|
boolean: false,
|
function: true,
|
object: true,
|
number: false,
|
string: false,
|
undefined: false
|
}
|
|
function isObject(value) {
|
return !!(value && objectTypes[typeof value])
|
}
|
|
function keys(object) {
|
if (!isObject(object)) {
|
return []
|
}
|
|
return Object.keys(object)
|
}
|
|
var index
|
var iterable = object
|
var result = iterable
|
|
var args = arguments
|
var argsIndex = 0
|
var argsLength = args.length
|
|
// Loop variables:
|
var ownIndex = -1
|
var ownProps = objectTypes[typeof iterable] && keys(iterable)
|
var length = ownProps ? ownProps.length : 0
|
|
while (++argsIndex < argsLength) {
|
iterable = args[argsIndex]
|
|
if (iterable && objectTypes[typeof iterable]) {
|
while (++ownIndex < length) {
|
index = ownProps[ownIndex]
|
|
if (
|
typeof result[index] === 'undefined' ||
|
result[index] === null
|
) {
|
result[index] = iterable[index]
|
} else if (isObject(result[index]) && isObject(iterable[index])) {
|
result[index] = setOptions(result[index], iterable[index])
|
} // Endif.
|
} // End of while loop.
|
} // Endif.
|
} // End of while loop.
|
|
return result
|
}
|
|
/**
|
* Configure this object to generate the given type of document.
|
* <br /><br />
|
*
|
* Called by the document constructor to configure the new document object to the given type.
|
*
|
* @param {string} new_type The type of document to create.
|
*/
|
function setGeneratorType(new_type) {
|
privateData.length = 0
|
var is_ok = false
|
|
if (new_type) {
|
for (var cur_type in officegenGlobals.types) {
|
if (
|
cur_type === new_type &&
|
officegenGlobals.types[cur_type] &&
|
officegenGlobals.types[cur_type].createFunc
|
) {
|
officegenGlobals.types[cur_type].createFunc(
|
genobj,
|
new_type,
|
genobj.options,
|
privateData,
|
officegenGlobals.types[cur_type]
|
)
|
is_ok = true
|
break
|
} // Endif.
|
} // End of for loop.
|
|
if (!is_ok) {
|
// console.error ( '\nFATAL ERROR: Either unknown or unsupported file type - %s\n', options.type )
|
genobj.emit('error', 'FATAL ERROR: Invalid file type.')
|
} // Endif.
|
} // Endif.
|
}
|
|
/**
|
* API for plugins.
|
* <br /><br />
|
* Officegen plugins can extend officegen to support more document formats.
|
* <br /><br />
|
* Examples how to do it can be found on lib/gendocx.js, lib/genpptx.js and lib/genxlsx.js.
|
* @namespace officegen#private#plugs
|
* @example <caption>Adding a new document type to officegen</caption>
|
* var baseobj = require ( "officegen" )
|
*
|
* function makeMyDoc ( officegenObj, typeCodeName, options, officegenObjPlugins, typeInfo ) {
|
* // officegenObjPlugins = Plugins access to extend officegenObj.
|
* }
|
*
|
* baseobj.plugins.registerDocType (
|
* 'mydoctype', // The document type's code name.
|
* makeMyDoc,
|
* {},
|
* baseobj.docType.TEXT,
|
* "My Special Document File Format"
|
* )
|
*/
|
privateData.plugs = {
|
/**
|
* Add a resource to the list of resources to place inside the output zip file.
|
* <br /><br />
|
*
|
* This method adding a resource to the list of resources to place inside the output document ZIP.
|
* <br />
|
* Changed by vtloc in 2014Jan10.
|
*
|
* @param {string} resource_name The name of the resource (path).
|
* @param {string} type_of_res The type of this resource: either 'file', 'buffer', 'stream' or 'officegen' (the last one allow you to put office document inside office document).
|
* @param {object} res_data Optional data to use when creating this resource.
|
* @param {function} res_cb Callback to generate this resource (for 'buffer' mode only).
|
* @param {boolean} is_always Is true if this resource is perment for all the zip of this document type.
|
* @param {boolean} removed_after_used Is true if we need to delete this file after used.
|
* @memberof officegen#private#plugs
|
*/
|
intAddAnyResourceToParse: function(
|
resource_name,
|
type_of_res,
|
res_data,
|
res_cb,
|
is_always,
|
removed_after_used
|
) {
|
var newRes = {}
|
|
newRes.name = resource_name
|
newRes.type = type_of_res
|
newRes.data = res_data
|
newRes.callback = res_cb
|
newRes.is_perment = is_always
|
|
// delete the temporatory resources after used
|
// @author vtloc
|
// @date 2014Jan10
|
if (removed_after_used) {
|
newRes.removed_after_used = removed_after_used
|
} else {
|
newRes.removed_after_used = false
|
} // Endif.
|
|
if (officegenGlobals.settings.verbose) {
|
console.log('[officegen] Push new res : ', newRes)
|
} // Endif.
|
|
privateData.resources.push(newRes)
|
},
|
|
/**
|
* Any additional plugin API must be placed here.
|
* @memberof officegen#private#plugs
|
*/
|
type: {}
|
}
|
|
// Public API:
|
|
/**
|
* Generating the output document stream.
|
* <br /><br />
|
*
|
* The user of officegen must call this method after filling all the information about what to put inside
|
* the generated document. This method is creating the output document directly into the given stream object.
|
*
|
* The options parameters properties:
|
*
|
* 'finalize' - callback to be called after finishing to generate the document.
|
* 'error' - callback to be called on error.
|
*
|
* @param {object} output_stream The stream to receive the generated document.
|
* @param {object} options Way to pass callbacks.
|
* @function generate
|
* @memberof officegen
|
* @instance
|
*/
|
this.generate = function(output_stream, options) {
|
if (officegenGlobals.settings.verbose) {
|
console.log('[officegen] Start generate() : ', {
|
outputType: privateData.features.outputType
|
})
|
}
|
|
if (typeof options === 'object') {
|
if (options.finalize) {
|
genobj.on('finalize', options.finalize)
|
} // Endif.
|
|
if (options.error) {
|
genobj.on('error', options.error)
|
} // Endif.
|
} // Endif.
|
|
if (privateData.features.page_name) {
|
if (privateData.pages.length === 0) {
|
genobj.emit(
|
'error',
|
'ERROR: No ' +
|
privateData.features.page_name +
|
' been found inside your document.'
|
)
|
} // Endif.
|
} // Endif.
|
|
// Allow the type generator to prepare everything:
|
genobj.emit('beforeGen', privateData)
|
|
var archive = archiver(
|
privateData.features.outputType === 'zip' ? 'zip' : 'tar'
|
)
|
|
/**
|
* Error handler.
|
* <br /><br />
|
*
|
* This is our error handler method for creating archive.
|
*
|
* @param {string} err The error string.
|
*/
|
function onArchiveError(err) {
|
genobj.emit('error', err)
|
}
|
|
archive.on('error', onArchiveError)
|
|
if (privateData.features.outputType === 'gzip') {
|
var zlib = require('zlib')
|
var gzipper = zlib.createGzip()
|
|
archive.pipe(gzipper).pipe(output_stream)
|
} else {
|
archive.pipe(output_stream)
|
} // Endif.
|
|
/**
|
* Add the next resource into the zip stream.
|
* <br /><br />
|
*
|
* This function adding the next resource into the zip stream.
|
*/
|
function generateNextResource() {
|
var resStream
|
|
for (
|
let cur_index = 0;
|
cur_index < privateData.resources.length;
|
cur_index++
|
) {
|
if (officegenGlobals.settings.verbose) {
|
console.log(
|
'[officegen] generateNextResource ' + cur_index + ' : ',
|
privateData.resources[cur_index]
|
)
|
}
|
|
if (typeof privateData.resources[cur_index] !== 'undefined') {
|
switch (privateData.resources[cur_index].type) {
|
// Generate the resource text data by calling to provided function:
|
case 'buffer':
|
resStream = privateData.resources[cur_index].callback(
|
privateData.resources[cur_index].data
|
)
|
break
|
|
// Just copy the file as is:
|
case 'file':
|
resStream = fs.createReadStream(
|
privateData.resources[cur_index].data ||
|
privateData.resources[cur_index].name
|
)
|
break
|
|
// Just use this stream:
|
case 'stream':
|
resStream = privateData.resources[cur_index].data
|
break
|
|
// Officegen object:
|
case 'officegen':
|
resStream = new PassThrough()
|
privateData.resources[cur_index].data.generate(resStream)
|
break
|
|
// Custom parser:
|
default:
|
for (var cur_parserType in officegenGlobals.resParserTypes) {
|
if (
|
cur_parserType === privateData.resources[cur_index].type &&
|
officegenGlobals.resParserTypes[cur_parserType] &&
|
officegenGlobals.resParserTypes[cur_parserType].parserFunc
|
) {
|
resStream = officegenGlobals.resParserTypes[
|
cur_parserType
|
].parserFunc(
|
genobj,
|
privateData.resources[cur_index].name,
|
privateData.resources[cur_index].callback, // Can be used as the template source for template engines.
|
privateData.resources[cur_index].data, // The data for the template engine.
|
officegenGlobals.resParserTypes[cur_parserType].extra_data
|
)
|
break
|
} // Endif.
|
} // End of for loop.
|
} // End of switch.
|
|
if (typeof resStream !== 'undefined') {
|
if (officegenGlobals.settings.verbose) {
|
console.log(
|
'[officegen] Adding into archive : "' +
|
privateData.resources[cur_index].name +
|
'" (' +
|
privateData.resources[cur_index].type +
|
')...'
|
)
|
} // Endif.
|
|
archive.append(resStream, {
|
name: privateData.resources[cur_index].name
|
})
|
|
continue
|
} else {
|
if (officegenGlobals.settings.verbose) {
|
console.log('[officegen] resStream is undefined') // is it normal ??
|
}
|
continue
|
} // Endif.
|
} else {
|
// Removed resource - just ignore it:
|
continue
|
} // Endif.
|
} // Endfor
|
|
privateData.resources.forEach(function(resource) {
|
if (resource.removed_after_used) {
|
var filename = resource.data || resource.name
|
|
if (officegenGlobals.settings.verbose) {
|
console.log('[officegen] Removing resource: ', filename)
|
}
|
|
fs.unlinkSync(filename)
|
}
|
})
|
|
// No more resources to add - close the archive:
|
if (officegenGlobals.settings.verbose) {
|
console.log('[officegen] Finalizing archive ...')
|
}
|
archive.finalize()
|
|
// Event to the type generator:
|
genobj.emit('afterGen', privateData, null, archive.pointer())
|
|
genobj.emit('finalize', archive.pointer())
|
}
|
|
// Start the process of generating the output zip stream:
|
generateNextResource()
|
}
|
|
/**
|
* Reuse this object for a new document of the same type.
|
* <br /><br />
|
*
|
* Call this method if you want to start generating a new document of the same type using this object.
|
* @function startNewDoc
|
* @memberof officegen
|
* @instance
|
*/
|
this.startNewDoc = function() {
|
var kill = []
|
|
for (var i = 0; i < privateData.resources.length; i++) {
|
if (!privateData.resources[i].is_perment) {
|
kill.push(i)
|
} // Endif.
|
} // End of for loop.
|
|
for (i = 0; i < kill.length; i++) {
|
privateData.resources.splice(kill[i] - i, 1)
|
} // End of for loop.
|
|
privateData.pages.length = 0
|
|
genobj.emit('clearDoc', privateData)
|
}
|
|
// Public API - plugin API:
|
|
/**
|
* Register a new resource to add into the generated ZIP stream.
|
* <br /><br />
|
*
|
* Using this method the user can add extra custom resources into the generated ZIP stream.
|
*
|
* @param {string} resource_name The name of the resource (path).
|
* @param {string} type_of_res The type of this resource: either 'file' or 'buffer'.
|
* @param {object} res_data Optional data to use when creating this resource.
|
* @param {function} res_cb Callback to generate this resource (for 'buffer' mode only).
|
* @function addResourceToParse
|
* @memberof officegen
|
* @instance
|
*/
|
this.addResourceToParse = function(
|
resource_name,
|
type_of_res,
|
res_data,
|
res_cb
|
) {
|
// We don't want the user to add permanent resources to the list of resources:
|
privateData.plugs.intAddAnyResourceToParse(
|
resource_name,
|
type_of_res,
|
res_data,
|
res_cb,
|
false
|
)
|
}
|
|
if (typeof options === 'string') {
|
options = { type: options }
|
} // Endif.
|
|
// See the officegen descriptions for the rules of the options:
|
genobj.options = setOptions(options, { type: 'unknown' })
|
|
if (genobj.options && genobj.options.onerr) {
|
genobj.on('error', genobj.options.onerr)
|
} // Endif.
|
|
if (genobj.options && genobj.options.onend) {
|
genobj.on('finalize', genobj.options.onend)
|
} // Endif.
|
|
// Configure this object depending on the user's selected type:
|
if (genobj.options.type) {
|
setGeneratorType(genobj.options.type)
|
} // Endif.
|
|
return this
|
}
|
|
sys.inherits(officegen, events.EventEmitter)
|
|
/**
|
* Create a new officegen object.
|
* <br /><br />
|
*
|
* This method creating a new officegen based object.
|
*/
|
module.exports = function(options) {
|
return new officegen(options)
|
}
|
|
/**
|
* Change the verbose state of officegen.
|
* <br /><br />
|
*
|
* This is a global settings effecting all the officegen objects in your application. You should
|
* use it only for debugging.
|
*
|
* @param {boolean} new_state Either true or false.
|
*/
|
module.exports.setVerboseMode = function setVerboseMode(new_state) {
|
officegenGlobals.settings.verbose = new_state
|
}
|
|
/**
|
* Plugin API effecting all the instances of the officegen object.
|
*
|
* @namespace officegen#plugins
|
*/
|
var plugins = {
|
/**
|
* Register a new type of document that we can generate.
|
* <br /><br />
|
*
|
* This method registering a new type of document that we can generate. You can extend officegen to support any
|
* type of document that based on resources files inside ZIP stream.
|
*
|
* @param {string} typeName The type of the document file.
|
* @param {function} createFunc The function to use to create this type of file.
|
* @param {object} schema_data Information needed by Schema-API to generate this kind of document.
|
* @param {string} docType Document type.
|
* @param {string} displayName The display name of this type.
|
* @memberof officegen#plugins
|
*/
|
registerDocType: function(
|
typeName,
|
createFunc,
|
schema_data,
|
docType,
|
displayName
|
) {
|
officegenGlobals.types[typeName] = {}
|
officegenGlobals.types[typeName].createFunc = createFunc
|
officegenGlobals.types[typeName].schema_data = schema_data
|
officegenGlobals.types[typeName].type = docType
|
officegenGlobals.types[typeName].display = displayName
|
},
|
|
/**
|
* Get a document type object by name.
|
* <br /><br />
|
*
|
* This method get a document type object.
|
*
|
* @param {string} typeName The name of the document type.
|
* @return The plugin object of the document type.
|
* @memberof officegen#plugins
|
*/
|
getDocTypeByName: function(typeName) {
|
return officegenGlobals.types[typeName]
|
},
|
|
/**
|
* Register a document prototype object.
|
* <br /><br />
|
*
|
* This method registering a prototype document object. You can place all the common code needed by a group of document
|
* types in a single prototype object.
|
*
|
* @param {string} typeName The name of the prototype object.
|
* @param {object} baseObj The prototype object.
|
* @param {string} displayName The display name of this type.
|
* @memberof officegen#plugins
|
*/
|
registerPrototype: function(typeName, baseObj, displayName) {
|
officegenGlobals.docPrototypes[typeName] = {}
|
officegenGlobals.docPrototypes[typeName].baseObj = baseObj
|
officegenGlobals.docPrototypes[typeName].display = displayName
|
},
|
|
/**
|
* Get a document prototype object by name.
|
* <br /><br />
|
*
|
* This method get a prototype object.
|
*
|
* @param {string} typeName The name of the prototype object.
|
* @return The prototype plugin object.
|
* @memberof officegen#plugins
|
*/
|
getPrototypeByName: function getPrototypeByName(typeName) {
|
return officegenGlobals.docPrototypes[typeName]
|
},
|
|
/**
|
* Register a new resource parser.
|
* <br /><br />
|
*
|
* This method registering a new resource parser. One use of this feature is in case that you are developing a new
|
* type of document and you want to extend officegen to use some kind of template engine as jade, ejs, haml* or CoffeeKup.
|
* In this case you can use a template engine to generate one or more of the resources inside the output archive.
|
* Another use of this method is to replace an existing plugin with different implementation.
|
*
|
* @param {string} typeName The type of the parser plugin.
|
* @param {function} parserFunc The resource generating function.
|
* @param {object} extra_data Optional additional data that may be required by the parser function.
|
* @param {string} displayName The display name of this type.
|
* @memberof officegen#plugins
|
*/
|
registerParserType: function(
|
typeName,
|
parserFunc,
|
extra_data,
|
displayName
|
) {
|
officegenGlobals.resParserTypes[typeName] = {}
|
officegenGlobals.resParserTypes[typeName].parserFunc = parserFunc
|
officegenGlobals.resParserTypes[typeName].extra_data = extra_data
|
officegenGlobals.resParserTypes[typeName].display = displayName
|
},
|
|
/**
|
* Get if we need verbose mode.
|
* @param {string} docType Optional, Allow filtering by document type.
|
* @param {string} moduleName Optional, Allow filtering by feature / module.
|
* @memberof officegen#plugins
|
*/
|
getVerboseMode: function(docType, moduleName) {
|
if (!docType && !moduleName) {
|
return !!officegenGlobals.settings.verbose
|
} // Endif.
|
|
var verboseFlag
|
|
if (
|
officegenGlobals.settings.verbose &&
|
typeof officegenGlobals.settings.verbose === 'object'
|
) {
|
if (
|
docType &&
|
officegenGlobals.settings.verbose.docType &&
|
typeof officegenGlobals.settings.verbose.docType === 'object' &&
|
officegenGlobals.settings.verbose.docType.indexOf
|
) {
|
verboseFlag =
|
officegenGlobals.settings.verbose.docType.indexOf(docType) >= 0
|
} // Endif.
|
|
if (
|
verboseFlag !== false &&
|
moduleName &&
|
officegenGlobals.settings.verbose.moduleName &&
|
typeof officegenGlobals.settings.verbose.moduleName === 'object' &&
|
officegenGlobals.settings.verbose.moduleName.indexOf
|
) {
|
verboseFlag =
|
officegenGlobals.settings.verbose.moduleName.indexOf(moduleName) >=
|
0
|
} // Endif.
|
} // Endif.
|
|
return !!verboseFlag
|
}
|
}
|
|
module.exports.plugins = plugins
|
module.exports.schema = officegenGlobals.types
|
module.exports.docType = { TEXT: 1, SPREADSHEET: 2, PRESENTATION: 3 }
|
})()
|