mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-15 18:39:52 +00:00
6e21f6bb27
* Adapted Cite extension to use current interfaces and token formats * Improved TokenCollector Change-Id: I20419b19edd9bbad2c2abf17a2ff1411b99c0c04
1159 lines
34 KiB
JavaScript
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;
|
|
}
|