mediawiki-extensions-Visual.../modules/parser/mediawiki.TokenTransformManager.js
Gabriel Wicke 6e21f6bb27 Forward-port Cite extension
* Adapted Cite extension to use current interfaces and token formats
* Improved TokenCollector

Change-Id: I20419b19edd9bbad2c2abf17a2ff1411b99c0c04
2012-05-03 13:22:01 +02:00

1159 lines
34 KiB
JavaScript

/**
* Token transformation managers with a (mostly) abstract
* TokenTransformManager base class and AsyncTokenTransformManager and
* SyncTokenTransformManager implementation subclasses. Individual
* transformations register for the token types they are interested in and are
* called on each matching token.
*
* Async token transformations are supported by the TokenAccumulator class,
* that manages as-early-as-possible and in-order return of tokens including
* buffering.
*
* See
* https://www.mediawiki.org/wiki/Parsoid/Token_stream_transformations
* for more documentation.
*
* @author Gabriel Wicke <gwicke@wikimedia.org>
*/
var events = require('events');
/**
* Base class for token transform managers
*
* @class
* @constructor
* @param {Function} callback, a callback function accepting a token list as
* its only argument.
*/
function TokenTransformManager( env, isInclude, pipeFactory, phaseEndRank, attributeType ) {
// Separate the constructor, so that we can call it from subclasses.
this._construct();
}
// Inherit from EventEmitter
TokenTransformManager.prototype = new events.EventEmitter();
TokenTransformManager.prototype.constructor = TokenTransformManager;
TokenTransformManager.prototype._construct = function () {
this.transformers = {
tag: {}, // for TagTk, EndTagTk, SelfclosingTagTk, keyed on name
text: [],
newline: [],
comment: [],
end: [], // eof
martian: [], // none of the above (unknown token type)
any: [] // all tokens, before more specific handlers are run
};
};
/**
* Register to a token source, normally the tokenizer.
* The event emitter emits a 'chunk' event with a chunk of tokens,
* and signals the end of tokens by triggering the 'end' event.
* XXX: Perform registration directly in the constructor?
*
* @method
* @param {Object} EventEmitter token even emitter.
*/
TokenTransformManager.prototype.addListenersOn = function ( tokenEmitter ) {
tokenEmitter.addListener('chunk', this.onChunk.bind( this ) );
tokenEmitter.addListener('end', this.onEndEvent.bind( this ) );
};
/**
* Add a transform registration.
*
* @method
* @param {Function} transform.
* @param {Number} rank, [0,3) with [0,1) in-order on input token stream,
* [1,2) out-of-order and [2,3) in-order on output token stream
* @param {String} type, one of 'tag', 'text', 'newline', 'comment', 'end',
* 'martian' (unknown token), 'any' (any token, matched before other matches).
* @param {String} tag name for tags, omitted for non-tags
*/
TokenTransformManager.prototype.addTransform = function ( transformation, rank, type, name ) {
var transArr,
transformer = {
transform: transformation,
rank: rank
};
if ( type === 'tag' ) {
name = name.toLowerCase();
transArr = this.transformers.tag[name];
if ( ! transArr ) {
transArr = this.transformers.tag[name] = [];
}
} else {
transArr = this.transformers[type];
}
transArr.push(transformer);
// sort ascending by rank
transArr.sort( this._cmpTransformations );
//this.env.dp( 'transforms: ', this.transformers );
};
/**
* Remove a transform registration
*
* @method
* @param {Function} transform.
* @param {Number} rank, [0,3) with [0,1) in-order on input token stream,
* [1,2) out-of-order and [2,3) in-order on output token stream
* @param {String} type, one of 'tag', 'text', 'newline', 'comment', 'end',
* 'martian' (unknown token), 'any' (any token, matched before other matches).
* @param {String} tag name for tags, omitted for non-tags
*/
TokenTransformManager.prototype.removeTransform = function ( rank, type, name ) {
var i = -1,
ts;
function rankUnEqual ( i ) {
return i.rank !== rank;
}
if ( type === 'tag' ) {
name = name.toLowerCase();
var maybeTransArr = this.transformers.tag.name;
if ( maybeTransArr ) {
this.transformers.tag.name = maybeTransArr.filter( rankUnEqual );
}
} else {
this.transformers[type] = this.transformers[type].filter( rankUnEqual ) ;
}
};
TokenTransformManager.prototype.setTokensRank = function ( tokens, rank ) {
for ( var i = 0, l = tokens.length; i < l; i++ ) {
tokens[i] = this.env.setTokenRank( rank, tokens[i] );
}
};
/**
* Predicate for sorting transformations by ascending rank.
*/
TokenTransformManager.prototype._cmpTransformations = function ( a, b ) {
return a.rank - b.rank;
};
/**
* Get all transforms for a given token
*/
TokenTransformManager.prototype._getTransforms = function ( token ) {
var ts;
switch ( token.constructor ) {
case String:
ts = this.transformers.text;
break;
case NlTk:
ts = this.transformers.newline;
break;
case CommentTk:
ts = this.transformers.comment;
break;
case EOFTk:
ts = this.transformers.end;
break;
case TagTk:
case EndTagTk:
case SelfclosingTagTk:
ts = this.transformers.tag[token.name.toLowerCase()];
if ( ! ts ) {
ts = [];
}
break;
default:
ts = this.transformers.martian;
break;
}
// XXX: cache this to avoid constant re-sorting?
if ( this.transformers.any.length ) {
ts = ts.concat( this.transformers.any );
ts.sort( this._cmpTransformations );
}
return ts;
};
/******************** Async token transforms: Phase 2 **********************/
/**
* Asynchronous and potentially out-of-order token transformations, used in phase 2.
*
* return protocol for individual transforms:
* { tokens: [tokens], async: true }: async expansion -> outstanding++ in parent
* { tokens: [tokens] }: fully expanded, tokens will be reprocessed
* { token: token }: single-token return
*
* @class
* @constructor
* @param {Function} childFactory: A function that can be used to create a
* new, nested transform manager:
* nestedAsyncTokenTransformManager = manager.newChildPipeline( inputType, args );
* @param {Object} args, the argument map for templates
* @param {Object} env, the environment.
*/
function AsyncTokenTransformManager ( env, isInclude, pipeFactory, phaseEndRank, attributeType ) {
this.env = env;
this.isInclude = isInclude;
this.pipeFactory = pipeFactory;
this.phaseEndRank = phaseEndRank;
this.attributeType = attributeType;
this.setFrame( null, null, {} );
this._construct();
}
// Inherit from TokenTransformManager, and thus also from EventEmitter.
AsyncTokenTransformManager.prototype = new TokenTransformManager();
AsyncTokenTransformManager.prototype.constructor = AsyncTokenTransformManager;
/**
* Reset the internal token and outstanding-callback state of the
* TokenTransformManager, but keep registrations untouched.
*
* @method
* @param {Object} args, template arguments
* @param {Object} The environment.
*/
AsyncTokenTransformManager.prototype.setFrame = function ( parentFrame, title, args ) {
// First piggy-back some reset action
this.tailAccumulator = undefined;
// initial top-level callback, emits chunks
this.tokenCB = this._returnTokens.bind( this );
// now actually set up the frame
if (parentFrame) {
if ( title === null ) {
// attribute, simply reuse the parent frame
this.frame = parentFrame;
} else {
this.frame = parentFrame.newChild( title, this, args );
}
} else {
this.frame = new Frame(title, this, args );
}
};
/**
* Simplified wrapper that processes all tokens passed in
*/
AsyncTokenTransformManager.prototype.process = function ( tokens ) {
if ( ! $.isArray ( tokens ) ) {
tokens = [tokens];
}
this.onChunk( tokens );
this.onEndEvent();
};
/**
* Transform and expand tokens. Transformed token chunks will be emitted in
* the 'chunk' event.
*
* @method
* @param {Array} chunk of tokens
*/
AsyncTokenTransformManager.prototype.onChunk = function ( tokens ) {
// Set top-level callback to next transform phase
var res = this.transformTokens ( tokens, this.tokenCB );
this.env.dp( 'AsyncTokenTransformManager onChunk res=', res );
if ( ! this.tailAccumulator ) {
this.emit( 'chunk', res.tokens );
} else {
this.tailAccumulator.append( res.tokens );
}
if ( res.async ) {
this.tailAccumulator = res.async;
this.tokenCB = res.async.getParentCB ( 'sibling' );
}
};
/**
* Run transformations from phases 0 and 1. This includes starting and
* managing asynchronous transformations.
*
*/
AsyncTokenTransformManager.prototype.transformTokens = function ( tokens, parentCB ) {
//console.warn('AsyncTokenTransformManager.transformTokens: ' + JSON.stringify(tokens) );
var res,
localAccum = [],
activeAccum = localAccum,
transforming = true,
childAccum = null,
accum = new TokenAccumulator( this, parentCB ),
token, ts, transformer, aborted,
maybeSyncReturn = function ( asyncCB, ret ) {
if ( transforming ) {
this.env.dp( 'maybeSyncReturn transforming', ret );
// transformTokens is ongoing
if ( false && ret.tokens && ! ret.async && ret.allTokensProcessed && ! childAccum ) {
localAccum.push.apply(localAccum, res.tokens );
} else if ( ret.tokens ) {
if ( res.tokens ) {
res.tokens = res.tokens.concat( ret.tokens );
res.async = ret.async;
} else {
res = ret;
}
} else {
if ( ! res.tokens ) {
res = ret;
} else {
res.async = ret.async;
}
}
} else {
this.env.dp( 'maybeSyncReturn async', ret );
asyncCB( ret );
}
},
cb = maybeSyncReturn.bind( this, accum.getParentCB( 'child' ) ),
minRank;
for ( var i = 0, l = tokens.length; i < l; i++ ) {
token = tokens[i];
minRank = token.rank || 0;
aborted = false;
ts = this._getTransforms( token );
if ( ts.length ) {
//this.env.tp( 'async trans' );
this.env.dp( token, ts );
for (var j = 0, lts = ts.length; j < lts; j++ ) {
res = { };
transformer = ts[j];
if ( minRank && transformer.rank < minRank ) {
// skip transformation, was already applied.
//console.warn( 'skipping transform');
res.token = token;
continue;
}
// Transform the token.
transformer.transform( token, this.frame, cb );
// Check the result, which is changed using the
// maybeSyncReturn callback
if ( res.token === undefined ) {
if ( res.tokens && res.tokens.length === 1 &&
token === res.tokens[0] )
{
res.token = token;
} else {
aborted = true;
break;
}
}
minRank = transformer.rank;
token = res.token;
}
} else {
res = { token: token };
}
if ( ! aborted ) {
res.token = this.env.setTokenRank( this.phaseEndRank, res.token );
// token is done.
// push to accumulator
activeAccum.push( res.token );
continue;
} else if( res.tokens ) {
// Splice in the returned tokens (while replacing the original
// token), and process them next.
//if ( ! res.allTokensProcessed ) {
[].splice.apply( tokens, [i, 1].concat(res.tokens) );
l = tokens.length;
if ( res.allTokensProcessed ) {
i += res.tokens.length - 1;
} else {
i--; // continue at first inserted token
}
}
if ( res.async ) {
this.env.dp( 'res.async' );
// The child now switched to activeAccum, we have to create a new
// accumulator for the next potential child.
activeAccum = accum;
childAccum = accum;
accum = new TokenAccumulator( this, childAccum.getParentCB( 'sibling' ) );
cb = maybeSyncReturn.bind( this, accum.getParentCB( 'child' ) );
}
}
transforming = false;
// Return finished tokens directly to caller, and indicate if further
// async actions are outstanding. The caller needs to point a sibling to
// the returned accumulator, or call .siblingDone() to mark the end of a
// chain.
return { tokens: localAccum, async: childAccum };
};
/**
* Top-level callback for tokens which are now free to be emitted iff they are
* indeed fully processed for sync01 and async12. If there were asynchronous
* expansions, then only the first TokenAccumulator has its callback set to
* this method. An exhausted TokenAccumulator passes its callback on to its
* siblings until the last accumulator is reached, so that the head
* accumulator will always call this method directly.
*
* @method
* @param {Object} tokens, async, allTokensProcessed
* @returns {Mixed} new parent callback for caller or falsy value.
*/
AsyncTokenTransformManager.prototype._returnTokens = function ( ret ) {
//tokens = this._transformPhase2( this.frame, tokens, this.parentCB );
this.env.dp( 'AsyncTokenTransformManager._returnTokens, emitting chunk: ',
ret );
if( !ret.allTokensProcessed ) {
var res = this.transformTokens( ret.tokens, this._returnTokens.bind(this) );
this.emit( 'chunk', res.tokens );
if ( res.async ) {
// XXX: this looks fishy
if ( ! this.tailAccumulator ) {
this.tailAccumulator = res.async;
this.tokenCB = res.async.getParentCB ( 'sibling' );
}
if ( ret.async ) {
// return sibling callback
return this.tokenCB;
} else {
// signal done-ness to last accum
res.async.siblingDone();
}
} else if ( !ret.async ) {
this.emit( 'end' );
// and reset internal state.
//this._reset();
}
} else {
this.emit( 'chunk', ret.tokens );
if ( ! ret.async ) {
//console.trace();
// signal our done-ness to consumers.
this.emit( 'end' );
// and reset internal state.
//this._reset();
}
}
};
/**
* Callback for the end event emitted from the tokenizer.
* Either signals the end of input to the tail of an ongoing asynchronous
* processing pipeline, or directly emits 'end' if the processing was fully
* synchronous.
*/
AsyncTokenTransformManager.prototype.onEndEvent = function () {
if ( this.tailAccumulator ) {
this.env.dp( 'AsyncTokenTransformManager.onEndEvent: calling siblingDone',
this.frame );
this.tailAccumulator.siblingDone();
} else {
// nothing was asynchronous, so we'll have to emit end here.
this.env.dp( 'AsyncTokenTransformManager.onEndEvent: synchronous done',
this.frame );
this.emit('end');
//this._reset();
}
};
/*************** In-order, synchronous transformer (phase 1 and 3) ***************/
/**
* Subclass for phase 3, in-order and synchronous processing.
*
* @class
* @constructor
* @param {Object} environment.
*/
function SyncTokenTransformManager ( env, isInclude, pipeFactory, phaseEndRank, attributeType ) {
this.env = env;
this.isInclude = isInclude;
this.pipeFactory = pipeFactory;
this.phaseEndRank = phaseEndRank;
this.attributeType = attributeType;
this._construct();
}
// Inherit from TokenTransformManager, and thus also from EventEmitter.
SyncTokenTransformManager.prototype = new TokenTransformManager();
SyncTokenTransformManager.prototype.constructor = SyncTokenTransformManager;
SyncTokenTransformManager.prototype.process = function ( tokens ) {
if ( ! $.isArray ( tokens ) ) {
tokens = [tokens];
}
this.onChunk( tokens );
//console.warn( JSON.stringify( this.transformers ) )
this.onEndEvent();
};
/**
* Global in-order and synchronous traversal on token stream. Emits
* transformed chunks of tokens in the 'chunk' event.
*
* @method
* @param {Array} Token chunk.
*/
SyncTokenTransformManager.prototype.onChunk = function ( tokens ) {
this.env.dp( 'SyncTokenTransformManager.onChunk, input: ', tokens );
var res,
localAccum = [],
localAccumLength = 0,
token,
// Top-level frame only in phase 3, as everything is already expanded.
ts, transformer,
aborted;
for ( var i = 0, l = tokens.length; i < l; i++ ) {
aborted = false;
token = tokens[i];
res = { token: token };
ts = this._getTransforms( token );
var minRank = token.rank || 0;
for (var j = 0, lts = ts.length; j < lts; j++ ) {
transformer = ts[j];
if ( transformer.rank < minRank ) {
// skip transformation, was already applied.
//this.env.ap( 'skipping transform', transformer);
continue;
}
// Transform the token.
res = transformer.transform( token, this, this.prevToken );
if ( res.token !== token ) {
aborted = true;
break;
}
minRank = transformer.rank;
token = res.token;
}
if( res.tokens ) {
// Splice in the returned tokens (while replacing the original
// token), and process them next.
[].splice.apply( tokens, [i, 1].concat(res.tokens) );
l = tokens.length;
if ( res.allTokensProcessed ) {
i += res.tokens.length - 1;
} else {
i--; // continue at first inserted token
}
} else if ( res.token ) {
res.token = this.env.setTokenRank( this.phaseEndRank, res.token );
if ( res.token.rank === this.phaseEndRank ) {
// token is done.
localAccum.push(res.token);
this.prevToken = res.token;
} else {
// re-process token.
tokens[i] = res.token;
i--;
}
}
}
this.env.dp( 'SyncTokenTransformManager.onChunk: emitting ', localAccum );
this.emit( 'chunk', localAccum );
};
/**
* Callback for the end event emitted from the tokenizer.
* Either signals the end of input to the tail of an ongoing asynchronous
* processing pipeline, or directly emits 'end' if the processing was fully
* synchronous.
*/
SyncTokenTransformManager.prototype.onEndEvent = function () {
// This phase is fully synchronous, so just pass the end along and prepare
// for the next round.
this.emit('end');
};
/********************** AttributeTransformManager *************************/
/**
* Utility transformation manager for attributes, using an attribute
* transformation pipeline (normally phase1 SyncTokenTransformManager and
* phase2 AsyncTokenTransformManager). This pipeline needs to be independent
* of the containing TokenTransformManager to isolate transforms from each
* other. The AttributeTransformManager returns its result by calling the
* supplied callback.
*
* @class
* @constructor
* @param {Object} Containing AsyncTokenTransformManager
* @param {Function} Callback function, called with expanded attribute array.
*/
function AttributeTransformManager ( manager, callback ) {
this.manager = manager;
this.frame = this.manager.frame;
this.callback = callback;
this.outstanding = 1;
this.kvs = [];
//this.pipe = manager.getAttributePipeline( manager.args );
}
/**
* Expand both key and values of all key/value pairs. Used for generic
* (non-template) tokens in the AttributeExpander handler, which runs after
* templates are already expanded.
*/
AttributeTransformManager.prototype.process = function ( attributes ) {
// Potentially need to use multiple pipelines to support concurrent async expansion
//this.pipe.process(
var pipe,
ref,
idCB = function( format, cb ){ cb( this ); };
//console.warn( 'AttributeTransformManager.process: ' + JSON.stringify( attributes ) );
// transform each argument (key and value), and handle asynchronous returns
for ( var i = 0, l = attributes.length; i < l; i++ ) {
var cur = attributes[i];
// fast path for string-only attributes
if ( cur.k.constructor === String && cur.v.constructor === String ) {
this.kvs.push( cur );
continue;
}
var kv = new KV( [], [] );
this.kvs.push( kv );
if ( cur.k.constructor === Array && cur.k.length ) {
// Assume that the return is async, will be decremented in callback
this.outstanding++;
// transform the key
pipe = this.manager.pipeFactory.getPipeline( this.manager.attributeType,
this.manager.isInclude );
pipe.setFrame( this.manager.frame, null );
pipe.on( 'chunk',
this._returnAttributeKey.bind( this, i, true )
);
pipe.on( 'end',
this._returnAttributeKey.bind( this, i, false, [] )
);
pipe.process( this.manager.env.cloneTokens( cur.k ).concat([ new EOFTk() ]) );
} else {
kv.k = cur.k;
}
if ( cur.v.constructor === Array && cur.v.length ) {
// Assume that the return is async, will be decremented in callback
this.outstanding++;
// transform the value
pipe = this.manager.pipeFactory.getPipeline( this.manager.attributeType,
this.manager.isInclude );
pipe.setFrame( this.manager.frame, null );
//pipe = this.manager.getAttributePipeline( this.manager.inputType,
// this.manager.args );
pipe.on( 'chunk',
this._returnAttributeValue.bind( this, i, true )
);
pipe.on( 'end',
this._returnAttributeValue.bind( this, i, false, [] )
);
//console.warn('starting attribute transform of ' + JSON.stringify( attributes[i].v ) );
pipe.process( this.manager.env.cloneTokens( cur.v ).concat([ new EOFTk() ]) );
} else {
kv.v = cur.v;
}
}
this.outstanding--;
if ( this.outstanding === 0 ) {
// synchronous, done
this.callback( this.kvs );
}
};
/**
* Expand only keys of key/value pairs. This is generally used for template
* parameters to avoid expanding unused values, which is very important for
* constructs like switches.
*/
AttributeTransformManager.prototype.processKeys = function ( attributes ) {
// Potentially need to use multiple pipelines to support concurrent async expansion
//this.pipe.process(
var pipe,
ref;
//console.warn( 'AttributeTransformManager.process: ' + JSON.stringify( attributes ) );
// transform each argument (key and value), and handle asynchronous returns
for ( var i = 0, l = attributes.length; i < l; i++ ) {
var cur = attributes[i];
var kv = new KV([], cur.v);
if ( kv.v.to !== this.manager.frame.convert ) {
if ( kv.v.to ) {
if ( kv.v.constructor === String ) {
kv.v = new String( kv.v );
} else {
kv.v = kv.v.slice();
}
} else if ( kv.v.constructor === String ) {
kv.v = new String( kv.v );
}
Object.defineProperty( kv.v, 'to',
{
value: this.manager.frame.convert,
enumerable: false
});
}
this.kvs.push( kv );
if ( cur.k.constructor === Array && cur.k.length && ! cur.k.to ) {
// Assume that the return is async, will be decremented in callback
this.outstanding++;
// transform the key
pipe = this.manager.pipeFactory.getPipeline( this.manager.attributeType,
this.manager.isInclude );
pipe.setFrame( this.manager.frame, null );
//pipe = this.manager.getAttributePipeline( this.manager.inputType,
// this.manager.args );
pipe.on( 'chunk',
this._returnAttributeKey.bind( this, i, true )
);
pipe.on( 'end',
this._returnAttributeKey.bind( this, i, false, [] )
);
pipe.process( this.manager.env.cloneTokens( cur.k ).concat([ new EOFTk() ]) );
} else {
kv.k = cur.k;
}
}
this.outstanding--;
if ( this.outstanding === 0 ) {
// synchronously done
this.callback( this.kvs );
}
};
/**
* Only expand values of attribute key/value pairs.
*/
AttributeTransformManager.prototype.processValues = function ( attributes ) {
// Potentially need to use multiple pipelines to support concurrent async expansion
//this.pipe.process(
var pipe,
ref;
//console.warn( 'AttributeTransformManager.process: ' + JSON.stringify( attributes ) );
// transform each argument (key and value), and handle asynchronous returns
for ( var i = 0, l = attributes.length; i < l; i++ ) {
var cur = attributes[i];
var kv = new KV( cur.k, [] );
this.kvs.push( kv );
if ( ! cur ) {
console.warn( JSON.stringify( attributes ) );
console.trace();
continue;
}
if ( cur.v.constructor === Array && cur.v.length ) {
// Assume that the return is async, will be decremented in callback
this.outstanding++;
// transform the value
pipe = this.manager.pipeFactory.getPipeline( this.manager.attributeType,
this.manager.isInclude );
pipe.setFrame( this.manager.frame, null );
//pipe = this.manager.getAttributePipeline( this.manager.inputType,
// this.manager.args );
pipe.on( 'chunk',
this._returnAttributeValue.bind( this, i, true )
);
pipe.on( 'end',
this._returnAttributeValue.bind( this, i, false, [] )
);
//console.warn('starting attribute transform of ' + JSON.stringify( attributes[i].v ) );
pipe.process( this.manager.env.cloneTokens( cur.v ).concat([ new EOFTk() ]) );
} else {
kv.value = cur.v;
}
}
this.outstanding--;
if ( this.outstanding === 0 ) {
// synchronously done
this.callback( this.kvs );
}
};
/**
* Callback for async argument value expansions
*/
AttributeTransformManager.prototype._returnAttributeValue = function ( ref, notYetDone, tokens ) {
this.manager.env.dp( 'check _returnAttributeValue: ', ref, tokens,
' notYetDone:', notYetDone );
this.kvs[ref].v = this.kvs[ref].v.concat( tokens );
if ( ! notYetDone ) {
var res = this.kvs[ref].v;
this.manager.env.stripEOFTkfromTokens( res );
this.outstanding--;
// Add the 'to' conversion method to the chunk for easy conversion in
// later processing (parser functions and template argument
// processing).
if ( res.to !== this.manager.frame.convert ) {
if ( res.to ) {
if ( res.constructor === String ) {
res = new String( res );
} else {
res = res.slice();
}
} else if ( res.constructor === String ) {
res = new String( res );
}
Object.defineProperty( res, 'to',
{
value: function( format, cb ) { cb( this ); },
enumerable: false
});
}
if ( this.outstanding === 0 ) {
this.callback( this.kvs );
}
}
};
/**
* Callback for async argument key expansions
*/
AttributeTransformManager.prototype._returnAttributeKey = function ( ref, notYetDone, tokens ) {
//console.warn( 'check _returnAttributeKey: ' + JSON.stringify( tokens ) +
// ' notYetDone:' + notYetDone );
this.kvs[ref].k = this.kvs[ref].k.concat( tokens );
if ( ! notYetDone ) {
this.manager.env.stripEOFTkfromTokens( this.kvs[ref].k );
this.outstanding--;
if ( this.outstanding === 0 ) {
this.callback( this.kvs );
}
}
};
/******************************* TokenAccumulator *************************/
/**
* Token accumulators buffer tokens between asynchronous processing points,
* and return fully processed token chunks in-order and as soon as possible.
* They support the AsyncTokenTransformManager.
*
* @class
* @constructor
* @param {Object} next TokenAccumulator to link to
* @param {Array} (optional) tokens, init accumulator with tokens or []
*/
function TokenAccumulator ( manager, parentCB ) {
this.manager = manager;
this.parentCB = parentCB;
this.accum = [];
// Wait for child and sibling by default
// Note: Need to decrement outstanding on last accum
// in a chain.
this.outstanding = 2;
}
/**
* Curry a parentCB with the object and reference.
*
* @method
* @param {Object} TokenAccumulator
* @param {misc} Reference / key for callback
* @returns {Function}
*/
TokenAccumulator.prototype.getParentCB = function ( reference ) {
return this._returnTokens.bind( this, reference );
};
/**
* Pass tokens to an accumulator
*
* @method
* @param {String}: reference, 'child' or 'sibling'.
* @param {Object}: { tokens, async, allTokensProcessed }
* @returns {Mixed}: new parent callback for caller or falsy value
*/
TokenAccumulator.prototype._returnTokens =
function ( reference, ret ) {
var cb,
returnTokens = [];
if ( ! ret.async ) {
this.outstanding--;
}
this.manager.env.dp( 'TokenAccumulator._returnTokens', ret );
// FIXME
if ( ret.tokens === undefined ) {
if ( this.manager.env.debug ) {
console.trace();
}
if ( ret.token ) {
ret.tokens = [ret.token];
} else {
ret.tokens = [];
}
}
if ( reference === 'child' ) {
var res = {};
if( !ret.allTokensProcessed ) {
// There might be transformations missing on the returned tokens,
// re-transform to make sure those are applied too.
res = this.manager.transformTokens( ret.tokens, this.parentCB );
ret.tokens = res.tokens;
}
if ( !ret.async ) {
// empty accum too
ret.tokens = ret.tokens.concat( this.accum );
this.accum = [];
}
//this.manager.env.dp( 'TokenAccumulator._returnTokens child: ',
// tokens, ' outstanding: ', this.outstanding );
ret.allTokensProcessed = true;
ret.async = this.outstanding;
this.parentCB( ret );
if ( res.async ) {
this.parentCB = res.async.getParentCB( 'sibling' );
}
return null;
} else {
// sibling
if ( this.outstanding === 0 ) {
ret.tokens = this.accum.concat( ret.tokens );
// A sibling will transform tokens, so we don't have to do this
// again.
//this.manager.env.dp( 'TokenAccumulator._returnTokens: ',
// 'sibling done and parentCB ',
// tokens );
ret.allTokensProcessed = true;
ret.async = false;
this.parentCB( ret );
return null;
} else if ( this.outstanding === 1 && ret.async ) {
//this.manager.env.dp( 'TokenAccumulator._returnTokens: ',
// 'sibling done and parentCB but async ',
// tokens );
// Sibling is not yet done, but child is. Return own parentCB to
// allow the sibling to go direct, and call back parent with
// tokens. The internal accumulator is empty at this stage, as its
// tokens are passed to the parent when the child is done.
ret.allTokensProcessed = true;
return this.parentCB( ret );
} else {
this.accum = this.accum.concat( ret.tokens );
//this.manager.env.dp( 'TokenAccumulator._returnTokens: sibling done, but not overall. async=',
// res.async, ', this.outstanding=', this.outstanding,
// ', this.accum=', this.accum, ' manager.title=', this.manager.title );
}
}
};
/**
* Mark the sibling as done (normally at the tail of a chain).
*/
TokenAccumulator.prototype.siblingDone = function () {
//console.warn( 'TokenAccumulator.siblingDone: ' );
this._returnTokens ( 'sibling', { tokens: [], async: false, allTokensProcessed: true} );
};
/**
* Push a token into the accumulator
*
* @method
* @param {Object} token
*/
TokenAccumulator.prototype.push = function ( token ) {
return this.accum.push(token);
};
/**
* Append tokens to an accumulator
*
* @method
* @param {Object} token
*/
TokenAccumulator.prototype.append = function ( token ) {
this.accum = this.accum.concat( token );
};
/******************************* Frame ******************************/
/**
* The Frame object
*
* A frame represents a template expansion scope including parameters passed
* to the template (args). It provides a generic 'convert' method which
* expands / converts individual parameter values in its scope. It also
* provides methods to check if another expansion would lead to loops or
* exceed the maximum expansion depth.
*/
function Frame ( title, manager, args, parentFrame ) {
this.title = title;
this.manager = manager;
this.args = new Params( this.manager.env, args );
if ( parentFrame ) {
this.parentFrame = parentFrame;
this.depth = parentFrame.depth + 1;
} else {
this.parentFrame = null;
this.depth = 0;
}
var self = this;
this.convert = function ( format, cb, parentCB ) {
self._convertThunk( this, format, cb, parentCB );
};
}
/**
* Create a new child frame
*/
Frame.prototype.newChild = function ( title, manager, args ) {
return new Frame( title, manager, args, this );
};
/**
* Expand / convert a thunk (a chunk of tokens not yet fully expanded).
*/
Frame.prototype._convertThunk = function ( chunk, format, cb, parentCB ) {
this.manager.env.dp( 'convertChunk', chunk );
if ( chunk.constructor === String ) {
// Plain text remains text. Nothing to do.
return cb( chunk );
}
// Set up a conversion cache on the chunk, if it does not yet exist
if ( chunk.toCache === undefined ) {
Object.defineProperty( chunk, 'toCache', { value: {}, enumerable: false } );
} else {
if ( chunk.toCache[format] !== undefined ) {
cb( chunk.toCache[format] );
return;
}
}
if ( format === 'text/plain/expanded' ) {
// Simply wrap normal expansion ;)
// XXX: Integrate this into the pipeline setup?
format = 'tokens/x-mediawiki/expanded';
var self = this,
origCB = cb;
cb = function( resChunk ) {
var res = self.manager.env.tokensToString( resChunk );
// cache the result
chunk.toCache['text/plain/expanded'] = res;
origCB( res );
};
}
// XXX: Should perhaps create a generic from..to conversion map in
// mediawiki.parser.js, at least for forward conversions.
if ( format === 'tokens/x-mediawiki/expanded' ) {
if ( parentCB ) {
// Signal (potentially) asynchronous expansion to parent.
// If
parentCB( { async: true } );
}
var pipeline = this.manager.pipeFactory.getPipeline(
this.manager.attributeType || 'tokens/x-mediawiki', true
);
pipeline.setFrame( this, null );
// In the interest of interface simplicity, we accumulate all emitted
// chunks in a single accumulator.
var accum = [];
// Callback used to cache the result of the conversion
var cacheIt = function ( res ) { chunk.toCache[format] = res; };
pipeline.addListener( 'chunk',
this.onThunkEvent.bind( this, cacheIt, accum, true, cb ) );
pipeline.addListener( 'end',
this.onThunkEvent.bind( this, cacheIt, accum, false, cb ) );
pipeline.process( chunk.concat( [new EOFTk()] ), this.title );
} else {
throw "Frame._convertThunk: Unsupported format " + format;
}
};
/**
* Event handler for chunk conversion pipelines
*/
Frame.prototype.onThunkEvent = function ( cacheIt, accum, notYetDone, cb, ret ) {
if ( notYetDone ) {
//this.manager.env.dp( 'Frame.onThunkEvent accum:', accum );
accum.push.apply( accum, ret );
} else {
this.manager.env.stripEOFTkfromTokens( accum );
this.manager.env.dp( 'Frame.onThunkEvent:', accum );
cacheIt( accum );
cb ( accum );
}
};
/**
* Check if expanding <title> would lead to a loop, or would exceed the
* maximum expansion depth.
*
* @method
* @param {String} Title to check.
*/
Frame.prototype.loopAndDepthCheck = function ( title, maxDepth ) {
// XXX: set limit really low for testing!
//console.warn( 'Loopcheck: ' + title + JSON.stringify( this, null, 2 ) );
if ( this.depth > maxDepth ) {
// too deep
//console.warn( 'Loopcheck: ' + JSON.stringify( this, null, 2 ) );
return 'Error: Expansion depth limit exceeded at ';
}
var elem = this;
do {
//console.warn( 'loop check: ' + title + ' vs ' + elem.title );
if ( elem.title === title ) {
// Loop detected
return 'Error: Expansion loop detected at ';
}
elem = elem.parentFrame;
} while ( elem );
// No loop detected.
return false;
};
if (typeof module == "object") {
module.exports.AsyncTokenTransformManager = AsyncTokenTransformManager;
module.exports.SyncTokenTransformManager = SyncTokenTransformManager;
module.exports.AttributeTransformManager = AttributeTransformManager;
}