Index: trunk/extensions/ParserPlayground/tests/parserTests.pegjs |
— | — | @@ -33,7 +33,8 @@ |
34 | 34 | comment / |
35 | 35 | article / |
36 | 36 | test / |
37 | | - line |
| 37 | + line / |
| 38 | + hooks |
38 | 39 | |
39 | 40 | |
40 | 41 | |
— | — | @@ -112,3 +113,18 @@ |
113 | 114 | end_test = |
114 | 115 | "!!" ws? "end" ws? eol |
115 | 116 | |
| 117 | + |
| 118 | +hooks = |
| 119 | + start_hooks text:text end_hooks |
| 120 | +{ |
| 121 | + return { |
| 122 | + type: 'hooks', |
| 123 | + text: text |
| 124 | + } |
| 125 | +} |
| 126 | + |
| 127 | +start_hooks = |
| 128 | + "!!" ws? "hooks" ":"? ws? eol |
| 129 | + |
| 130 | +end_hooks = |
| 131 | + "!!" ws? "endhooks" ws? eol |
Index: trunk/extensions/ParserPlayground/tests/parserTests.js |
— | — | @@ -39,6 +39,9 @@ |
40 | 40 | global.PEG = _require('lib.pegjs.js'); |
41 | 41 | |
42 | 42 | // Our code... |
| 43 | +_import('mediawiki.parser.environment.js', ['MWParserEnvironment']); |
| 44 | +_import('ext.cite.taghook.ref.js', ['MWRefTagHook']); |
| 45 | + |
43 | 46 | _import('ext.parserPlayground.serializer.js', ['MWTreeSerializer']); |
44 | 47 | _import('ext.parserPlayground.renderer.js', ['MWTreeRenderer']); |
45 | 48 | _import('ext.parserPlayground.pegParser.js', ['PegParser']); |
— | — | @@ -47,10 +50,16 @@ |
48 | 51 | PegParser.src = fs.readFileSync(path.join(basePath, 'pegParser.pegjs.txt'), 'utf8'); |
49 | 52 | |
50 | 53 | var parser = new PegParser(); |
51 | | -var renderer = new MWTreeRenderer(); |
52 | 54 | |
| 55 | +var testFileName = '../../../tests/parser/parserTests.txt'; // default |
| 56 | +if (process.argv.length > 2) { |
| 57 | + // hack :D |
| 58 | + testFileName = process.argv[2]; |
| 59 | + console.log(testFileName); |
| 60 | +} |
| 61 | + |
53 | 62 | var testParser = PEG.buildParser(fs.readFileSync('parserTests.pegjs', 'utf8')); |
54 | | -var testFile = fs.readFileSync('../../../tests/parser/parserTests.txt', 'utf8'); |
| 63 | +var testFile = fs.readFileSync(testFileName, 'utf8'); |
55 | 64 | |
56 | 65 | |
57 | 66 | try { |
— | — | @@ -108,6 +117,12 @@ |
109 | 118 | if (err) { |
110 | 119 | console.log('PARSE FAIL', err); |
111 | 120 | } else { |
| 121 | + var environment = new MWParserEnvironment({ |
| 122 | + tagHooks: { |
| 123 | + 'ref': MWRefTagHook |
| 124 | + } |
| 125 | + }); |
| 126 | + var renderer = new MWTreeRenderer(environment); |
112 | 127 | renderer.treeToHtml(tree, function(node, err) { |
113 | 128 | if (err) { |
114 | 129 | console.log('RENDER FAIL', err); |
Index: trunk/extensions/ParserPlayground/ParserPlayground.php |
— | — | @@ -61,6 +61,9 @@ |
62 | 62 | |
63 | 63 | $wgResourceModules['ext.parserPlayground'] = array( |
64 | 64 | 'scripts' => array( |
| 65 | + 'mediawiki.parser.environment.js', |
| 66 | + 'ext.cite.taghook.ref.js', |
| 67 | + |
65 | 68 | 'lib.jsdiff.js', |
66 | 69 | 'lib.pegjs.js', |
67 | 70 | 'jquery.nodetree.js', |
Index: trunk/extensions/ParserPlayground/modules/ext.parserPlayground.renderer.js |
— | — | @@ -1,9 +1,9 @@ |
2 | 2 | /** |
3 | 3 | * @param {ParserContext} context |
4 | 4 | */ |
5 | | -function MWTreeRenderer(context) { |
| 5 | +function MWTreeRenderer(env) { |
6 | 6 | // whee |
7 | | - this.context = context || {}; |
| 7 | + this.env = env || {}; |
8 | 8 | } |
9 | 9 | |
10 | 10 | /** |
— | — | @@ -45,6 +45,7 @@ |
46 | 46 | // A sequence of block-level elements... |
47 | 47 | var page = $('<div class="parseNode"></div>'); |
48 | 48 | subParseArray(tree.content, page); |
| 49 | + /* |
49 | 50 | if (self.context.refs) { |
50 | 51 | // We're at the end; drop all the remaining refs! |
51 | 52 | subParseArray([{ |
— | — | @@ -52,6 +53,7 @@ |
53 | 54 | name: 'references' |
54 | 55 | }], page); |
55 | 56 | } |
| 57 | + */ |
56 | 58 | node = page[0]; |
57 | 59 | break; |
58 | 60 | case 'para': |
— | — | @@ -111,7 +113,42 @@ |
112 | 114 | t.append('}}'); |
113 | 115 | node = t[0]; |
114 | 116 | break; |
| 117 | + case 'placeholder': |
| 118 | + if ('content' in tree) { |
| 119 | + var $place = $('<span>'); // hmmmm |
| 120 | + subParseArray(tree.content, $place); |
| 121 | + node = $place[0]; |
| 122 | + } |
| 123 | + break; |
| 124 | + case 'span': |
| 125 | + var $span = $('<span>'); |
| 126 | + if ('attrs' in tree) { |
| 127 | + $.map(tree.attrs, function(val, key) { |
| 128 | + $span.attr(key, val); // @fixme safety! |
| 129 | + }); |
| 130 | + if ('content' in tree) { |
| 131 | + subParseArray(tree.content, $span); |
| 132 | + } |
| 133 | + } |
| 134 | + node = $span[0]; |
| 135 | + break; |
| 136 | + case 'hashlink': |
| 137 | + var $a = $('<a>'); |
| 138 | + $a.attr('href', '#' + tree.target); |
| 139 | + subParseArray(tree.content, $a); |
| 140 | + node = $a[0]; |
| 141 | + break; |
115 | 142 | case 'ext': |
| 143 | + var hook = this.env.getTagHook(tree.name); |
| 144 | + if (!hook) { |
| 145 | + console.log('kabooom! no ext ' + tree.name) |
| 146 | + } |
| 147 | + var transformed = hook.execute(tree); |
| 148 | + var $ext = $('<span>'); // hmmmm |
| 149 | + subParseArray([transformed], $ext); |
| 150 | + node = $ext[0]; |
| 151 | + // @fixme |
| 152 | + /* |
116 | 153 | if (tree.name == 'ref') { |
117 | 154 | // Save the reference for later! |
118 | 155 | // @fixme names etc? |
— | — | @@ -176,6 +213,7 @@ |
177 | 214 | callback(null, 'Unrecognized extension in parse tree'); |
178 | 215 | return; |
179 | 216 | } |
| 217 | + */ |
180 | 218 | break; |
181 | 219 | case 'comment': |
182 | 220 | var h = $('<span class="parseNode comment"></span>').text('<!--' + tree.text + '-->'); |
Index: trunk/extensions/ParserPlayground/modules/ext.cite.taghook.ref.js |
— | — | @@ -0,0 +1,160 @@ |
| 2 | +/** |
| 3 | + * The ref / references tags don't do any fancy HTML, so we can actually |
| 4 | + * implement this in terms of parse tree manipulations, skipping the need |
| 5 | + * for renderer-specific plugins as well. |
| 6 | + * |
| 7 | + * Pretty neat huh! |
| 8 | + */ |
| 9 | + |
| 10 | +MWRefTagHook = function( env ) { |
| 11 | + if (!('cite' in env)) { |
| 12 | + env.cite = { |
| 13 | + refGroups: {} |
| 14 | + }; |
| 15 | + } |
| 16 | + var refGroups = env.cite.refGroups; |
| 17 | + |
| 18 | + var getRefGroup = function(group) { |
| 19 | + if (!(group in refGroups)) { |
| 20 | + var refs = [], |
| 21 | + byName = {}; |
| 22 | + refGroups[group] = { |
| 23 | + refs: refs, |
| 24 | + byName: byName, |
| 25 | + add: function(node, options) { |
| 26 | + var ref; |
| 27 | + if (options.name && options.name in byName) { |
| 28 | + ref = byName[options.name]; |
| 29 | + } else { |
| 30 | + var n = refs.length; |
| 31 | + var key = n + ''; |
| 32 | + if (options.name) { |
| 33 | + key = options.name + '-' + key; |
| 34 | + } |
| 35 | + ref = { |
| 36 | + node: node, |
| 37 | + index: n, |
| 38 | + groupIndex: n, // @fixme |
| 39 | + name: options.name, |
| 40 | + group: options.group, |
| 41 | + key: key, |
| 42 | + target: 'cite_note-' + key, |
| 43 | + linkbacks: [] |
| 44 | + }; |
| 45 | + refs[n] = ref; |
| 46 | + if (options.name) { |
| 47 | + byName[options.name] = ref; |
| 48 | + } |
| 49 | + } |
| 50 | + ref.linkbacks.push( |
| 51 | + 'cite_ref-' + ref.key + '-' + ref.linkbacks.length |
| 52 | + ); |
| 53 | + return ref; |
| 54 | + } |
| 55 | + } |
| 56 | + } |
| 57 | + return refGroups[group]; |
| 58 | + }; |
| 59 | + |
| 60 | + this.execute = function( node ) { |
| 61 | + var options = $.extend({ |
| 62 | + name: null, |
| 63 | + group: null |
| 64 | + }, node.params); |
| 65 | + |
| 66 | + var group = getRefGroup(options.group); |
| 67 | + var ref = group.add(node, options); |
| 68 | + var linkback = ref.linkbacks[ref.linkbacks.length - 1]; |
| 69 | + |
| 70 | + var bits = [] |
| 71 | + if (options.group) { |
| 72 | + bits.push(options.group); |
| 73 | + } |
| 74 | + bits.push(env.formatNum( ref.groupIndex + 1 )); |
| 75 | + |
| 76 | + return { |
| 77 | + type: 'span', |
| 78 | + attrs: { |
| 79 | + id: linkback, |
| 80 | + 'class': 'reference' |
| 81 | + }, |
| 82 | + content: [ |
| 83 | + { |
| 84 | + type: 'hashlink', |
| 85 | + target: '#' + ref.target, |
| 86 | + content: [ |
| 87 | + '[' + bits.join(' ') + ']' |
| 88 | + ] |
| 89 | + }, |
| 90 | + ], |
| 91 | + origNode: node |
| 92 | + }; |
| 93 | + }; |
| 94 | +}; |
| 95 | + |
| 96 | +MWReferencesTagHook = function( env ) { |
| 97 | + var refGroups = env.cite.refGroups; |
| 98 | + |
| 99 | + var arrow = '↑'; |
| 100 | + var renderLine = function( ref ) { |
| 101 | + var out = { |
| 102 | + type: 'li', |
| 103 | + attrs: { |
| 104 | + id: 'cite-note-' + ref.target |
| 105 | + }, |
| 106 | + content: [] |
| 107 | + }; |
| 108 | + if (ref.linkbacks.length == 1) { |
| 109 | + out.content.push({ |
| 110 | + type: 'hashlink', |
| 111 | + target: '#' + ref.linkbacks[0], |
| 112 | + content: [ |
| 113 | + arrow |
| 114 | + ] |
| 115 | + }) |
| 116 | + } else { |
| 117 | + out.content.push(arrow) |
| 118 | + $.each(ref.linkbacks, function(i, linkback) { |
| 119 | + out.contents.push({ |
| 120 | + type: 'hashlink', |
| 121 | + target: '#' + ref.linkbacks[0], |
| 122 | + content: [ |
| 123 | + env.formatNum( ref.groupIndex + '.' + i) |
| 124 | + ] |
| 125 | + }); |
| 126 | + }) |
| 127 | + } |
| 128 | + out.content.push(' '); |
| 129 | + out.content.push({ |
| 130 | + type: 'placeholder', |
| 131 | + content: ref.node.content |
| 132 | + }); |
| 133 | + return out; |
| 134 | + }; |
| 135 | + this.execute = function( node ) { |
| 136 | + var options = $.extend({ |
| 137 | + name: null, |
| 138 | + group: null |
| 139 | + }, node.params); |
| 140 | + if (options.group in refGroups) { |
| 141 | + var group = refGroups[options.group]; |
| 142 | + return { |
| 143 | + type: 'ol', |
| 144 | + attrs: { |
| 145 | + 'class': 'references' |
| 146 | + }, |
| 147 | + content: $.each(group, renderLine), |
| 148 | + origNode: node |
| 149 | + } |
| 150 | + } else { |
| 151 | + return { |
| 152 | + type: 'placeholder', |
| 153 | + origNode: node |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | +} |
| 158 | + |
| 159 | +if (typeof module == "object") { |
| 160 | + module.exports.MWRefTagHook = MWRefTagHook; |
| 161 | +} |
Index: trunk/extensions/ParserPlayground/modules/ext.parserPlayground.js |
— | — | @@ -124,12 +124,17 @@ |
125 | 125 | var pp = context.parserPlayground; |
126 | 126 | pp.parser = new parserClass(); |
127 | 127 | // hack |
| 128 | + pp.env = new MWParserEnvironment({ |
| 129 | + tagHooks: { |
| 130 | + 'ref': MWRefTagHook |
| 131 | + } |
| 132 | + }); |
128 | 133 | if (pp.parser instanceof MediaWikiParser) { |
129 | 134 | pp.serializer = pp.parser; |
130 | 135 | pp.renderer = pp.parser; |
131 | 136 | } else { |
132 | 137 | pp.serializer = new MWTreeSerializer(); |
133 | | - pp.renderer = new MWTreeRenderer(); |
| 138 | + pp.renderer = new MWTreeRenderer(pp.env); |
134 | 139 | } |
135 | 140 | context.parserPlayground.fn.initDisplay(); |
136 | 141 | $.cookie('pp-editmode', className, { |
Index: trunk/extensions/ParserPlayground/modules/mediawiki.parser.environment.js |
— | — | @@ -0,0 +1,86 @@ |
| 2 | +var MWParserEnvironment = function(opts) { |
| 3 | + var options = { |
| 4 | + tagHooks: {}, |
| 5 | + parserFunctions: {} |
| 6 | + }; |
| 7 | + $.extend(options, opts); |
| 8 | + this.tagHooks = options.tagHooks; |
| 9 | + this.parserFunctions = options.parserFunctions; |
| 10 | +}; |
| 11 | + |
| 12 | +$.extend(MWParserEnvironment.prototype, { |
| 13 | + // Does this need separate UI/content inputs? |
| 14 | + formatNum: function( num ) { |
| 15 | + return num + ''; |
| 16 | + }, |
| 17 | + |
| 18 | + getVariable: function( varname, options ) { |
| 19 | + // |
| 20 | + }, |
| 21 | + |
| 22 | + /** |
| 23 | + * @return MWParserFunction |
| 24 | + */ |
| 25 | + getParserFunction: function( name ) { |
| 26 | + if (name in this.parserFunctions) { |
| 27 | + return new this.parserFunctions[name]( this ); |
| 28 | + } else { |
| 29 | + return null; |
| 30 | + } |
| 31 | + }, |
| 32 | + |
| 33 | + /** |
| 34 | + * @return MWParserTagHook |
| 35 | + */ |
| 36 | + getTagHook: function( name ) { |
| 37 | + if (name in this.tagHooks) { |
| 38 | + return new this.tagHooks[name](this); |
| 39 | + } else { |
| 40 | + return null; |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | +}); |
| 45 | + |
| 46 | + |
| 47 | + |
| 48 | +/** |
| 49 | + * @parm MWParserEnvironment env |
| 50 | + * @constructor |
| 51 | + */ |
| 52 | +MWParserTagHook = function( env ) { |
| 53 | + if (!env) { |
| 54 | + throw new Error( 'Tag hook requires a parser environment.' ); |
| 55 | + } |
| 56 | + this.env = env; |
| 57 | +}; |
| 58 | + |
| 59 | +/** |
| 60 | + * @param string text (or a parse tree?) |
| 61 | + * @param object params map of named parameters (strings or parse frames?) |
| 62 | + * @return either a string or a parse frame -- finalize this? |
| 63 | + */ |
| 64 | +MWParserTagHook.execute = function( text, params ) { |
| 65 | + return ''; |
| 66 | +}; |
| 67 | + |
| 68 | + |
| 69 | +MWParserFunction = function( env) { |
| 70 | + if (!env) { |
| 71 | + throw new Error( 'Parser funciton requires a parser environment.'); |
| 72 | + } |
| 73 | + this.env = env; |
| 74 | +}; |
| 75 | + |
| 76 | +/** |
| 77 | + * @param string text (or a parse tree?) |
| 78 | + * @param object params map of named parameters (strings or parse frames?) |
| 79 | + * @return either a string or a parse frame -- finalize this? |
| 80 | + */ |
| 81 | +MWParserFunction.execute = function( text, params ) { |
| 82 | + return ''; |
| 83 | +}; |
| 84 | + |
| 85 | +if (typeof module == "object") { |
| 86 | + module.exports.MWParserEnvironment = MWParserEnvironment; |
| 87 | +} |
Index: trunk/extensions/ParserPlayground/modules/pegParser.pegjs.txt |
— | — | @@ -239,8 +239,9 @@ |
240 | 240 | |
241 | 241 | ref = ref_full / ref_empty |
242 | 242 | |
| 243 | +/* Can we do backreferences to genericize this? */ |
243 | 244 | ref_full |
244 | | - = start:ref_start ">" content:ref_content+ close:ref_end { |
| 245 | + = start:ref_start ">" content:ref_content* close:ref_end { |
245 | 246 | return { |
246 | 247 | type: 'ext', |
247 | 248 | name: 'ref', |