Convert the Cite extension to a token stream transformer.

This required a few further additions to the TokenTransformDispatcher. In
particular, there is now an 'any' token match whose callbacks are executed
before more specific callbacks. This is used by the Cite extension to eat all
tokens between ref and /ref tags. This need is very common, so should be
broken out to an intermediate layer in the future.

In general, the requirements for the TokenTransformDispatcher API are now
clearer, and the API should likely be cleaned up / simplified.
This commit is contained in:
Gabriel Wicke 2011-12-13 14:48:47 +00:00
commit 0fed6f9c79

249
modules/parser/ext.Cite.js Normal file
View file

@ -0,0 +1,249 @@
/**
* The ref / references tags don't do any fancy HTML, so we can actually
* implement this in terms of parse tree manipulations, skipping the need
* for renderer-specific plugins as well.
*
* Pretty neat huh!
*/
function Cite () {
this.refGroups = {};
this.refTokens = [];
}
Cite.prototype.register = function ( dispatcher ) {
// Register for ref and references tag tokens
var self = this;
this.onRefCB = function (ctx) {
return self.onRef(ctx);
};
dispatcher.appendListener( this.onRefCB, 'tag', 'ref' );
dispatcher.appendListener( function (ctx) {
return self.onReferences(ctx);
}, 'tag', 'references' );
dispatcher.appendListener( function (ctx) {
return self.onEnd(ctx);
}, 'end' );
};
// Convert list of key-value pairs to object, with first entry for a key
// winning.
// XXX: Move to general util module
Cite.prototype.attribsToObject = function ( attribs ) {
if ( attribs === undefined ) {
return {};
}
var obj = {};
for ( var i = 0, l = attribs.length; i < l; i++ ) {
var kv = attribs[i];
if (! kv[0] in obj) {
obj[kv[0]] = kv[1];
}
}
return obj;
};
Cite.prototype.onRef = function ( tokenCTX ) {
var refGroups = this.refGroups;
var getRefGroup = function(group) {
if (!(group in refGroups)) {
var refs = [],
byName = {};
refGroups[group] = {
refs: refs,
byName: byName,
add: function(tokens, options) {
var ref;
if (options.name && options.name in byName) {
ref = byName[options.name];
} else {
var n = refs.length;
var key = n + '';
if (options.name) {
key = options.name + '-' + key;
}
ref = {
tokens: tokens,
index: n,
groupIndex: n, // @fixme
name: options.name,
group: options.group,
key: key,
target: 'cite_note-' + key,
linkbacks: []
};
refs[n] = ref;
if (options.name) {
byName[options.name] = ref;
}
}
ref.linkbacks.push(
'cite_ref-' + ref.key + '-' + ref.linkbacks.length
);
return ref;
}
};
}
return refGroups[group];
};
var token = tokenCTX.token;
// Collect all tokens between ref start and endtag
if ( token.type === 'TAG' && token.name.toLowerCase() === 'ref' ) {
this.curRef = tokenCTX.token;
// Prepend self for 'any' token type
tokenCTX.dispatcher.prependListener(this.onRefCB, 'any' );
tokenCTX.token = null;
return tokenCTX;
} else if ( token.type === 'ENDTAG' && token.name.toLowerCase() === 'ref' ) {
tokenCTX.dispatcher.removeListener(this.onRefCB, 'any' );
// fall through for further processing!
} else {
// Inside ref block: Collect all other tokens in refTokens and abort
this.refTokens.push(tokenCTX.token);
tokenCTX.token = null;
return tokenCTX;
}
var options = $.extend({
name: null,
group: null
}, this.attribsToObject(this.curRef.attribs));
var group = getRefGroup(options.group);
var ref = group.add(this.refTokens, options);
this.refTokens = [];
var linkback = ref.linkbacks[ref.linkbacks.length - 1];
var bits = [];
if (options.group) {
bits.push(options.group);
}
//bits.push(env.formatNum( ref.groupIndex + 1 ));
bits.push(ref.groupIndex + 1);
tokenCTX.token = [
{
type: 'TAG',
name: 'span',
attribs: [['id', linkback],
['class', 'reference'],
// ignore element when serializing back to wikitext
['data-nosource', '']]
},
{
type: 'TAG',
name: 'a',
attribs:
[['data-type', 'hashlink'],
['href', '#' + ref.target]
// XXX: Add round-trip info here?
]
},
{
type: 'TEXT',
value: '[' + bits.join(' ') + ']'
},
{
type: 'ENDTAG',
name: 'a'
},
{
type: 'ENDTAG',
name: 'span'
}
];
return tokenCTX;
};
Cite.prototype.onReferences = function ( tokenCTX ) {
var refGroups = this.refGroups;
var arrow = '↑';
var renderLine = function( ref ) {
//console.log('reftokens: ' + JSON.stringify(ref.tokens, null, 2));
var out = [{
type: 'TAG',
name: 'li',
attribs: [['id', ref.target]]
}];
if (ref.linkbacks.length == 1) {
out = out.concat([{
type: 'TAG',
name: 'a',
attribs:
[['data-type', 'hashlink'],
['href', '#' + ref.linkbacks[0]]
]
},
{type: 'TEXT', value: arrow},
{type: 'ENDTAG', name: 'a'}
],
ref.tokens // The original content tokens
);
} else {
out.content.push({type: 'TEXT', value: arrow});
$.each(ref.linkbacks, function(i, linkback) {
out = out.concat([{
type: 'TAG',
name: 'a',
attribs:
[['data-type', 'hashlink'],
['href', '#' + ref.linkbacks[0]]
]
},
// XXX: make formatNum available!
//{type: 'TEXT', value: env.formatNum( ref.groupIndex + '.' + i)},
{type: 'TEXT', value: ref.groupIndex + '.' + i},
{type: 'ENDTAG', name: 'a'}
],
ref.tokens // The original content tokens
);
});
}
return out;
};
var token = tokenCTX.token;
var options = $.extend({
name: null,
group: null
}, this.attribsToObject(token.attribs));
if (options.group in refGroups) {
var group = refGroups[options.group];
var listItems = $.map(group.refs, renderLine);
tokenCTX.token = [{
type: 'TAG',
name: 'ol',
attribs: [['class', 'references']]
}].concat(listItems, {type: 'ENDTAG', name: 'ol'});
} else {
tokenCTX.token = {
type: 'SELFCLOSINGTAG',
name: 'placeholder',
attribs: [['data-origNode', JSON.stringify(token)]]
};
}
return tokenCTX;
};
Cite.prototype.onEnd = function ( tokenCTX ) {
// XXX: Emit error messages if references tag was missing!
// Clean up
this.refGroups = {};
this.refTokens = [];
return tokenCTX;
}
if (typeof module == "object") {
module.exports.Cite = Cite;
}