2011-12-08 14:37:31 +00:00
|
|
|
/* Generic token transformation dispatcher with support for asynchronous token
|
|
|
|
* expansion. Individual transformations register for the token types they are
|
2011-12-12 10:01:47 +00:00
|
|
|
* interested in and are called on each matching token.
|
|
|
|
*
|
2012-01-03 18:44:31 +00:00
|
|
|
* See
|
|
|
|
* https://www.mediawiki.org/wiki/Future/Parser_development/Token_stream_transformations
|
|
|
|
* for more documentation.
|
2011-12-14 09:33:25 +00:00
|
|
|
*
|
2012-01-03 18:44:31 +00:00
|
|
|
* @author Gabriel Wicke <gwicke@wikimedia.org>
|
|
|
|
*/
|
2011-12-08 14:37:31 +00:00
|
|
|
|
2012-01-03 18:44:31 +00:00
|
|
|
var events = require('events');
|
2011-12-28 01:37:06 +00:00
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
|
|
|
* Central dispatcher for potentially asynchronous token transformations.
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @constructor
|
|
|
|
* @param {Function} callback, a callback function accepting a token list as
|
|
|
|
* its only argument.
|
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
function TokenTransformDispatcher( ) {
|
2011-12-08 14:37:31 +00:00
|
|
|
this.transformers = {
|
2012-01-03 18:44:31 +00:00
|
|
|
// phase 0 and 1, rank 2 marks tokens as fully processed for these
|
|
|
|
// phases.
|
|
|
|
2: {
|
|
|
|
tag: {}, // for TAG, ENDTAG, SELFCLOSINGTAG, 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
|
|
|
|
},
|
|
|
|
// phase 3, with ranks >= 2 but < 3. 3 marks tokens as fully
|
|
|
|
// processed.
|
|
|
|
3: {
|
|
|
|
tag: {}, // for TAG, ENDTAG, SELFCLOSINGTAG, 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
|
|
|
|
}
|
2011-12-08 14:37:31 +00:00
|
|
|
};
|
2011-12-12 14:03:54 +00:00
|
|
|
this.reset();
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
|
|
|
|
2012-01-03 18:44:31 +00:00
|
|
|
// Inherit from EventEmitter
|
|
|
|
TokenTransformDispatcher.prototype = new events.EventEmitter();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register to a token source, normally the tokenizer.
|
|
|
|
* The event emitter emits an 'tokens' event which contains a chunk of tokens,
|
|
|
|
* and signals the end of tokens by triggering the 'end' event.
|
|
|
|
*
|
|
|
|
* @param {Object} EventEmitter token even emitter.
|
|
|
|
*/
|
|
|
|
TokenTransformDispatcher.prototype.subscribeToTokenEmitter = function ( tokenEmitter ) {
|
|
|
|
tokenEmitter.addListener('chunk', this.transformTokens.bind( this ) );
|
|
|
|
tokenEmitter.addListener('end', this.onEndEvent.bind( this ) );
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2011-12-14 09:33:25 +00:00
|
|
|
* Reset the internal token and outstanding-callback state of the
|
2011-12-13 20:13:09 +00:00
|
|
|
* TokenTransformDispatcher, but keep registrations untouched.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenTransformDispatcher.prototype.reset = function ( env ) {
|
|
|
|
this.tailAccumulator = undefined;
|
|
|
|
this.phase2TailCB = this.returnTokens01.bind( this );
|
2011-12-12 20:53:14 +00:00
|
|
|
this.accum = new TokenAccumulator(null);
|
2011-12-12 14:03:54 +00:00
|
|
|
this.firstaccum = this.accum;
|
2012-01-03 18:44:31 +00:00
|
|
|
this.prevToken = undefined;
|
|
|
|
this.frame = {
|
|
|
|
args: {}, // no arguments at the top level
|
|
|
|
env: this.env
|
|
|
|
};
|
|
|
|
// Should be as static as possible re this and frame
|
|
|
|
// This is circular, but that should not really matter for non-broken GCs
|
|
|
|
// that handle pure JS ref loops.
|
|
|
|
this.frame.transformPhase = this.transformPhase01.bind( this, this.frame );
|
|
|
|
};
|
|
|
|
|
|
|
|
TokenTransformDispatcher.prototype._rankToPhase = function ( rank ) {
|
|
|
|
if ( rank < 0 || rank > 3 ) {
|
|
|
|
throw "TransformDispatcher error: Invalid transformation rank " + rank;
|
|
|
|
}
|
|
|
|
if ( rank <= 2 ) {
|
|
|
|
return 2;
|
|
|
|
} else {
|
|
|
|
return 3;
|
|
|
|
}
|
2011-12-12 14:03:54 +00:00
|
|
|
};
|
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2012-01-03 18:44:31 +00:00
|
|
|
* Add a transform registration.
|
2011-12-13 20:13:09 +00:00
|
|
|
*
|
|
|
|
* @method
|
2012-01-03 18:44:31 +00:00
|
|
|
* @param {Function} transform.
|
2011-12-13 20:13:09 +00:00
|
|
|
* @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
|
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenTransformDispatcher.prototype.addTransform = function ( transformation, rank, type, name ) {
|
|
|
|
var phase = this._rankToPhase( rank ),
|
|
|
|
transArr,
|
|
|
|
transformer = {
|
|
|
|
transform: transformation,
|
|
|
|
rank: rank
|
|
|
|
};
|
2011-12-08 14:37:31 +00:00
|
|
|
if ( type === 'tag' ) {
|
2011-12-13 14:48:47 +00:00
|
|
|
name = name.toLowerCase();
|
2012-01-03 18:44:31 +00:00
|
|
|
transArr = this.transformers[phase].tag[name];
|
|
|
|
if ( ! transArr ) {
|
|
|
|
transArr = this.transformers[phase].tag[name] = [];
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
|
|
|
} else {
|
2012-01-03 18:44:31 +00:00
|
|
|
transArr = this.transformers[phase][type];
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
2012-01-03 18:44:31 +00:00
|
|
|
transArr.push(transformer);
|
|
|
|
// sort ascending by rank
|
|
|
|
transArr.sort( function ( t1, t2 ) { return t1.rank - t2.rank; } );
|
2011-12-08 14:37:31 +00:00
|
|
|
};
|
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2012-01-03 18:44:31 +00:00
|
|
|
* Remove a transform registration
|
2011-12-13 20:13:09 +00:00
|
|
|
*
|
|
|
|
* @method
|
2012-01-03 18:44:31 +00:00
|
|
|
* @param {Number} rank, the numeric rank of the handler.
|
2011-12-13 20:13:09 +00:00
|
|
|
* @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
|
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenTransformDispatcher.prototype.removeTransform = function ( rank, type, name ) {
|
|
|
|
var i = -1,
|
|
|
|
phase = this._rankToPhase( rank ),
|
|
|
|
ts;
|
|
|
|
|
|
|
|
function rankUnEqual ( i ) {
|
|
|
|
return i.rank !== rank;
|
|
|
|
}
|
|
|
|
|
2011-12-08 14:37:31 +00:00
|
|
|
if ( type === 'tag' ) {
|
2011-12-13 14:48:47 +00:00
|
|
|
name = name.toLowerCase();
|
2012-01-03 18:44:31 +00:00
|
|
|
var maybeTransArr = this.transformers[phase].tag.name;
|
|
|
|
if ( maybeTransArr ) {
|
|
|
|
this.transformers[phase].tag.name = maybeTransArr.filter( rankUnEqual );
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
|
|
|
} else {
|
2012-01-03 18:44:31 +00:00
|
|
|
this.transformers[phase][type] = this.transformers[phase][type].filter( rankUnEqual ) ;
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2012-01-03 18:44:31 +00:00
|
|
|
* Enforce separation between phases when token types or tag names have
|
|
|
|
* changed, or when multiple tokens were returned. Processing will restart
|
|
|
|
* with the new rank.
|
2011-12-13 20:13:09 +00:00
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenTransformDispatcher.prototype._resetTokenRank = function ( res, transformer ) {
|
|
|
|
if ( res.token ) {
|
|
|
|
// reset rank after type or name change
|
|
|
|
if ( transformer.rank < 1 ) {
|
|
|
|
res.token.rank = 0;
|
|
|
|
} else {
|
|
|
|
res.token.rank = 1;
|
|
|
|
}
|
|
|
|
} else if ( res.tokens && transformer.rank > 2 ) {
|
|
|
|
for ( var i = 0; i < res.tokens.length; i++ ) {
|
|
|
|
if ( res.tokens[i].rank === undefined ) {
|
|
|
|
// Do not run phase 0 on newly created tokens from
|
|
|
|
// phase 1.
|
|
|
|
res.tokens[i].rank = 2;
|
|
|
|
}
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-12-12 10:01:47 +00:00
|
|
|
|
|
|
|
/* Call all transformers on a tag.
|
|
|
|
*
|
2012-01-03 18:44:31 +00:00
|
|
|
* @param {Object} The current token.
|
|
|
|
* @param {Function} Completion callback for async processing.
|
|
|
|
* @param {Number} Rank of phase end, both key for transforms and rank for
|
|
|
|
* processed tokens.
|
|
|
|
* @param {Object} The frame, contains a reference to the environment.
|
|
|
|
* @returns {Object} Token(s) and async indication.
|
2011-12-12 10:01:47 +00:00
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenTransformDispatcher.prototype._transformTagToken = function ( token, cb, phaseEndRank, frame ) {
|
2011-12-13 14:48:47 +00:00
|
|
|
// prepend 'any' transformers
|
2012-01-03 18:44:31 +00:00
|
|
|
var ts = this.transformers[phaseEndRank].any,
|
|
|
|
res = { token: token },
|
|
|
|
transform,
|
|
|
|
l, i,
|
|
|
|
aborted = false,
|
|
|
|
tName = token.name.toLowerCase(),
|
|
|
|
tagts = this.transformers[phaseEndRank].tag[tName];
|
|
|
|
|
2011-12-13 14:48:47 +00:00
|
|
|
if ( tagts ) {
|
|
|
|
ts = ts.concat(tagts);
|
|
|
|
}
|
|
|
|
//console.log(JSON.stringify(ts, null, 2));
|
2011-12-08 14:37:31 +00:00
|
|
|
if ( ts ) {
|
2012-01-03 18:44:31 +00:00
|
|
|
for ( i = 0, l = ts.length; i < l; i++ ) {
|
|
|
|
transformer = ts[i];
|
|
|
|
if ( res.token.rank && transformer.rank <= res.token.rank ) {
|
|
|
|
// skip transformation, was already applied.
|
|
|
|
continue;
|
|
|
|
}
|
2011-12-12 10:01:47 +00:00
|
|
|
// Transform token with side effects
|
2012-01-03 18:44:31 +00:00
|
|
|
res = transformer.transform( res.token, cb, frame, this.prevToken );
|
|
|
|
// if multiple tokens or null token: process returned tokens (in parent)
|
|
|
|
if ( !res.token || // async implies tokens instead of token, so no
|
|
|
|
// need to check explicitly
|
|
|
|
res.token.type !== token.type ||
|
|
|
|
res.token.name !== token.name ) {
|
|
|
|
this._resetTokenRank ( res, transformer );
|
|
|
|
aborted = true;
|
2011-12-08 14:37:31 +00:00
|
|
|
break;
|
|
|
|
}
|
2012-01-03 18:44:31 +00:00
|
|
|
// track progress on token
|
|
|
|
res.token.rank = transformer.rank;
|
|
|
|
}
|
|
|
|
if ( ! aborted ) {
|
|
|
|
// Mark token as fully processed.
|
|
|
|
res.token.rank = phaseEndRank;
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
|
|
|
}
|
2012-01-03 18:44:31 +00:00
|
|
|
return res;
|
2011-12-08 14:37:31 +00:00
|
|
|
};
|
|
|
|
|
2011-12-22 11:43:55 +00:00
|
|
|
/* Call all transformers on non-tag token types.
|
2011-12-12 10:01:47 +00:00
|
|
|
*
|
2012-01-03 18:44:31 +00:00
|
|
|
* @param {Object} The current token.
|
|
|
|
* @param {Function} Completion callback for async processing.
|
|
|
|
* @param {Number} Rank of phase end, both key for transforms and rank for
|
|
|
|
* processed tokens.
|
|
|
|
* @param {Object} The frame, contains a reference to the environment.
|
|
|
|
* @param {Array} ts List of token transformers for this token type.
|
|
|
|
* @returns {Object} Token(s) and async indication.
|
2011-12-12 10:01:47 +00:00
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenTransformDispatcher.prototype._transformToken = function ( token, cb, phaseEndRank, frame, ts ) {
|
2011-12-13 14:48:47 +00:00
|
|
|
// prepend 'any' transformers
|
2012-01-03 18:44:31 +00:00
|
|
|
ts = this.transformers[phaseEndRank].any.concat(ts);
|
|
|
|
var transformer,
|
|
|
|
res = { token: token },
|
|
|
|
aborted = false;
|
2011-12-08 14:37:31 +00:00
|
|
|
if ( ts ) {
|
|
|
|
for (var i = 0, l = ts.length; i < l; i++ ) {
|
2012-01-03 18:44:31 +00:00
|
|
|
transformer = ts[i];
|
|
|
|
if ( res.token.rank && transformer.rank <= res.token.rank ) {
|
|
|
|
// skip transformation, was already applied.
|
|
|
|
continue;
|
|
|
|
}
|
2011-12-13 14:48:47 +00:00
|
|
|
// Transform token with side effects
|
2012-01-03 18:44:31 +00:00
|
|
|
// XXX: it should be a better idea to move the token.rank out of
|
|
|
|
// token and into a wrapper object to ensure that transformations
|
|
|
|
// don't mess with it!
|
|
|
|
res = transformer.transform( res.token, cb, frame, this.prevToken );
|
|
|
|
if ( !res.token ||
|
|
|
|
res.token.type !== token.type ) {
|
|
|
|
this._resetTokenRank ( res, transformer );
|
|
|
|
aborted = true;
|
2011-12-08 14:37:31 +00:00
|
|
|
break;
|
|
|
|
}
|
2012-01-03 18:44:31 +00:00
|
|
|
res.token.rank = transformer.rank;
|
|
|
|
}
|
|
|
|
if ( ! aborted ) {
|
|
|
|
// mark token as completely processed
|
|
|
|
res.token.rank = phaseEndRank; // need phase passed in!
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
2012-01-03 18:44:31 +00:00
|
|
|
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
2012-01-03 18:44:31 +00:00
|
|
|
return res;
|
2011-12-08 14:37:31 +00:00
|
|
|
};
|
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
|
|
|
* Transform and expand tokens.
|
2011-12-12 10:01:47 +00:00
|
|
|
*
|
2012-01-03 18:44:31 +00:00
|
|
|
* Callback for token chunks emitted from the tokenizer.
|
2011-12-13 20:13:09 +00:00
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenTransformDispatcher.prototype.transformTokens = function ( tokens ) {
|
|
|
|
//console.log('TokenTransformDispatcher transformTokens');
|
|
|
|
var res = this.transformPhase01 ( this.frame, tokens, this.phase2TailCB );
|
|
|
|
this.phase2TailCB( tokens, true );
|
|
|
|
if ( res.async ) {
|
|
|
|
this.tailAccumulator = res.async;
|
|
|
|
this.phase2TailCB = res.async.getParentCB ( 'sibling' );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback for the event emitted from the tokenizer.
|
|
|
|
*
|
|
|
|
* This simply decrements the outstanding counter on the top-level
|
|
|
|
*/
|
|
|
|
TokenTransformDispatcher.prototype.onEndEvent = function () {
|
|
|
|
if ( this.tailAccumulator ) {
|
|
|
|
this.tailAccumulator.siblingDone();
|
|
|
|
} else {
|
|
|
|
// nothing was asynchronous, so we'll have to emit end here.
|
|
|
|
this.emit('end');
|
2011-12-12 10:01:47 +00:00
|
|
|
}
|
2012-01-03 18:44:31 +00:00
|
|
|
};
|
2011-12-12 20:53:14 +00:00
|
|
|
|
2012-01-03 18:44:31 +00:00
|
|
|
/**
|
|
|
|
* add parent, parentref args
|
|
|
|
* return
|
|
|
|
* {tokens: [tokens], async: true}: async expansion -> outstanding++ in parent
|
|
|
|
* {tokens: [tokens], async: false}: fully expanded
|
|
|
|
* {token: {token}}: single-token return
|
|
|
|
* child after first expand (example: template expanded)
|
|
|
|
* return some finished tokens, reuse parent accumulator
|
|
|
|
* if new accumulator: set parent, ref
|
|
|
|
*/
|
2011-12-12 20:53:14 +00:00
|
|
|
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenTransformDispatcher.prototype.transformPhase01 = function ( frame, tokens, parentCB ) {
|
|
|
|
|
|
|
|
//console.log('transformPhase01: ' + JSON.stringify(tokens) );
|
|
|
|
|
|
|
|
var res,
|
|
|
|
phaseEndRank = 2,
|
|
|
|
// Prepare a new accumulator, to be used by async children (if any)
|
|
|
|
localAccum = [],
|
|
|
|
accum = new TokenAccumulator( parentCB ),
|
|
|
|
cb = accum.getParentCB( 'child' ),
|
|
|
|
activeAccum = null,
|
|
|
|
tokensLength = tokens.length,
|
|
|
|
token,
|
|
|
|
ts = this.transformers[phaseEndRank];
|
|
|
|
|
|
|
|
for ( var i = 0; i < tokensLength; i++ ) {
|
|
|
|
token = tokens[i];
|
|
|
|
|
|
|
|
switch( token.type ) {
|
2011-12-08 14:37:31 +00:00
|
|
|
case 'TAG':
|
|
|
|
case 'ENDTAG':
|
|
|
|
case 'SELFCLOSINGTAG':
|
2012-01-03 18:44:31 +00:00
|
|
|
res = this._transformTagToken( token, cb, phaseEndRank, frame );
|
2011-12-08 14:37:31 +00:00
|
|
|
break;
|
|
|
|
case 'TEXT':
|
2012-01-03 18:44:31 +00:00
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame, ts.text );
|
2011-12-08 14:37:31 +00:00
|
|
|
break;
|
|
|
|
case 'COMMENT':
|
2012-01-03 18:44:31 +00:00
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame, ts.comment);
|
2011-12-08 14:37:31 +00:00
|
|
|
break;
|
|
|
|
case 'NEWLINE':
|
2012-01-03 18:44:31 +00:00
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame, ts.newline );
|
2011-12-08 14:37:31 +00:00
|
|
|
break;
|
|
|
|
case 'END':
|
2012-01-03 18:44:31 +00:00
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame, ts.end );
|
2011-12-08 14:37:31 +00:00
|
|
|
break;
|
|
|
|
default:
|
2012-01-03 18:44:31 +00:00
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame, ts.martian );
|
2011-12-08 14:37:31 +00:00
|
|
|
break;
|
|
|
|
}
|
2012-01-03 18:44:31 +00:00
|
|
|
|
|
|
|
if( res.tokens ) {
|
2011-12-12 10:01:47 +00:00
|
|
|
// Splice in the returned tokens (while replacing the original
|
|
|
|
// token), and process them next.
|
2012-01-03 18:44:31 +00:00
|
|
|
[].splice.apply( tokens, [i, 1].concat(res.tokens) );
|
|
|
|
tokensLength = tokens.length;
|
2011-12-12 10:01:47 +00:00
|
|
|
i--; // continue at first inserted token
|
2012-01-03 18:44:31 +00:00
|
|
|
} else if ( res.token ) {
|
|
|
|
if ( res.token.rank === 2 ) {
|
|
|
|
// token is done.
|
|
|
|
if ( activeAccum ) {
|
|
|
|
// push to accumulator
|
|
|
|
activeAccum.push( res.token );
|
|
|
|
} else {
|
|
|
|
// If there is no accumulator yet, then directly return the
|
|
|
|
// token to the parent. Collect them in localAccum for this
|
|
|
|
// purpose.
|
|
|
|
localAccum.push(res.token);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// re-process token.
|
|
|
|
tokens[i] = res.token;
|
|
|
|
i--;
|
|
|
|
}
|
|
|
|
} else if ( res.async ) {
|
|
|
|
// The child now switched to activeAccum, we have to create a new
|
|
|
|
// accumulator for the next potential child.
|
|
|
|
activeAccum = accum;
|
|
|
|
accum = new TokenAccumulator( activeAccum.getParentCB( 'sibling' ) );
|
|
|
|
cb = accum.getParentCB( 'child' );
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
2011-12-12 20:53:14 +00:00
|
|
|
}
|
|
|
|
|
2012-01-03 18:44:31 +00:00
|
|
|
// 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: activeAccum };
|
2011-12-08 14:37:31 +00:00
|
|
|
};
|
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2012-01-03 18:44:31 +00:00
|
|
|
* Callback from tokens fully processed for phase 0 and 1, which are now ready
|
|
|
|
* for synchronous and globally in-order phase 2 processing.
|
2011-12-13 20:13:09 +00:00
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenTransformDispatcher.prototype.returnTokens01 = function ( tokens, notYetDone ) {
|
|
|
|
// FIXME: store frame in object?
|
|
|
|
tokens = this.transformPhase2( this.frame, tokens, this.parentCB );
|
|
|
|
//console.log('returnTokens01, after transformPhase2.');
|
|
|
|
|
|
|
|
this.emit( 'chunk', tokens );
|
|
|
|
|
|
|
|
if ( ! notYetDone ) {
|
|
|
|
console.log('returnTokens01 done.');
|
|
|
|
// signal our done-ness to consumers.
|
|
|
|
this.emit( 'end' );
|
|
|
|
// and reset internal state.
|
|
|
|
this.reset();
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2012-01-03 18:44:31 +00:00
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2012-01-03 18:44:31 +00:00
|
|
|
* Phase 2
|
2011-12-13 20:13:09 +00:00
|
|
|
*
|
2012-01-03 18:44:31 +00:00
|
|
|
* Global in-order traversal on expanded token stream (after async phase 1).
|
|
|
|
* Very similar to transformPhase01, but without async handling.
|
|
|
|
*/
|
|
|
|
TokenTransformDispatcher.prototype.transformPhase2 = function ( frame, tokens, cb ) {
|
|
|
|
var res,
|
|
|
|
phaseEndRank = 3,
|
|
|
|
localAccum = [],
|
|
|
|
localAccumLength = 0,
|
|
|
|
tokensLength = tokens.length,
|
|
|
|
token,
|
|
|
|
ts = this.transformers[phaseEndRank];
|
|
|
|
|
|
|
|
for ( var i = 0; i < tokensLength; i++ ) {
|
|
|
|
token = tokens[i];
|
|
|
|
|
|
|
|
switch( token.type ) {
|
|
|
|
case 'TAG':
|
|
|
|
case 'ENDTAG':
|
|
|
|
case 'SELFCLOSINGTAG':
|
|
|
|
res = this._transformTagToken( token, cb, phaseEndRank,
|
|
|
|
frame );
|
|
|
|
break;
|
|
|
|
case 'TEXT':
|
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame,
|
|
|
|
ts.text );
|
|
|
|
break;
|
|
|
|
case 'COMMENT':
|
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame,
|
|
|
|
ts.comment );
|
|
|
|
break;
|
|
|
|
case 'NEWLINE':
|
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame,
|
|
|
|
ts.newline );
|
|
|
|
break;
|
|
|
|
case 'END':
|
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame,
|
|
|
|
ts.end );
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
res = this._transformToken( token, cb, phaseEndRank, frame,
|
|
|
|
ts.martian );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
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) );
|
|
|
|
tokensLength = tokens.length;
|
|
|
|
i--; // continue at first inserted token
|
|
|
|
} else if ( res.token ) {
|
|
|
|
if ( res.token.rank === phaseEndRank ) {
|
|
|
|
// token is done.
|
|
|
|
localAccum.push(res.token);
|
|
|
|
this.prevToken = res.token;
|
|
|
|
} else {
|
|
|
|
// re-process token.
|
|
|
|
tokens[i] = res.token;
|
|
|
|
i--;
|
|
|
|
}
|
|
|
|
}
|
2011-12-12 20:53:14 +00:00
|
|
|
}
|
2012-01-03 18:44:31 +00:00
|
|
|
return localAccum;
|
2011-12-08 14:37:31 +00:00
|
|
|
};
|
|
|
|
|
2012-01-03 18:44:31 +00:00
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2012-01-03 18:44:31 +00:00
|
|
|
* Token accumulators buffer tokens between asynchronous processing points,
|
|
|
|
* and return fully processed token chunks in-order and as soon as possible.
|
2011-12-13 20:13:09 +00:00
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @constructor
|
|
|
|
* @param {Object} next TokenAccumulator to link to
|
|
|
|
* @param {Array} (optional) tokens, init accumulator with tokens or []
|
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
function TokenAccumulator ( parentCB ) {
|
|
|
|
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;
|
2011-12-08 14:37:31 +00:00
|
|
|
}
|
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2012-01-03 18:44:31 +00:00
|
|
|
* Curry a parentCB with the object and reference.
|
2011-12-13 20:13:09 +00:00
|
|
|
*
|
2012-01-03 18:44:31 +00:00
|
|
|
* @param {Object} TokenAccumulator
|
|
|
|
* @param {misc} Reference / key for callback
|
|
|
|
* @returns {Function}
|
2011-12-13 20:13:09 +00:00
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenAccumulator.prototype.getParentCB = function ( reference ) {
|
|
|
|
return this.returnTokens01.bind( this, reference );
|
2011-12-08 14:37:31 +00:00
|
|
|
};
|
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2012-01-03 18:44:31 +00:00
|
|
|
* Pass tokens to an accumulator
|
2011-12-13 20:13:09 +00:00
|
|
|
*
|
|
|
|
* @method
|
2012-01-03 18:44:31 +00:00
|
|
|
* @param {Object} token
|
|
|
|
*/
|
|
|
|
TokenAccumulator.prototype.returnTokens01 = function ( reference, tokens, notYetDone ) {
|
|
|
|
var res,
|
|
|
|
cb,
|
|
|
|
returnTokens = [];
|
|
|
|
|
|
|
|
if ( ! notYetDone ) {
|
|
|
|
this.outstanding--;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( reference === 'child' ) {
|
|
|
|
// XXX: Use some marker to avoid re-transforming token chunks several
|
|
|
|
// times?
|
|
|
|
res = this.transformPhase01( this.frame, tokens, this.parentCB );
|
|
|
|
|
|
|
|
if ( res.async ) {
|
|
|
|
// new asynchronous expansion started, chain of accumulators
|
|
|
|
// created
|
|
|
|
if ( this.outstanding === 0 ) {
|
|
|
|
// Last accum in chain should only wait for child
|
|
|
|
res.async.outstanding--;
|
|
|
|
cb = this.parentCB;
|
|
|
|
} else {
|
|
|
|
cb = this.parentCB;
|
|
|
|
// set own callback to new sibling, the end of accumulator chain
|
|
|
|
this.parentCB = res.async.getParentCB( 'sibling' );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( ! notYetDone ) {
|
|
|
|
// Child is done, return accumulator from sibling. Siblings
|
|
|
|
// process tokens themselves, so we concat those to the result of
|
|
|
|
// processing tokens from the child.
|
|
|
|
tokens = res.tokens.concat( this.accum );
|
|
|
|
this.accum = [];
|
|
|
|
}
|
|
|
|
this.cb( res.tokens, res.async );
|
|
|
|
return null;
|
|
|
|
} else {
|
|
|
|
// sibling
|
|
|
|
if ( this.outstanding === 0 ) {
|
|
|
|
tokens = this.accum.concat( tokens );
|
|
|
|
// A sibling will transform tokens, so we don't have to do this
|
|
|
|
// again.
|
|
|
|
this.parentCB( res.tokens, false );
|
|
|
|
return null;
|
|
|
|
} else if ( this.outstanding === 1 && notYetDone ) {
|
|
|
|
// 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.
|
|
|
|
return this.parentCB( tokens, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mark the sibling as done (normally at the tail of a chain).
|
2011-12-13 20:13:09 +00:00
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenAccumulator.prototype.siblingDone = function () {
|
|
|
|
this.returnTokens01 ( 'sibling', [], false );
|
2011-12-12 10:01:47 +00:00
|
|
|
};
|
|
|
|
|
2012-01-03 18:44:31 +00:00
|
|
|
|
2011-12-13 20:13:09 +00:00
|
|
|
/**
|
2012-01-03 18:44:31 +00:00
|
|
|
* Push a token into the accumulator
|
2011-12-13 20:13:09 +00:00
|
|
|
*
|
|
|
|
* @method
|
2012-01-03 18:44:31 +00:00
|
|
|
* @param {Object} token
|
2011-12-13 20:13:09 +00:00
|
|
|
*/
|
2012-01-03 18:44:31 +00:00
|
|
|
TokenAccumulator.prototype.push = function ( token ) {
|
|
|
|
return this.accum.push(token);
|
2011-12-08 14:37:31 +00:00
|
|
|
};
|
2011-12-12 14:03:54 +00:00
|
|
|
|
2012-01-03 18:44:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
/* TODO list
|
|
|
|
*
|
|
|
|
* transformPhase01 called first for phase 0-1 (in-order per source file)
|
|
|
|
* then only phase 2 (order independent, if 2 <= token phase < 3, 3 ~ done)
|
|
|
|
* -> don't execute order-dependent transforms in this phase!
|
|
|
|
* * enforce phase on tokens, but not priority within phase
|
|
|
|
* -> cycles possible in async phase
|
|
|
|
* final transform (phase 2) globally in-order and synchronous in root returnTokens01
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* Transformation phases
|
|
|
|
* [0,2)
|
|
|
|
* [2,3] (and 1..2 in templates etc, but clamp phase on *returned* tokens to 2)
|
|
|
|
* 3
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
2011-12-12 14:03:54 +00:00
|
|
|
if (typeof module == "object") {
|
2011-12-13 11:45:12 +00:00
|
|
|
module.exports.TokenTransformDispatcher = TokenTransformDispatcher;
|
2011-12-12 14:03:54 +00:00
|
|
|
}
|