mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-25 23:05:35 +00:00
379 lines
12 KiB
JavaScript
379 lines
12 KiB
JavaScript
/* Generic token transformation dispatcher with support for asynchronous token
|
|
* expansion. Individual transformations register for the token types they are
|
|
* interested in and are called on each matching token.
|
|
*
|
|
* A transformer might set TokenContext.token to null, a single token, or an
|
|
* array of tokens before returning it.
|
|
* - Null removes the token and stops further processing for this token.
|
|
* - A single token is further processed using the remaining transformations
|
|
* registered for this token, and finally placed in the output token list.
|
|
* - A list of tokens stops the processing for this token. Instead, processing
|
|
* restarts with the first returned token.
|
|
*
|
|
* Additionally, transformers performing asynchronous actions on a token can
|
|
* create a new TokenAccumulator using .newAccumulator(). This creates a new
|
|
* accumulator for each asynchronous result, with the asynchronously processed
|
|
* token last in its internal accumulator. This setup avoids the need to apply
|
|
* operational-transform-like index transformations when parallel expansions
|
|
* insert tokens in front of other ongoing expansion tasks.
|
|
*
|
|
* XXX: I am not completely happy with the mutable TokenContext construct. At
|
|
* least the token should probably be passed as a separate argument. Also,
|
|
* integrate the general environment (configuration, cache etc). (gwicke)
|
|
* */
|
|
|
|
$ = require('jquery');
|
|
|
|
/**
|
|
* Central dispatcher for potentially asynchronous token transformations.
|
|
*
|
|
* @class
|
|
* @constructor
|
|
* @param {Function} callback, a callback function accepting a token list as
|
|
* its only argument.
|
|
*/
|
|
function TokenTransformDispatcher( callback ) {
|
|
this.cb = callback; // Called with transformed token list when done
|
|
this.transformers = {
|
|
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
|
|
};
|
|
this.reset();
|
|
}
|
|
|
|
/**
|
|
* Reset the internal token and outstanding-callback state of the
|
|
* TokenTransformDispatcher, but keep registrations untouched.
|
|
*
|
|
* @method
|
|
*/
|
|
TokenTransformDispatcher.prototype.reset = function () {
|
|
this.accum = new TokenAccumulator(null);
|
|
this.firstaccum = this.accum;
|
|
this.outstanding = 1; // Number of outstanding processing steps
|
|
// (e.g., async template fetches/expansions)
|
|
};
|
|
|
|
/**
|
|
* Append a listener registration. The new listener will be executed after
|
|
* other listeners for the same token have been called.
|
|
*
|
|
* @method
|
|
* @param {Function} listener, a function accepting a TokenContext and
|
|
* returning a TokenContext.
|
|
* @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
|
|
*/
|
|
TokenTransformDispatcher.prototype.appendListener = function ( listener, type, name ) {
|
|
if ( type === 'tag' ) {
|
|
name = name.toLowerCase();
|
|
if ( $.isArray(this.transformers.tag.name) ) {
|
|
this.transformers.tag[name].push(listener);
|
|
} else {
|
|
this.transformers.tag[name] = [listener];
|
|
}
|
|
} else {
|
|
this.transformers[type].push(listener);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Prepend a listener registration. The new listener will be called before
|
|
* other listeners for the same token have been called.
|
|
*
|
|
* @method
|
|
* @param {Function} listener, a function accepting a TokenContext and
|
|
* returning a TokenContext.
|
|
* @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
|
|
*/
|
|
TokenTransformDispatcher.prototype.prependListener = function ( listener, type, name ) {
|
|
if ( type === 'tag' ) {
|
|
name = name.toLowerCase();
|
|
if ( $.isArray(this.transformers.tag.name) ) {
|
|
this.transformers.tag[name].unshift(listener);
|
|
} else {
|
|
this.transformers.tag[name] = [listener];
|
|
}
|
|
} else {
|
|
this.transformers[type].unshift(listener);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove a listener registration
|
|
*
|
|
* XXX: matching the function for equality is not ideal. Use a string key
|
|
* instead?
|
|
*
|
|
* @method
|
|
* @param {Function} listener, a function accepting a TokenContext and
|
|
* returning a TokenContext.
|
|
* @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
|
|
*/
|
|
TokenTransformDispatcher.prototype.removeListener = function ( listener, type, name ) {
|
|
var i = -1;
|
|
var ts;
|
|
if ( type === 'tag' ) {
|
|
name = name.toLowerCase();
|
|
if ( $.isArray(this.transformers.tag.name) ) {
|
|
ts = this.transformers.tag[name];
|
|
i = ts.indexOf(listener);
|
|
}
|
|
} else {
|
|
ts = this.transformers[type];
|
|
i = ts.indexOf(listener);
|
|
}
|
|
if ( i >= 0 ) {
|
|
ts.splice(i, 1);
|
|
}
|
|
};
|
|
|
|
/* Constructor for information context relevant to token transformers
|
|
*
|
|
* @param token The token to precess
|
|
* @param accum {TokenAccumulator} The active TokenAccumulator.
|
|
* @param processor {TokenTransformDispatcher} The TokenTransformDispatcher object.
|
|
* @param lastToken Last returned token or {undefined}.
|
|
* @returns {TokenContext}.
|
|
*/
|
|
function TokenContext ( token, accum, dispatcher, lastToken ) {
|
|
this.token = token;
|
|
this.accum = accum;
|
|
this.dispatcher = dispatcher;
|
|
this.lastToken = lastToken;
|
|
return this;
|
|
}
|
|
|
|
/* Call all transformers on a tag.
|
|
*
|
|
* @param {TokenContext} The current token and its context.
|
|
* @returns {TokenContext} Context with updated token and/or accum.
|
|
*/
|
|
TokenTransformDispatcher.prototype._transformTagToken = function ( tokenCTX ) {
|
|
// prepend 'any' transformers
|
|
var ts = this.transformers.any;
|
|
var tagts = this.transformers.tag[tokenCTX.token.name.toLowerCase()];
|
|
if ( tagts ) {
|
|
ts = ts.concat(tagts);
|
|
}
|
|
//console.log(JSON.stringify(ts, null, 2));
|
|
if ( ts ) {
|
|
for (var i = 0, l = ts.length; i < l; i++ ) {
|
|
// Transform token with side effects
|
|
tokenCTX = ts[i]( tokenCTX );
|
|
if ( tokenCTX.token === null || $.isArray(tokenCTX.token) ) {
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
return tokenCTX;
|
|
};
|
|
|
|
/* Call all transformers on non-tag token types.
|
|
*
|
|
* @param tokenCTX {TokenContext} The current token and its context.
|
|
* @param ts List of token transformers for this token type.
|
|
* @returns {TokenContext} Context with updated token and/or accum.
|
|
*/
|
|
TokenTransformDispatcher.prototype._transformToken = function ( tokenCTX, ts ) {
|
|
// prepend 'any' transformers
|
|
ts = this.transformers.any.concat(ts);
|
|
if ( ts ) {
|
|
for (var i = 0, l = ts.length; i < l; i++ ) {
|
|
// Transform token with side effects
|
|
tokenCTX = ts[i]( tokenCTX );
|
|
if ( tokenCTX.token === null || $.isArray(tokenCTX.token) ) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return tokenCTX;
|
|
};
|
|
|
|
/**
|
|
* Transform and expand tokens.
|
|
*
|
|
* Normally called with undefined accum. Asynchronous expansions will call
|
|
* this with their known accum, which allows expanded tokens to be spliced in
|
|
* at the appropriate location in the token list, which is always at the tail
|
|
* end of the current accumulator. Calls back registered callback if there are
|
|
* no more outstanding asynchronous expansions.
|
|
*
|
|
* @param {Array} Tokens to process.
|
|
* @param {Object} TokenAccumulator object. Undefined for first call, set to
|
|
* accumulator with expanded token at tail for asynchronous expansions.
|
|
* @param {Int} delta, default 1. Decrement the outstanding async callback
|
|
* count by this much to determine when all outstanding actions are done.
|
|
* Main use of this argument is to avoid counting some extra callbacks from
|
|
* actions before they are done.
|
|
*/
|
|
TokenTransformDispatcher.prototype.transformTokens = function ( tokens, accum, delta ) {
|
|
if ( accum === undefined ) {
|
|
this.reset();
|
|
accum = this.accum;
|
|
}
|
|
|
|
//console.log('transformTokens: ' + JSON.stringify(tokens) + JSON.stringify(accum.accum) );
|
|
|
|
var tokenCTX = new TokenContext(undefined, accum, this, undefined);
|
|
var origLen = tokens.length;
|
|
for ( var i = 0; i < tokens.length; i++ ) {
|
|
tokenCTX.lastToken = tokenCTX.token; // FIXME: Fix re-entrant case!
|
|
tokenCTX.token = tokens[i];
|
|
tokenCTX.pos = i;
|
|
tokenCTX.accum = accum;
|
|
switch(tokenCTX.token.type) {
|
|
case 'TAG':
|
|
case 'ENDTAG':
|
|
case 'SELFCLOSINGTAG':
|
|
tokenCTX = this._transformTagToken( tokenCTX );
|
|
break;
|
|
case 'TEXT':
|
|
tokenCTX = this._transformToken( tokenCTX, this.transformers.text );
|
|
break;
|
|
case 'COMMENT':
|
|
tokenCTX = this._transformToken( tokenCTX, this.transformers.comment);
|
|
break;
|
|
case 'NEWLINE':
|
|
tokenCTX = this._transformToken( tokenCTX, this.transformers.newline );
|
|
break;
|
|
case 'END':
|
|
tokenCTX = this._transformToken( tokenCTX, this.transformers.end );
|
|
break;
|
|
default:
|
|
tokenCTX = this._transformToken( tokenCTX, this.transformers.martian );
|
|
break;
|
|
}
|
|
// add special DELAYED value
|
|
if( $.isArray(tokenCTX.token) ) {
|
|
// Splice in the returned tokens (while replacing the original
|
|
// token), and process them next.
|
|
[].splice.apply(tokens, [i, 1].concat(tokenCTX.token));
|
|
//l += tokenCTX.token.length - 1;
|
|
i--; // continue at first inserted token
|
|
} else if (tokenCTX.token) {
|
|
// push to accumulator
|
|
accum.push(tokenCTX.token);
|
|
}
|
|
// Update current accum, in case a new one was spliced in by a
|
|
// transformation starting asynch work.
|
|
accum = tokenCTX.accum;
|
|
}
|
|
|
|
if ( delta === undefined ) {
|
|
delta = 1;
|
|
}
|
|
|
|
this.finish( delta );
|
|
};
|
|
|
|
/**
|
|
* Decrement the number of outstanding async actions by delta and call the
|
|
* callback with a list of tokens if none are remaining.
|
|
*
|
|
* @method
|
|
* @param {Int} delta, how much to decrement the number of outstanding async
|
|
* actions.
|
|
*/
|
|
TokenTransformDispatcher.prototype.finish = function ( delta ) {
|
|
this.outstanding -= delta;
|
|
if ( this.outstanding === 0 ) {
|
|
// Join the token accumulators back into a single token list
|
|
var a = this.firstaccum;
|
|
var tokens = a.accum;
|
|
while ( a.next !== null ) {
|
|
a = a.next;
|
|
tokens = tokens.concat(a.accum);
|
|
}
|
|
//console.log('TOKENS: ' + JSON.stringify(tokens, null, 2));
|
|
// Call our callback with the flattened token list
|
|
this.cb(tokens);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Start a new accumulator for asynchronous work.
|
|
*
|
|
* @param {Object} TokenAccumulator object after which to insert a new
|
|
* accumulator
|
|
* @count {Int} (optional, default 1) The number of callbacks to expect before
|
|
* considering the asynch work on the new accumulator done.
|
|
* */
|
|
TokenTransformDispatcher.prototype.newAccumulator = function ( accum, count ) {
|
|
if ( count !== undefined ) {
|
|
this.outstanding += count;
|
|
} else {
|
|
this.outstanding++;
|
|
}
|
|
if ( accum === undefined ) {
|
|
accum = this.accum;
|
|
}
|
|
return accum.insertAccumulator( );
|
|
};
|
|
|
|
/**
|
|
* Token accumulators in a linked list. Using a linked list simplifies async
|
|
* callbacks for template expansions as it avoids stable references to chunks.
|
|
*
|
|
* @class
|
|
* @constructor
|
|
* @param {Object} next TokenAccumulator to link to
|
|
* @param {Array} (optional) tokens, init accumulator with tokens or []
|
|
*/
|
|
function TokenAccumulator ( next, tokens ) {
|
|
this.next = next;
|
|
if ( tokens ) {
|
|
this.accum = tokens;
|
|
} else {
|
|
this.accum = [];
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Push a token into the accumulator
|
|
*
|
|
* @method
|
|
* @param {Object} token
|
|
*/
|
|
TokenAccumulator.prototype.push = function ( token ) {
|
|
return this.accum.push(token);
|
|
};
|
|
|
|
/**
|
|
* Pop a token from the accumulator
|
|
*
|
|
* @method
|
|
* @returns {Object} token
|
|
*/
|
|
TokenAccumulator.prototype.pop = function ( ) {
|
|
return this.accum.pop();
|
|
};
|
|
|
|
/**
|
|
* Insert an accumulator after this one.
|
|
*
|
|
* @method
|
|
* @returns {Object} created TokenAccumulator
|
|
*/
|
|
TokenAccumulator.prototype.insertAccumulator = function ( ) {
|
|
this.next = new TokenAccumulator(this.next);
|
|
return this.next;
|
|
};
|
|
|
|
if (typeof module == "object") {
|
|
module.exports.TokenTransformDispatcher = TokenTransformDispatcher;
|
|
}
|
|
|