Index: trunk/extensions/VisualEditor/tests/parser/parserTests.js |
— | — | @@ -58,7 +58,7 @@ |
59 | 59 | |
60 | 60 | var testWhiteList = require('./parserTests-whitelist.js').testWhiteList; |
61 | 61 | |
62 | | -//_import(pj('parser', 'mediawiki.parser.environment.js'), ['MWParserEnvironment']); |
| 62 | +_import(pj('parser', 'mediawiki.parser.environment.js'), ['MWParserEnvironment']); |
63 | 63 | _import(pj('parser', 'mediawiki.parser.js'), ['ParserPipeline']); |
64 | 64 | |
65 | 65 | // WikiDom and serializers |
— | — | @@ -552,11 +552,8 @@ |
553 | 553 | // } |
554 | 554 | //}); |
555 | 555 | |
556 | | - // move this config out of here |
557 | | - var config = { |
558 | | - parserEnv: {} |
559 | | - }; |
560 | | - var parserPipeline = new ParserPipeline( config ); |
| 556 | + var env = new MWParserEnvironment({}); |
| 557 | + var parserPipeline = new ParserPipeline( env ); |
561 | 558 | |
562 | 559 | var comments = [], |
563 | 560 | pt = this; |
Index: trunk/extensions/VisualEditor/modules/parser/mediawiki.TokenTransformManager.js |
— | — | @@ -303,12 +303,12 @@ |
304 | 304 | * @param {Object} args, the argument map for templates |
305 | 305 | * @param {Object} env, the environment. |
306 | 306 | */ |
307 | | -function AsyncTokenTransformManager ( childFactory, args, env ) { |
| 307 | +function AsyncTokenTransformManager ( childFactories, args, env ) { |
308 | 308 | // Factory function for new AsyncTokenTransformManager creation with |
309 | 309 | // default transforms enabled |
310 | 310 | // Also sets up a tokenizer and phase-1-transform depending on the input format |
311 | 311 | // nestedAsyncTokenTransformManager = manager.newChildPipeline( inputType, args ); |
312 | | - this.childFactory = childFactory; |
| 312 | + this.childFactories = childFactories; |
313 | 313 | this._construct(); |
314 | 314 | this._reset( args, env ); |
315 | 315 | } |
— | — | @@ -326,12 +326,33 @@ |
327 | 327 | * @returns {Object} Pipeline, which is an object with 'first' pointing to the |
328 | 328 | * first stage of the pipeline, and 'last' pointing to the last stage. |
329 | 329 | */ |
330 | | -AsyncTokenTransformManager.prototype.newChildPipeline = function ( inputType, args ) { |
331 | | - var pipe = this.childFactory( inputType, args ); |
| 330 | +AsyncTokenTransformManager.prototype.newChildPipeline = function ( inputType, args, title ) { |
| 331 | + var pipe = this.childFactories.input( inputType, args ); |
| 332 | + |
| 333 | + // now set up a few things on the child AsyncTokenTransformManager. |
| 334 | + var child = pipe.last; |
| 335 | + // We assume that the title was already checked against this.loopCheck |
| 336 | + // before! |
| 337 | + child.loopCheck = new LoopCheck ( this.loopCheck, title ); |
| 338 | + // Same for depth! |
| 339 | + child.depth = this.depth + 1; |
332 | 340 | return pipe; |
333 | 341 | }; |
334 | 342 | |
335 | 343 | /** |
| 344 | + * Create a pipeline for attribute transformations. |
| 345 | + * |
| 346 | + * @method |
| 347 | + * @param {String} Input type, currently only support 'text/wiki'. |
| 348 | + * @param {Object} Template arguments |
| 349 | + * @returns {Object} Pipeline, which is an object with 'first' pointing to the |
| 350 | + * first stage of the pipeline, and 'last' pointing to the last stage. |
| 351 | + */ |
| 352 | +AsyncTokenTransformManager.prototype.newAttributePipeline = function ( inputType, args ) { |
| 353 | + return this.childFactories.attributes( inputType, args ); |
| 354 | +}; |
| 355 | + |
| 356 | +/** |
336 | 357 | * Reset the internal token and outstanding-callback state of the |
337 | 358 | * TokenTransformManager, but keep registrations untouched. |
338 | 359 | * |
— | — | @@ -547,7 +568,7 @@ |
548 | 569 | localAccum = [], |
549 | 570 | localAccumLength = 0, |
550 | 571 | tokensLength = tokens.length, |
551 | | - cb = undefined, // XXX: not meaningful for purely synchronous processing! |
| 572 | + cb, // XXX: not meaningful for purely synchronous processing! |
552 | 573 | token, |
553 | 574 | // Top-level frame only in phase 3, as everything is already expanded. |
554 | 575 | ts = this.transformers; |
— | — | @@ -613,10 +634,43 @@ |
614 | 635 | }; |
615 | 636 | |
616 | 637 | |
| 638 | +/********************** AttributeTransformManager *************************/ |
617 | 639 | |
| 640 | +/** |
| 641 | + * Utility transformation manager for attributes, using an attribute |
| 642 | + * transformation pipeline (normally phase1 SyncTokenTransformManager and |
| 643 | + * phase2 AsyncTokenTransformManager). This pipeline needs to be independent |
| 644 | + * of the containing TokenTransformManager to isolate transforms from each |
| 645 | + * other. |
| 646 | + * |
| 647 | + * @class |
| 648 | + * @constructor |
| 649 | + * @param {Object} Containing TokenTransformManager |
| 650 | + */ |
| 651 | +function AttributeTransformManager ( manager, callback ) { |
| 652 | + this.callback = callback; |
| 653 | + var pipe = manager.newAttributePipeline( manager.args ); |
| 654 | + pipe.addListener( 'chunk', this.onChunk.bind( this ) ); |
| 655 | + pipe.addListener( 'end', this.onEnd.bind( this ) ); |
| 656 | +} |
618 | 657 | |
| 658 | +/** |
| 659 | + * Collect chunks returned from the pipeline |
| 660 | + */ |
| 661 | +AttributeTransformManager.prototype.onChunk = function ( chunk ) { |
| 662 | + this.callback( chunk, true ); |
| 663 | +}; |
619 | 664 | |
| 665 | +/** |
| 666 | + * Empty the pipeline by returning to the parent |
| 667 | + */ |
| 668 | +AttributeTransformManager.prototype.onEnd = function ( ) { |
| 669 | + this.callback( [], false ); |
| 670 | +}; |
620 | 671 | |
| 672 | + |
| 673 | + |
| 674 | + |
621 | 675 | /******************************* TokenAccumulator *************************/ |
622 | 676 | /** |
623 | 677 | * Token accumulators buffer tokens between asynchronous processing points, |
— | — | @@ -730,6 +784,43 @@ |
731 | 785 | }; |
732 | 786 | |
733 | 787 | |
| 788 | +/** |
| 789 | + * Loop check helper class for AsyncTokenTransformManager. |
| 790 | + * |
| 791 | + * We use a bottom-up linked list to allow sharing of paths between async |
| 792 | + * expansions. |
| 793 | + * |
| 794 | + * @class |
| 795 | + * @constructor |
| 796 | + */ |
| 797 | +function LoopCheck ( parent, title ) { |
| 798 | + if ( parent ) { |
| 799 | + this.parent = parent; |
| 800 | + } else { |
| 801 | + this.parent = null; |
| 802 | + } |
| 803 | + this.title = title; |
| 804 | +} |
| 805 | + |
| 806 | +/** |
| 807 | + * Check if expanding <title> would lead to a loop. |
| 808 | + * |
| 809 | + * @method |
| 810 | + * @param {String} Title to check. |
| 811 | + */ |
| 812 | +LoopCheck.prototype.check = function ( title ) { |
| 813 | + var elem = this; |
| 814 | + do { |
| 815 | + if ( elem.title === title ) { |
| 816 | + // Loop detected |
| 817 | + return true; |
| 818 | + } |
| 819 | + elem = elem.parent; |
| 820 | + } while ( elem ); |
| 821 | + // No loop detected. |
| 822 | + return false; |
| 823 | +}; |
| 824 | + |
734 | 825 | if (typeof module == "object") { |
735 | 826 | module.exports.AsyncTokenTransformManager = AsyncTokenTransformManager; |
736 | 827 | module.exports.SyncTokenTransformManager = SyncTokenTransformManager; |
Index: trunk/extensions/VisualEditor/modules/parser/parse.js |
— | — | @@ -7,9 +7,10 @@ |
8 | 8 | ( function() { |
9 | 9 | |
10 | 10 | var ParserPipeline = require('./mediawiki.parser.js').ParserPipeline, |
| 11 | + ParserEnv = require('./mediawiki.parser.environment.js').MWParserEnvironment, |
11 | 12 | optimist = require('optimist'); |
12 | 13 | |
13 | | - var parser = new ParserPipeline(); |
| 14 | + var parser = new ParserPipeline( new ParserEnv({}) ); |
14 | 15 | |
15 | 16 | |
16 | 17 | process.stdin.resume(); |
Index: trunk/extensions/VisualEditor/modules/parser/mediawiki.parser.js |
— | — | @@ -112,35 +112,64 @@ |
113 | 113 | * always an AsyncTokenTransformManager, which emits its output in events. |
114 | 114 | */ |
115 | 115 | ParserPipeline.prototype.makeInputPipeline = function ( inputType, args ) { |
116 | | - if ( inputType === 'text/wiki' ) { |
117 | | - var wikiTokenizer = new PegTokenizer(); |
| 116 | + switch ( inputType ) { |
| 117 | + case 'text/wiki': |
| 118 | + var wikiTokenizer = new PegTokenizer(); |
118 | 119 | |
119 | | - /** |
120 | | - * Token stream transformations. |
121 | | - * This is where all the wiki-specific functionality is implemented. |
122 | | - * See https://www.mediawiki.org/wiki/Future/Parser_development/Token_stream_transformations |
123 | | - */ |
124 | | - var tokenPreProcessor = new TokenTransformManager.SyncTokenTransformManager ( this.env ); |
125 | | - tokenPreProcessor.listenForTokensFrom ( wikiTokenizer ); |
| 120 | + /** |
| 121 | + * Token stream transformations. |
| 122 | + * This is where all the wiki-specific functionality is implemented. |
| 123 | + * See https://www.mediawiki.org/wiki/Future/Parser_development/Token_stream_transformations |
| 124 | + */ |
| 125 | + // XXX: Use this.env.config.transforms['inputType'][stage] or |
| 126 | + // somesuch to set up the transforms by input type |
| 127 | + var tokenPreProcessor = new TokenTransformManager.SyncTokenTransformManager ( this.env ); |
| 128 | + tokenPreProcessor.listenForTokensFrom ( wikiTokenizer ); |
126 | 129 | |
127 | | - var tokenExpander = new TokenTransformManager.AsyncTokenTransformManager ( |
128 | | - this.makeInputPipeline.bind( this ), args, this.env ); |
129 | | - tokenExpander.listenForTokensFrom ( tokenPreProcessor ); |
130 | | - |
131 | | - return { first: wikiTokenizer, last: tokenExpander }; |
132 | | - } else { |
133 | | - throw "ParserPipeline.makeInputPipeline: Unsupported input type " + inputType; |
| 130 | + var tokenExpander = new TokenTransformManager.AsyncTokenTransformManager ( |
| 131 | + { |
| 132 | + 'input': this.makeInputPipeline.bind( this ), |
| 133 | + 'attributes': this.makeAttributePipeline.bind( this ) |
| 134 | + }, |
| 135 | + args, this.env |
| 136 | + ); |
| 137 | + tokenExpander.listenForTokensFrom ( tokenPreProcessor ); |
| 138 | + |
| 139 | + return { first: wikiTokenizer, last: tokenExpander }; |
| 140 | + |
| 141 | + default: |
| 142 | + throw "ParserPipeline.makeInputPipeline: Unsupported input type " + inputType; |
134 | 143 | } |
135 | 144 | }; |
136 | 145 | |
137 | 146 | |
138 | 147 | /** |
139 | | - * Parse an input |
| 148 | + * Factory for attribute transformations, with input type implicit in the |
| 149 | + * environment. |
| 150 | + */ |
| 151 | +ParserPipeline.prototype.makeAttributePipeline = function ( args ) { |
| 152 | + /** |
| 153 | + * Token stream transformations. |
| 154 | + * This is where all the wiki-specific functionality is implemented. |
| 155 | + * See https://www.mediawiki.org/wiki/Future/Parser_development/Token_stream_transformations |
| 156 | + */ |
| 157 | + var tokenPreProcessor = new TokenTransformManager.SyncTokenTransformManager ( this.env ); |
| 158 | + var tokenExpander = new TokenTransformManager.AsyncTokenTransformManager ( |
| 159 | + this.makeInputPipeline.bind( this ), args, this.env ); |
| 160 | + tokenExpander.listenForTokensFrom ( tokenPreProcessor ); |
| 161 | + |
| 162 | + return { first: tokenPreProcessor, last: tokenExpander }; |
| 163 | +}; |
| 164 | + |
| 165 | + |
| 166 | +/** |
| 167 | + * Feed the parser pipeline with some input, the output is emitted in events. |
140 | 168 | * |
141 | 169 | * @method |
142 | 170 | * @param {Mixed} All arguments are passed through to the underlying input |
143 | 171 | * pipeline's first element's process() method. For a wikitext pipeline (the |
144 | | - * default), this would be the wikitext to tokenize. |
| 172 | + * default), this would be the wikitext to tokenize: |
| 173 | + * pipeline.parse ( wikiText ); |
145 | 174 | */ |
146 | 175 | ParserPipeline.prototype.parse = function ( ) { |
147 | 176 | // Set the pipeline in motion by feeding the first element with the given |
Index: trunk/extensions/VisualEditor/modules/parser/ext.core.TemplateHandler.js |
— | — | @@ -45,36 +45,44 @@ |
46 | 46 | * processes the template. |
47 | 47 | */ |
48 | 48 | TemplateHandler.prototype.onTemplate = function ( token, cb ) { |
| 49 | + |
| 50 | + this.parentCB = cb; |
| 51 | + this.origToken = token; |
| 52 | + |
49 | 53 | // check for 'subst:' |
50 | 54 | // check for variable magic names |
51 | 55 | // check for msg, msgnw, raw magics |
52 | 56 | // check for parser functions |
53 | 57 | |
54 | | - // create a new frame for argument and title expansions |
55 | | - var newFrame = { |
| 58 | + // create a new temporary frame for argument and title expansions |
| 59 | + var templateExpandData = { |
56 | 60 | args: {}, |
57 | 61 | env: frame.env, |
58 | | - target: token.attribs[0][1], // XXX: use data-target key instead! |
59 | | - // Also handle templates and args in |
60 | | - // target! |
61 | 62 | outstanding: 0, |
62 | | - cb: cb |
| 63 | + cb: cb, |
| 64 | + origToken: token |
63 | 65 | }, |
64 | | - argcb, |
| 66 | + transformCB, |
65 | 67 | i = 0, |
66 | 68 | kvs = [], |
67 | 69 | res, |
68 | 70 | kv; |
69 | 71 | // XXX: transform the target |
| 72 | + transformCB = this._returnArgValue.bind( this, { frame: templateExpandData } ); |
| 73 | + res = this.manager.transformTokens( token.target, transformCB ); |
| 74 | + if ( res.async ) { |
| 75 | + newFrame.outstanding++; |
| 76 | + } |
| 77 | + newFrame.target = res.tokens; |
70 | 78 | |
71 | | - |
| 79 | + |
72 | 80 | // transform each argument (key and value), and handle asynchronous returns |
73 | 81 | for ( var key in token.args ) { |
74 | 82 | if ( token.hasOwnProperty( key ) ) { |
75 | 83 | kv = { key: [], value: [] }; |
76 | 84 | // transform the value |
77 | | - argCB = this._returnArgValue.bind( this, { index: i, frame: newFrame } ); |
78 | | - res = frame.transformTokens( frame, args[key], argCB ); |
| 85 | + transformCB = this._returnArgValue.bind( this, { frame: templateExpandData, index: i } ); |
| 86 | + res = this.manager.transformTokens( args[key], transformCB ); |
79 | 87 | if ( res.async ) { |
80 | 88 | newFrame.outstanding++; |
81 | 89 | } |
— | — | @@ -90,6 +98,9 @@ |
91 | 99 | i++; |
92 | 100 | } |
93 | 101 | } |
| 102 | + |
| 103 | + // Move the above to AttributeTransformer class |
| 104 | + |
94 | 105 | |
95 | 106 | if ( newFrame.outstanding === 0 ) { |
96 | 107 | return this._expandTemplate ( newFrame ); |
— | — | @@ -102,9 +113,8 @@ |
103 | 114 | * Callback for async argument value expansions |
104 | 115 | */ |
105 | 116 | TemplateHandler.prototype._returnArgValue = function ( ref, tokens, notYetDone ) { |
106 | | - var frame = ref.frame, |
107 | | - res; |
108 | | - frame.args[ref.index].push( tokens ); |
| 117 | + var frame = ref.frame; |
| 118 | + frame.args[ref.index].value.push( tokens ); |
109 | 119 | if ( ! notYetDone ) { |
110 | 120 | frame.outstanding--; |
111 | 121 | if ( frame.outstanding === 0 ) { |
— | — | @@ -115,17 +125,60 @@ |
116 | 126 | }; |
117 | 127 | |
118 | 128 | /** |
| 129 | + * Callback for async argument key expansions |
| 130 | + */ |
| 131 | +TemplateHandler.prototype._returnArgKey = function ( ref, tokens, notYetDone ) { |
| 132 | + var frame = ref.frame; |
| 133 | + frame.args[ref.index].key.push( tokens ); |
| 134 | + if ( ! notYetDone ) { |
| 135 | + frame.outstanding--; |
| 136 | + if ( frame.outstanding === 0 ) { |
| 137 | + // this calls back to frame.cb, so no return here. |
| 138 | + this._expandTemplate( frame ); |
| 139 | + } |
| 140 | + } |
| 141 | +}; |
| 142 | + |
| 143 | +/** |
| 144 | + * Callback for async target expansion |
| 145 | + */ |
| 146 | +TemplateHandler.prototype._returnTarget = function ( ref, tokens, notYetDone ) { |
| 147 | + var frame = ref.frame; |
| 148 | + frame.target.push( tokens ); |
| 149 | + if ( ! notYetDone ) { |
| 150 | + frame.outstanding--; |
| 151 | + if ( frame.outstanding === 0 ) { |
| 152 | + // this calls back to frame.cb, so no return here. |
| 153 | + this._expandTemplate( frame ); |
| 154 | + } |
| 155 | + } |
| 156 | +}; |
| 157 | + |
| 158 | +/** |
119 | 159 | * Fetch, tokenize and token-transform a template after all arguments and the |
120 | 160 | * target were expanded in frame. |
121 | 161 | */ |
122 | 162 | TemplateHandler.prototype._expandTemplate = function ( frame ) { |
| 163 | + // First, check the target for loops |
| 164 | + this.manager.loopCheck.check( frame.target ); |
| 165 | + |
123 | 166 | // Create a new nested transformation pipeline for the input type |
124 | 167 | // (includes the tokenizer and synchronous stage-1 transforms for |
125 | 168 | // 'text/wiki' input). |
126 | 169 | // Returned pipe (for now): |
127 | 170 | // { first: tokenizer, last: AsyncTokenTransformManager } |
128 | | - var pipe = this.manager.newChildPipeline( inputType, args ); |
| 171 | + this.inputPipeline = this.manager.newChildPipeline( inputType, args ); |
129 | 172 | |
| 173 | + // Hook up the AsyncTokenTransformManager output events to call back our |
| 174 | + // parentCB. |
| 175 | + this.inputPipeline.last.addListener( 'chunk', this._onChunk.bind ( this ) ); |
| 176 | + this.inputPipeline.last.addListener( 'end', this._onEnd.bind ( this ) ); |
| 177 | + |
| 178 | + |
| 179 | + // Resolve a possibly relative link |
| 180 | + var templateName = this.env.resolveTitle( this.target, 'Template' ); |
| 181 | + this._fetchTemplateAndTitle( templateName, this._processTemplateAndTitle.bind( this ) ); |
| 182 | + |
130 | 183 | // Set up a pipeline: |
131 | 184 | // fetch template source -> tokenizer |
132 | 185 | // getInputPipeline( inputType ) |
— | — | @@ -151,9 +204,45 @@ |
152 | 205 | |
153 | 206 | |
154 | 207 | /** |
| 208 | + * Convert AsyncTokenTransformManager output chunks to parent callbacks |
| 209 | + */ |
| 210 | +TemplateHandler.prototype._onChunk = function( chunk ) { |
| 211 | + // We encapsulate the output by default, so collect tokens here. |
| 212 | + this.resultTokens = this.resultTokens.concat( chunk ); |
| 213 | +}; |
| 214 | + |
| 215 | +/** |
| 216 | + * Handle the end event by calling our parentCB with notYetDone set to false. |
| 217 | + */ |
| 218 | +TemplateHandler.prototype._onEnd = function( ) { |
| 219 | + // Encapsulate the template in a single token, which contains all the |
| 220 | + // information needed for the editor. |
| 221 | + var res = { |
| 222 | + type: 'container', |
| 223 | + tokens: this.resultTokens, // The editor needs HTML serialization instead |
| 224 | + args: this.manager.args, // Here, the editor needs wikitext. |
| 225 | + attribs: this.origToken.attribs // Hmm.. |
| 226 | + |
| 227 | + }; |
| 228 | + this.parentCB( res, false ); |
| 229 | +}; |
| 230 | + |
| 231 | + |
| 232 | + |
| 233 | +/** |
| 234 | + * Process a fetched template source |
| 235 | + */ |
| 236 | +TemplateHandler.prototype._processTemplateAndTitle = function( src, title ) { |
| 237 | + // Feed the pipeline. XXX: Support different formats. |
| 238 | + this.inputPipeline.process ( src ); |
| 239 | +}; |
| 240 | + |
| 241 | + |
| 242 | + |
| 243 | +/** |
155 | 244 | * Fetch a template |
156 | 245 | */ |
157 | | -TemplateHandler.prototype._fetchTemplateAndTitle = function( title, frame, callback ) { |
| 246 | +TemplateHandler.prototype._fetchTemplateAndTitle = function( title, callback ) { |
158 | 247 | // @fixme normalize name? |
159 | 248 | if (title in this.pageCache) { |
160 | 249 | // @fixme should this be forced to run on next event? |
— | — | @@ -163,7 +252,7 @@ |
164 | 253 | console.log(title); |
165 | 254 | console.log(this.pageCache); |
166 | 255 | $.ajax({ |
167 | | - url: frame.env.wgScriptPath + '/api' + frame.env.wgScriptExtension, |
| 256 | + url: this.manager.env.wgScriptPath + '/api' + this.manager.env.wgScriptExtension, |
168 | 257 | data: { |
169 | 258 | format: 'json', |
170 | 259 | action: 'query', |
Index: trunk/extensions/VisualEditor/modules/parser/mediawiki.parser.environment.js |
— | — | @@ -13,487 +13,64 @@ |
14 | 14 | this.domCache = options.domCache; |
15 | 15 | }; |
16 | 16 | |
17 | | -$.extend(MWParserEnvironment.prototype, { |
18 | | - // Does this need separate UI/content inputs? |
19 | | - formatNum: function( num ) { |
20 | | - return num + ''; |
21 | | - }, |
22 | | - |
23 | | - getVariable: function( varname, options ) { |
24 | | - // |
25 | | - }, |
26 | | - |
27 | | - /** |
28 | | - * @return MWParserFunction |
29 | | - */ |
30 | | - getParserFunction: function( name ) { |
31 | | - if (name in this.parserFunctions) { |
32 | | - return new this.parserFunctions[name]( this ); |
33 | | - } else { |
34 | | - return null; |
| 17 | +MWParserEnvironment.prototype.lookupKV = function ( kvs, key ) { |
| 18 | + var kv; |
| 19 | + for ( var i = 0, l = kvs.length; i < l; i++ ) { |
| 20 | + kv = kvs[i]; |
| 21 | + if ( kv[0] === key ) { |
| 22 | + // found, return it. |
| 23 | + return kv[1]; |
35 | 24 | } |
36 | | - }, |
37 | | - |
38 | | - /** |
39 | | - * @return MWParserTagHook |
40 | | - */ |
41 | | - getTagHook: function( name ) { |
42 | | - if (name in this.tagHooks) { |
43 | | - return new this.tagHooks[name](this); |
44 | | - } else { |
45 | | - return null; |
46 | | - } |
47 | | - }, |
48 | | - |
49 | | - /** |
50 | | - * @fixme do this for real eh |
51 | | - */ |
52 | | - resolveTitle: function( name, namespace ) { |
53 | | - // hack! |
54 | | - if (name.indexOf(':') == -1 && typeof namespace ) { |
55 | | - // hack hack hack |
56 | | - name = namespace + ':' + name; |
57 | | - } |
58 | | - return name; |
59 | | - }, |
60 | | - |
61 | | - /** |
62 | | - * Async. |
63 | | - * |
64 | | - * @todo make some optimizations for fetching multiple at once |
65 | | - * |
66 | | - * @param string name |
67 | | - * @param function(text, err) callback |
68 | | - */ |
69 | | - fetchTemplate: function( title, callback ) { |
70 | | - this.fetchTemplateAndTitle( title, function( text, title, err ) { |
71 | | - callback(title, err); |
72 | | - }); |
73 | | - }, |
74 | | - |
75 | | - fetchTemplateAndTitle: function( title, callback ) { |
76 | | - // @fixme normalize name? |
77 | | - if (title in this.pageCache) { |
78 | | - // @fixme should this be forced to run on next event? |
79 | | - callback( this.pageCache[title], title ); |
80 | | - } else { |
81 | | - // whee fun hack! |
82 | | - console.log(title); |
83 | | - console.log(this.pageCache); |
84 | | - $.ajax({ |
85 | | - url: wgScriptPath + '/api' + wgScriptExtension, |
86 | | - data: { |
87 | | - format: 'json', |
88 | | - action: 'query', |
89 | | - prop: 'revisions', |
90 | | - rvprop: 'content', |
91 | | - titles: name |
92 | | - }, |
93 | | - success: function(data, xhr) { |
94 | | - var src = null, title = null; |
95 | | - $.each(data.query.pages, function(i, page) { |
96 | | - if (page.revisions && page.revisions.length) { |
97 | | - src = page.revisions[0]['*']; |
98 | | - title = page.title; |
99 | | - } |
100 | | - }); |
101 | | - if (typeof src !== 'string') { |
102 | | - callback(null, null, 'Page not found'); |
103 | | - } else { |
104 | | - callback(src, title); |
105 | | - } |
106 | | - }, |
107 | | - error: function(msg) { |
108 | | - callback(null, null, 'Page/template fetch failure'); |
109 | | - }, |
110 | | - dataType: 'json', |
111 | | - cache: false // @fixme caching, versions etc? |
112 | | - }, 'json'); |
113 | | - } |
114 | | - }, |
115 | | - |
116 | | - getTemplateDom: function( title, callback ) { |
117 | | - var self = this; |
118 | | - if (title in this.domCache) { |
119 | | - callback(this.domCache[title], null); |
120 | | - return; |
121 | | - } |
122 | | - this.fetchTemplateAndTitle( title, function( text, title, err ) { |
123 | | - if (err) { |
124 | | - callback(null, err); |
125 | | - return; |
126 | | - } |
127 | | - self.pageCache[title] = text; |
128 | | - self.parser.parseToTree( text, function( templateTree, err ) { |
129 | | - this.domCache[title] = templateTree; |
130 | | - callback(templateTree, err); |
131 | | - }); |
132 | | - }); |
133 | | - }, |
134 | | - |
135 | | - braceSubstitution: function( templateNode, frame, callback ) { |
136 | | - // stuff in Parser.braceSubstitution |
137 | | - // expand/flatten the 'title' piece (to get the template reference) |
138 | | - var self = this; |
139 | | - frame.flatten(self.resolveTitle(templateNode.name, 'Template'), function(templateName, err) { |
140 | | - if (err) { |
141 | | - callback(null, err); |
142 | | - return; |
143 | | - } |
144 | | - var out = { |
145 | | - type: 'placeholder', |
146 | | - orig: templateNode, |
147 | | - contents: [] |
148 | | - }; |
149 | | - |
150 | | - // check for 'subst:' |
151 | | - // check for variable magic names |
152 | | - // check for msg, msgnw, raw magics |
153 | | - // check for parser functions |
154 | | - |
155 | | - // resolve template name |
156 | | - // load template w/ canonical name |
157 | | - // load template w/ variant names |
158 | | - // recursion depth check |
159 | | - // fetch from DB or interwiki |
160 | | - // infinte loop check |
161 | | - self.getTemplateDom(templateName, function(dom, err) { |
162 | | - // Expand in-place! |
163 | | - var templateFrame = frame.newChild(templateNode.params || {}); |
164 | | - templateFrame.expand(dom, 0, function(expandedTemplateNode) { |
165 | | - out.contents = expandedTemplateNode.contents; |
166 | | - callback(out); |
167 | | - return; // done |
168 | | - }); |
169 | | - return; // wait for async |
170 | | - }); |
171 | | - }); |
172 | | - }, |
173 | | - |
174 | | - argSubstitution: function( argNode, frame, callback ) { |
175 | | - frame.flatten(argNode.name, function(argName, err) { |
176 | | - if (err) { |
177 | | - callback(null, err); |
178 | | - return; |
179 | | - } |
180 | | - |
181 | | - var arg = frame.getArgument(argName); |
182 | | - console.log(argName, arg, frame); |
183 | | - if (arg === false && 'params' in argNode && argNode.params.length) { |
184 | | - // No match in frame, use the supplied default |
185 | | - arg = argNode.params[0].val; |
186 | | - } |
187 | | - var out = { |
188 | | - type: 'placeholder', |
189 | | - orig: argNode, |
190 | | - contents: [arg] |
191 | | - }; |
192 | | - callback(out); |
193 | | - }); |
194 | 25 | } |
| 26 | + // nothing found! |
| 27 | + return null; |
| 28 | +}; |
195 | 29 | |
196 | 30 | |
197 | | -}); |
| 31 | +// Does this need separate UI/content inputs? |
| 32 | +MWParserEnvironment.prototype.formatNum = function( num ) { |
| 33 | + return num + ''; |
| 34 | +}; |
198 | 35 | |
199 | | -function PPFrame(env) { |
200 | | - this.env = env; |
201 | | - this.loopCheckHash = []; |
202 | | - this.depth = 0; |
203 | | -} |
204 | | - |
205 | | -// Flag constants |
206 | | -$.extend(PPFrame, { |
207 | | - NO_ARGS: 1, |
208 | | - NO_TEMPLATES: 2, |
209 | | - STRIP_COMMENTS: 4, |
210 | | - NO_IGNORE: 8, |
211 | | - RECOVER_COMMENTS: 16 |
212 | | -}); |
213 | | -PPFrame.RECOVER_ORIG = PPFrame.NO_ARGS |
214 | | - | PPFrame.NO_TEMPLATES |
215 | | - | PPFrame.STRIP_COMMENTS |
216 | | - | PPFrame.NO_IGNORE |
217 | | - | PPFrame.RECOVER_COMMENTS; |
218 | | - |
219 | | -$.extend(PPFrame.prototype, { |
220 | | - newChild: function(args, title) { |
| 36 | +MWParserEnvironment.prototype.getVariable = function( varname, options ) { |
221 | 37 | // |
222 | | - var child = new PPFrame(this.env); |
223 | | - child.args = args || {}; |
224 | | - child.title = title; |
225 | | - return child; |
226 | | - }, |
| 38 | +}; |
227 | 39 | |
228 | | - /** |
229 | | - * Using simple recursion for now -- PHP version is a little fancier. |
230 | | - * |
231 | | - * The iterator loop is set off in a closure so we can continue it after |
232 | | - * waiting for an asynchronous template fetch. |
233 | | - * |
234 | | - * Note that this is inefficient, as we have to wait for the entire round |
235 | | - * trip before continuing -- in browser-based work this may be particularly |
236 | | - * slow. This can be mitigated by prefetching templates based on previous |
237 | | - * knowledge or an initial tree-walk. |
238 | | - * |
239 | | - * @param {object} tree |
240 | | - * @param {number} flags |
241 | | - * @param {function(tree, error)} callback |
242 | | - */ |
243 | | - expand: function(root, flags, callback) { |
244 | | - /** |
245 | | - * Clone a node, but give the clone an empty contents |
246 | | - */ |
247 | | - var cloneNode = function(node) { |
248 | | - var out = $.extend({}, node); |
249 | | - out.contents = []; |
250 | | - return out; |
251 | | - } |
252 | | - |
253 | | - // stub node to write into |
254 | | - var rootOut = cloneNode(root); |
255 | | - |
256 | | - var self = this, |
257 | | - env = self.env, |
258 | | - expansionDepth = 0, |
259 | | - outStack = [{contents: []}, cloneNode(root)], |
260 | | - iteratorStack = [false, root], |
261 | | - indexStack = [0, 0], |
262 | | - contextNode = false, |
263 | | - newIterator = false, |
264 | | - continuing = false, |
265 | | - iters = 0, |
266 | | - maxIters = 10; // for debugging |
267 | | - |
268 | | - if (env.debug) { |
269 | | - var $chunk = $('<div style="border: solid 1px blue">').append('<hr>'); |
270 | | - $chunk.append('<h3>Original</h3>'); |
271 | | - $chunk.nodeTree(root); |
272 | | - $chunk.appendTo('body'); |
273 | | - var debug = function(label, node) { |
274 | | - $('<h3></h3>').text(label).appendTo($chunk); |
275 | | - if (typeof node == "string" || typeof node == "number") { |
276 | | - $('<p>').text(node).appendTo($chunk); |
277 | | - } else if (node) { |
278 | | - $chunk.nodeTree(node); |
279 | | - } |
280 | | - }; |
281 | | - } else { |
282 | | - var debug = function() {}; |
283 | | - } |
284 | | - var iteration = function() { |
285 | | - // This while loop is a tail call recursion optimization simulator :) |
286 | | - while (iteratorStack.length > 1) { |
287 | | - var level = outStack.length - 1, |
288 | | - iteratorNode = iteratorStack[level], |
289 | | - out = outStack[level], |
290 | | - index = indexStack[level]; // ???? |
291 | | - |
292 | | - if (env.debug) { |
293 | | - $chunk.append('<hr>'); |
294 | | - iters++; |
295 | | - var $h = $('<h3>').text('iter ' + iters).attr('id', 'iter' + iters); |
296 | | - if (iters > 1) { |
297 | | - $h.append(' '); |
298 | | - $('<a>').attr('href', '#iter' + (iters - 1)).text('prev').appendTo($h); |
299 | | - } |
300 | | - $h.append(' '); |
301 | | - $('<a>').attr('href', '#iter' + (iters + 1)).text('next').appendTo($h); |
302 | | - $chunk.append($h); |
303 | | - |
304 | | - if (iters > maxIters) { |
305 | | - debug('aborted'); |
306 | | - return; |
307 | | - } |
308 | | - $chunk.append('<h3>level ' + level + '</h3>'); |
309 | | - } |
310 | | - debug('index', index); |
311 | | - if (continuing) { |
312 | | - // If we're re-entering from an asynchronous data fetch, |
313 | | - // skip over this part, we've done it before. |
314 | | - continuing = false; |
315 | | - } else { |
316 | | - newIterator = false; |
317 | | - if (index >= iteratorNode.contents.length) { |
318 | | - // All done with this iterator. |
319 | | - iteratorStack[level] = false; |
320 | | - contextNode = false; |
321 | | - } else { |
322 | | - // Increment for the next round... |
323 | | - contextNode = iteratorNode.contents[index]; |
324 | | - indexStack[level]++; |
325 | | - index++; |
326 | | - } |
327 | | - debug('contextNode', contextNode); |
328 | | - debug('indexStack (next)', indexStack); |
329 | | - debug('outStack', outStack); |
330 | | - |
331 | | - if (contextNode === false) { |
332 | | - // nothing to do |
333 | | - } else if (typeof contextNode === 'string') { |
334 | | - out.contents.push(contextNode); |
335 | | - } else if (contextNode.type === 'template') { |
336 | | - // Double-brace expansion |
337 | | - continuing = true; |
338 | | - self.env.braceSubstitution(contextNode, self, function(replacementNode, err) { |
339 | | - //out.contents.push(replacementNode); |
340 | | - newIterator = replacementNode; |
341 | | - // ... and continue on the next node! |
342 | | - iteration(); |
343 | | - }); |
344 | | - return; // pause for async work... |
345 | | - } else if (contextNode.type == 'tplarg') { |
346 | | - // Triple-brace expansion |
347 | | - continuing = true; |
348 | | - self.env.argSubstitution(contextNode, self, function(replacementNode, err) { |
349 | | - //out.contents.push(replacementNode); |
350 | | - newIterator = replacementNode; |
351 | | - // ... and continue on the next node! |
352 | | - iteration(); |
353 | | - }); |
354 | | - return; // pause for async work... |
355 | | - } else { |
356 | | - if ('contents' in contextNode && contextNode.contents.length) { |
357 | | - // Generic recursive expansion |
358 | | - newIterator = contextNode; |
359 | | - debug('diving into child'); |
360 | | - } else { |
361 | | - // No children; push as-is. |
362 | | - out.contents.push(contextNode); |
363 | | - debug('no children'); |
364 | | - } |
365 | | - } |
366 | | - } |
367 | | - |
368 | | - if (newIterator !== false) { |
369 | | - outStack.push(cloneNode(newIterator)); |
370 | | - iteratorStack.push(newIterator); |
371 | | - indexStack.push(0); |
372 | | - debug('iterator stack push!'); |
373 | | - debug('outStack', outStack); |
374 | | - debug('iteratorStack', iteratorStack); |
375 | | - debug('indexStack', indexStack); |
376 | | - } else if ( iteratorStack[level] === false) { |
377 | | - // Return accumulated value to parent |
378 | | - // With tail recursion |
379 | | - debug('returning output up the stack'); |
380 | | - while (iteratorStack[level] === false && level > 0) { |
381 | | - outStack[level - 1].contents.push(out); |
382 | | - outStack.pop(); |
383 | | - iteratorStack.pop(); |
384 | | - indexStack.pop(); |
385 | | - level--; |
386 | | - } |
387 | | - } |
388 | | - debug('end of iteration'); |
389 | | - |
390 | | - // hack! |
391 | | - if (iteratorStack.length > 1) { |
392 | | - // Run us after running the event loop |
393 | | - setTimeout(iteration, 0); |
394 | | - return; |
395 | | - } |
396 | | - } |
397 | | - // We've reached the end of the loop! |
398 | | - --expansionDepth; |
399 | | - var finalOut = outStack.pop().contents[0]; |
400 | | - debug('done', finalOut); |
401 | | - callback(finalOut, null); |
402 | | - }; |
403 | | - iteration(); |
404 | | - }, |
405 | | - |
406 | | - flatten: function(root, callback) { |
407 | | - new MWTreeSerializer(this).treeToSource(root, callback); |
408 | | - }, |
409 | | - |
410 | | - implodeWithFlags: function(sep, flags) { |
411 | | - |
412 | | - }, |
413 | | - |
414 | | - implode: function(sep) { |
415 | | - |
416 | | - }, |
417 | | - |
418 | | - virtualImport: function(sep) { |
419 | | - |
420 | | - }, |
421 | | - |
422 | | - virtualBracketedImplode: function(start, sep, end /*, ... */ ) { |
423 | | - |
424 | | - }, |
425 | | - |
426 | | - isEmpty: function() { |
427 | | - |
428 | | - }, |
429 | | - |
430 | | - getArguments: function() { |
431 | | - |
432 | | - }, |
433 | | - |
434 | | - getNumberedArguments: function() { |
435 | | - |
436 | | - }, |
437 | | - |
438 | | - getNamedArguments: function() { |
439 | | - |
440 | | - }, |
441 | | - |
442 | | - getArgument: function( name ) { |
443 | | - if (name in this.args) { |
444 | | - return this.args[name]; |
445 | | - } else { |
446 | | - return false; |
447 | | - } |
448 | | - }, |
449 | | - |
450 | | - loopCheck: function(title) { |
451 | | - }, |
452 | | - |
453 | | - isTemplate: function() { |
454 | | - |
455 | | - } |
456 | | - |
457 | | -}); |
458 | | - |
459 | | - |
460 | | - |
461 | 40 | /** |
462 | | - * @parm MWParserEnvironment env |
463 | | - * @constructor |
| 41 | + * @return MWParserFunction |
464 | 42 | */ |
465 | | -MWParserTagHook = function( env ) { |
466 | | - if (!env) { |
467 | | - throw new Error( 'Tag hook requires a parser environment.' ); |
| 43 | +MWParserEnvironment.prototype.getParserFunction = function( name ) { |
| 44 | + if (name in this.parserFunctions) { |
| 45 | + return new this.parserFunctions[name]( this ); |
| 46 | + } else { |
| 47 | + return null; |
468 | 48 | } |
469 | | - this.env = env; |
470 | 49 | }; |
471 | 50 | |
472 | 51 | /** |
473 | | - * @param string text (or a parse tree?) |
474 | | - * @param object params map of named parameters (strings or parse frames?) |
475 | | - * @return either a string or a parse frame -- finalize this? |
| 52 | + * @return MWParserTagHook |
476 | 53 | */ |
477 | | -MWParserTagHook.execute = function( text, params ) { |
478 | | - return ''; |
479 | | -}; |
480 | | - |
481 | | - |
482 | | -MWParserFunction = function( env) { |
483 | | - if (!env) { |
484 | | - throw new Error( 'Parser function requires a parser environment.'); |
| 54 | +MWParserEnvironment.prototype.getTagHook = function( name ) { |
| 55 | + if (name in this.tagHooks) { |
| 56 | + return new this.tagHooks[name](this); |
| 57 | + } else { |
| 58 | + return null; |
485 | 59 | } |
486 | | - this.env = env; |
487 | 60 | }; |
488 | 61 | |
489 | 62 | /** |
490 | | - * @param string text (or a parse tree?) |
491 | | - * @param object params map of named parameters (strings or parse frames?) |
492 | | - * @return either a string or a parse frame -- finalize this? |
| 63 | + * @fixme do this for real eh |
493 | 64 | */ |
494 | | -MWParserFunction.execute = function( text, params ) { |
495 | | - return ''; |
| 65 | +MWParserEnvironment.prototype.resolveTitle = function( name, namespace ) { |
| 66 | + // hack! |
| 67 | + if (name.indexOf(':') == -1 && typeof namespace ) { |
| 68 | + // hack hack hack |
| 69 | + name = namespace + ':' + name; |
| 70 | + } |
| 71 | + return name; |
496 | 72 | }; |
497 | 73 | |
| 74 | + |
498 | 75 | if (typeof module == "object") { |
499 | 76 | module.exports.MWParserEnvironment = MWParserEnvironment; |
500 | 77 | } |