mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Cite
synced 2024-11-28 08:50:07 +00:00
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:
commit
0fed6f9c79
249
modules/parser/ext.Cite.js
Normal file
249
modules/parser/ext.Cite.js
Normal 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;
|
||||
}
|
Loading…
Reference in a new issue