jenkins-bot has submitted this change and it was merged.

Change subject: Split parsoidService into more manageable files
......................................................................


Split parsoidService into more manageable files

Change-Id: I9a2a573e6afe14c911d42d4d5ecd048eff7f1865
---
M api/ParsoidService.js
A api/routes.js
A api/utils.js
3 files changed, 776 insertions(+), 728 deletions(-)

Approvals:
  Subramanya Sastry: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/api/ParsoidService.js b/api/ParsoidService.js
index 903b281..bf6c2ba 100644
--- a/api/ParsoidService.js
+++ b/api/ParsoidService.js
@@ -1,309 +1,34 @@
-/*
+/**
  * Simple Parsoid web service.
  */
 "use strict";
 
 require('../lib/core-upgrade.js');
 
-/**
- * @class ParserServiceModule
- * @singleton
- * @private
- */
-
 // global includes
 var express = require('express'),
-       domino = require('domino'),
        hbs = require('handlebars'),
-       childProc = require('child_process'),
        cluster = require('cluster'),
-       fs = require('fs'),
-       path = require('path'),
-       url = require('url'),
-       util = require('util'),
-       pkg = require('../package.json'),
-       Diff = require('../lib/mediawiki.Diff.js').Diff,
-       LogData = require('../lib/LogData.js').LogData;
+       path = require('path');
 
-// local includes
-var mp = '../lib/';
+// relative includes
+var mp = '../lib/',
+       ParsoidConfig = require( mp + 'mediawiki.ParsoidConfig' ).ParsoidConfig;
 
 
 function ParsoidService(options) {
-       /**
-        * The name of this instance.
-        * @property {string}
-        */
-       var instanceName = cluster.isWorker ? 'worker(' + process.pid + ')' : 
'master';
 
-       console.log( ' - ' + instanceName + ' loading...' );
+       var instanceName = cluster.isWorker
+               ? 'worker(' + process.pid + ')'
+               : 'master';
 
-       var WikitextSerializer = require(mp + 
'mediawiki.WikitextSerializer.js').WikitextSerializer,
-               SelectiveSerializer = require( mp + 
'mediawiki.SelectiveSerializer.js' ).SelectiveSerializer,
-               Util = require( mp + 'mediawiki.Util.js' ).Util,
-               DU = require( mp + 'mediawiki.DOMUtils.js' ).DOMUtils,
-               libtr = require(mp + 'mediawiki.ApiRequest.js'),
-               ParsoidConfig = require( mp + 'mediawiki.ParsoidConfig' 
).ParsoidConfig,
-               MWParserEnvironment = require( mp + 
'mediawiki.parser.environment.js' ).MWParserEnvironment,
-               TemplateRequest = libtr.TemplateRequest;
+       console.log( " - %s loading ...", instanceName );
 
-
-/**
-        * Set header, but only if response hasn't been sent.
-        *
-        * @method
-        * @param {MWParserEnvironment} env
-        * @param {Response} res The response object from our routing function.
-        * @property {Function} Serializer
-        */
-       function setHeader (res, env) {
-               if (env.responseSent) {
-                       return;
-               } else {
-                       res.setHeader.apply(res, 
Array.prototype.slice.call(arguments, 2));
-               }
-       }
-
-       /**
-        * End response, but only if response hasn't been sent.
-        *
-        * @method
-        * @param {MWParserEnvironment} env
-        * @param {Response} res The response object from our routing function.
-        * @property {Function} Serializer
-        */
-       function endResponse(res, env) {
-               if (env.responseSent) {
-                       return;
-               } else {
-                       env.responseSent = true;
-                       res.end.apply(res, 
Array.prototype.slice.call(arguments, 2));
-                       env.log("end/response");
-               }
-       }
-
-       /**
-        * Send response, but only if response hasn't been sent.
-        *
-        * @method
-        * @param {MWParserEnvironment} env
-        * @param {Response} res The response object from our routing function.
-        * @property {Function} Serializer
-        */
-       function sendResponse(res, env) {
-               if (env.responseSent) {
-                       return;
-               } else {
-                       env.responseSent = true;
-                       res.send.apply(res, 
Array.prototype.slice.call(arguments, 2));
-               }
-       }
-
-       /**
-        * Render response, but only if response hasn't been sent.
-        */
-
-       function renderResponse(res, env) {
-               if (env.responseSent) {
-                       return;
-               } else {
-                       env.responseSent = true;
-                       res.render.apply(res, 
Array.prototype.slice.call(arguments, 2));
-               }
-       }
-
-       function jsonResponse(res, env) {
-               if (env.responseSent) {
-                       return;
-               } else {
-                       env.responseSent = true;
-                       res.json.apply(res, 
Array.prototype.slice.call(arguments, 2));
-               }
-       }
-
-       /**
-        * The global parsoid configuration object.
-        * @property {ParsoidConfig}
-        */
+       // The global parsoid configuration object.
        var parsoidConfig = new ParsoidConfig( options, null );
 
-       /**
-        * The serializer to use for the web requests.
-        * @property {Function} Serializer
-        */
-       var Serializer = parsoidConfig.useSelser ? SelectiveSerializer : 
WikitextSerializer;
-
-       /**
-        * Get the interwiki regexp.
-        *
-        * @method
-        * @returns {RegExp} The regular expression that matches to all 
interwikis accepted by the API.
-        */
-       var interwikiRE;
-       function getInterwikiRE() {
-               // this RE won't change -- so, cache it
-               if (!interwikiRE) {
-                       interwikiRE = parsoidConfig.interwikiRegexp;
-               }
-               return interwikiRE;
-       }
-
-       var htmlSpecialChars = function ( s ) {
-               return s.replace(/&/g,'&')
-                       .replace(/</g,'&lt;')
-                       .replace(/"/g,'&quot;')
-                       .replace(/'/g,'&#039;');
-       };
-
-       var roundTripDiff = function ( selser, req, res, env, document ) {
-               var out = [];
-
-               var finalCB =  function () {
-                       var i;
-                       out = out.join('');
-
-                       // Strip selser trigger comment
-                       out = out.replace(/<!--rtSelserEditTestComment-->\n*$/, 
'');
-
-                       // Emit base href so all relative urls resolve properly
-                       var hNodes = document.body.firstChild.childNodes;
-                       var headNodes = "";
-                       for (i = 0; i < hNodes.length; i++) {
-                               if (hNodes[i].nodeName.toLowerCase() === 
'base') {
-                                       headNodes += 
DU.serializeNode(hNodes[i]);
-                                       break;
-                               }
-                       }
-
-                       var bNodes = document.body.childNodes;
-                       var bodyNodes = "";
-                       for (i = 0; i < bNodes.length; i++) {
-                               bodyNodes += DU.serializeNode(bNodes[i]);
-                       }
-
-                       var htmlSpeChars = htmlSpecialChars(out);
-
-                       var src = env.page.src.replace(/\n(?=\n)/g, '\n ');
-                       out = out.replace(/\n(?=\n)/g, '\n ');
-
-                       var patch = Diff.convertChangesToXML( 
Diff.diffLines(src, out) );
-
-                       setHeader(res, env, 'X-Parsoid-Performance', 
env.getPerformanceHeader());
-
-                       renderResponse(res, env, "roundtrip", {
-                               headers: headNodes,
-                               bodyNodes: bodyNodes,
-                               htmlSpeChars: htmlSpeChars,
-                               patch: patch,
-                               reqUrl: req.url
-                       });
-
-                       env.log("info", "completed parsing in", 
env.performance.duration, "ms");
-               };
-
-               // Re-parse the HTML to uncover foster-parenting issues
-               document = domino.createDocument(document.outerHTML);
-
-               var Serializer = selser ? SelectiveSerializer : 
WikitextSerializer;
-               new Serializer({ env: env }).serializeDOM(
-                       document.body,
-                       function( chunk ) { out.push(chunk); },
-                       finalCB
-               );
-       };
-
-       function handleCacheRequest( env, req, res, cb, src, cacheErr, cacheSrc 
) {
-               var errorHandlingCB = function ( src, err, doc ) {
-                       if ( err ) {
-                               env.log("fatal/request", err);
-                               return;
-                       }
-                       cb( req, res, src, doc );
-               };
-
-               if ( cacheErr ) {
-                       // No luck with the cache request, just proceed as 
normal.
-                       Util.parse(env, errorHandlingCB, null, src);
-                       return;
-               }
-               // Extract transclusion and extension content from the DOM
-               var expansions = DU.extractExpansions(DU.parseHTML(cacheSrc));
-
-               // Figure out what we can reuse
-               var parsoidHeader = JSON.parse(req.headers['x-parsoid'] || 
'{}');
-               if (parsoidHeader.cacheID) {
-                       if (parsoidHeader.mode === 'templates') {
-                               // Transclusions need to be updated, so don't 
reuse them.
-                               expansions.transclusions = {};
-                       } else if (parsoidHeader.mode === 'files') {
-                               // Files need to be updated, so don't reuse 
them.
-                               expansions.files = {};
-                       }
-               }
-
-               // pass those expansions into Util.parse to prime the caches.
-               //console.log('expansions:', expansions);
-               Util.parse(env, errorHandlingCB, null, src, expansions);
-       }
-
-       var parse = function ( env, req, res, cb, err, src_and_metadata ) {
-               if ( err ) {
-                       env.log("fatal/request", err);
-                       return;
-               }
-
-               // Set the source
-               env.setPageSrcInfo( src_and_metadata );
-
-               // Now env.page.meta.title has the canonical title, and
-               // env.page.meta.revision.parentid has the predecessor oldid
-
-               // See if we can reuse transclusion or extension expansions.
-               if (env.conf.parsoid.parsoidCacheURI &&
-                               // And don't parse twice for recursive parsoid 
requests
-                               ! req.headers['x-parsoid-request'])
-               {
-                       // Try to retrieve a cached copy of the content so that 
we can recycle
-                       // template and / or extension expansions.
-                       var parsoidHeader = JSON.parse(req.headers['x-parsoid'] 
|| '{}'),
-                               // If we get a prevID passed in in X-Parsoid 
(from our PHP
-                               // extension), use that explicitly. Otherwise 
default to the
-                               // parentID.
-                               cacheID = parsoidHeader.cacheID ||
-                                       env.page.meta.revision.parentid,
-                               cacheRequest = new 
libtr.ParsoidCacheRequest(env,
-                                       env.page.meta.title, cacheID);
-                       cacheRequest.once('src',
-                                       handleCacheRequest.bind(null, env, req, 
res, cb, env.page.src));
-               } else {
-                       handleCacheRequest(env, req, res, cb, env.page.src, 
"Recursive request", null);
-               }
-       };
-
-
-       /**
-        * Send a redirect response with optional code and a relative URL
-        *
-        * (Returns if a response has already been sent.)
-        * This is not strictly HTTP spec conformant, but works in most 
clients. More
-        * importantly, it works both behind proxies and on the internal 
network.
-        */
-       function relativeRedirect(args) {
-               if (!args.code) {
-                       args.code = 302; // moved temporarily
-               }
-
-               if (args.res && args.env && args.env.responseSent ) {
-                       return;
-               } else {
-                       args.res.writeHead(args.code, {
-                               'Location': args.path
-                       });
-                       args.res.end();
-               }
-       }
-
-       /* -------------------- web app access points below 
--------------------- */
+       // Load routes
+       var routes = require('./routes')( parsoidConfig );
 
        var app = express.createServer();
 
@@ -324,7 +49,7 @@
        app.use(express.favicon(path.join(__dirname, "favicon.ico")));
 
        // Increase the form field size limit from the 2M default.
-       app.use(express.bodyParser({maxFieldsSize: 15 * 1024 * 1024}));
+       app.use(express.bodyParser({ maxFieldsSize: 15 * 1024 * 1024 }));
 
        // Support gzip / deflate transfer-encoding
        app.use(express.compress());
@@ -332,441 +57,8 @@
        // limit upload file size
        app.use(express.limit('15mb'));
 
-       app.get('/', function(req, res){
-               res.render('home');
-       });
-
-       function interParams( req, res, next ) {
-               res.local('iwp', req.params[0] || parsoidConfig.defaultWiki || 
'');
-               res.local('pageName', req.params[1] || '');
-               res.local('oldid', req.query.oldid || null);
-               next();
-       }
-
-       function parserEnvMw( req, res, next ) {
-               MWParserEnvironment.getParserEnv( parsoidConfig, null, 
res.local('iwp'), res.local('pageName'), req.headers.cookie, function ( err, 
env ) {
-
-                       function errCB ( res, env, logData, callback ) {
-                               try {
-                                       if ( !env.responseSent ) {
-                                               setHeader(res, env, 
'Content-Type', 'text/plain; charset=UTF-8' );
-                                               sendResponse(res, env, 
logData.fullMsg(), logData.code || 500);
-                                               if ( typeof callback === 
'function' ) {
-                                                       res.on('finish', 
callback);
-                                               }
-                                               return;
-                                       }
-                               } catch (e) {
-                                       console.log( e.stack || e );
-                                       res.end();
-                               }
-                               if ( typeof callback === 'function' ) {
-                                       callback();
-                               }
-                       }
-
-                       if ( err ) {
-                               return errCB(res, {}, new LogData(null, 
"error", err));
-                       }
-
-                       env.logger.registerBackend(/fatal(\/.*)?/, 
errCB.bind(this, res, env));
-                       res.local("env", env);
-                       next();
-               });
-       }
-
-       // robots.txt: no indexing.
-       app.get(/^\/robots.txt$/, function ( req, res ) {
-               res.end("User-agent: *\nDisallow: /\n" );
-       });
-
-       // Redirects for old-style URL compatibility
-       app.get( new RegExp( '^/((?:_rt|_rtve)/)?(' + getInterwikiRE() +
-                                       '):(.*)$' ), function ( req, res ) {
-               if ( req.params[0] ) {
-                       relativeRedirect({"path" : '/' + req.params[0] + 
req.params[1] + '/' + req.params[2], "res" : res, "code" : 301});
-               } else {
-                       relativeRedirect({"path" : '/' + req.params[1] + '/' + 
req.params[2], "res" : res, "code": 301});
-               }
-               res.end();
-       });
-
-       function action( res ) {
-               return [ "", res.local('iwp'), res.local('pageName') ].join( 
"/" );
-       }
-
-       // Form-based HTML DOM -> wikitext interface for manual testing
-       app.get( new RegExp('/_html/(?:(' + getInterwikiRE() + ')/(.*))?'), 
interParams, parserEnvMw, function ( req, res ) {
-               var env = res.local('env');
-               renderResponse(res, env, "form", {
-                       title: "Your HTML DOM:",
-                       action: action(res),
-                       name: "html"
-               });
-       });
-
-       // Form-based wikitext -> HTML DOM interface for manual testing
-       app.get( new RegExp('/_wikitext/(?:(' + getInterwikiRE() + ')/(.*))?'), 
interParams, parserEnvMw, function ( req, res ) {
-               var env = res.local('env');
-               renderResponse(res, env, "form", {
-                       title: "Your wikitext:",
-                       action: action(res),
-                       name: "wt"
-               });
-       });
-
-       // Round-trip article testing
-       app.get( new RegExp('/_rt/(?:(' + getInterwikiRE() + ')/(.*))?'), 
interParams, parserEnvMw, function(req, res) {
-               var env = res.local('env');
-               var target = env.resolveTitle( env.normalizeTitle( 
env.page.name ), '' );
-
-               req.connection.setTimeout(300 * 1000);
-               env.log('info', 'starting parsing');
-
-               var oldid = null;
-               if ( req.query.oldid ) {
-                       oldid = req.query.oldid;
-               }
-               var tpr = new TemplateRequest( env, target, oldid );
-               tpr.once('src', parse.bind( tpr, env, req, res, 
roundTripDiff.bind( null, false ) ));
-       });
-
-       // Round-trip article testing with newline stripping for editor-created 
HTML
-       // simulation
-       app.get( new RegExp('/_rtve/(' + getInterwikiRE() + ')/(.*)'), 
interParams, parserEnvMw, function(req, res) {
-               var env = res.local('env');
-               var target = env.resolveTitle( env.normalizeTitle( 
env.page.name ), '' );
-
-               env.log('info', 'starting parsing');
-               var oldid = null;
-               if ( req.query.oldid ) {
-                       oldid = req.query.oldid;
-               }
-               var tpr = new TemplateRequest( env, target, oldid ),
-                       cb = function ( req, res, src, document ) {
-                               // strip newlines from the html
-                               var html = 
document.innerHTML.replace(/[\r\n]/g, ''),
-                                       newDocument = DU.parseHTML(html);
-                               roundTripDiff( false, req, res, src, 
newDocument );
-                       };
-
-               tpr.once('src', parse.bind( tpr, env, req, res, cb ));
-       });
-
-       // Round-trip article testing with selser over re-parsed HTML.
-       app.get( new RegExp('/_rtselser/(' + getInterwikiRE() + ')/(.*)'), 
interParams, parserEnvMw, function (req, res) {
-               var env = res.local('env');
-               var target = env.resolveTitle( env.normalizeTitle( 
env.page.name ), '' );
-
-               env.log('info', 'starting parsing');
-               var oldid = null;
-               if ( req.query.oldid ) {
-                       oldid = req.query.oldid;
-               }
-               var tpr = new TemplateRequest( env, target, oldid ),
-                       tprCb = function ( req, res, src, document ) {
-                               var newDocument = DU.parseHTML( 
DU.serializeNode(document) ),
-                                       newNode = 
newDocument.createComment('rtSelserEditTestComment');
-                               newDocument.body.appendChild(newNode);
-                               roundTripDiff( true, req, res, src, newDocument 
);
-                       };
-
-               tpr.once( 'src', parse.bind( tpr, env, req, res, tprCb ) );
-       });
-
-       // Form-based round-tripping for manual testing
-       app.get( new RegExp('/_rtform/(?:(' + getInterwikiRE() + ')/(.*))?'), 
interParams, parserEnvMw, function ( req, res ) {
-               var env = res.local('env');
-               renderResponse(res, env, "form", {
-                       title: "Your wikitext:",
-                       action: "/_rtform/" + res.local('pageName'),
-                       name: "content"
-               });
-       });
-
-       app.post( new RegExp('/_rtform/(?:(' + getInterwikiRE() + ')/(.*))?'), 
interParams, parserEnvMw, function ( req, res ) {
-               var env = res.local('env');
-               setHeader(res, env, 'Content-Type', 'text/html; charset=UTF-8');
-               // we don't care about \r, and normalize everything to \n
-               parse( env, req, res, roundTripDiff.bind( null, false ), null, {
-                       revision: { '*': req.body.content.replace(/\r/g, '') }
-               });
-       });
-
-       function html2wt( req, res, html ) {
-               var env = res.local('env');
-               env.page.id = req.body.oldid || null;
-
-               if ( env.conf.parsoid.allowCORS ) {
-                       // allow cross-domain requests (CORS) so that parsoid 
service
-                       // can be used by third-party sites
-                       setHeader(res, env, 'Access-Control-Allow-Origin',
-                                                  env.conf.parsoid.allowCORS );
-               }
-
-               var html2wtCb = function () {
-                       var doc;
-                       try {
-                               doc = DU.parseHTML( html.replace( /\r/g, '' ) );
-                       } catch ( e ) {
-                               env.log("fatal", e, "There was an error in the 
HTML5 parser!");
-                               return;
-                       }
-
-                       try {
-                               var out = [];
-                               new Serializer( { env: env, oldid: env.page.id 
} ).serializeDOM(
-                                       doc.body,
-                                       function ( chunk ) {
-                                               out.push( chunk );
-                                       }, function () {
-                                               setHeader(res, env, 
'Content-Type', 'text/x-mediawiki; charset=UTF-8' );
-                                               setHeader(res, env, 
'X-Parsoid-Performance', env.getPerformanceHeader() );
-                                               endResponse(res, env,  
out.join( '' ) );
-                                       } );
-                       } catch ( e ) {
-                               env.log("fatal", e);
-                               return;
-                       }
-               };
-
-               if ( env.conf.parsoid.fetchWT ) {
-                       var target = env.resolveTitle( env.normalizeTitle( 
env.page.name ), '' );
-                       var tpr = new TemplateRequest( env, target, env.page.id 
);
-                       tpr.once( 'src', function ( err, src_and_metadata ) {
-                               if ( err ) {
-                                       env.log("error", "There was an error 
fetching the original wikitext for", target, err);
-                               } else {
-                                       env.setPageSrcInfo( src_and_metadata );
-                               }
-                               html2wtCb();
-                       } );
-               } else {
-                       html2wtCb();
-               }
-       }
-
-       function wt2html( req, res, wt, v2 ) {
-               var env = res.local('env'),
-                       prefix = res.local('iwp'),
-                       oldid = res.local('oldid'),
-                       target = env.resolveTitle( env.normalizeTitle( 
env.page.name ), '' );
-
-               // Set the timeout to 600 seconds..
-               req.connection.setTimeout( 600 * 1000 );
-
-               if ( env.conf.parsoid.allowCORS ) {
-                       // allow cross-domain requests (CORS) so that parsoid 
service
-                       // can be used by third-party sites
-                       setHeader( res, env, 'Access-Control-Allow-Origin', 
env.conf.parsoid.allowCORS );
-               }
-
-               var tmpCb;
-               if ( wt ) {
-                       wt = wt.replace( /\r/g, '' );
-                       env.log('info', 'starting parsing');
-
-                       // clear default page name
-                       if ( !res.local('pageName') ) {
-                               env.page.name = '';
-                       }
-
-                       var parser = 
env.pipelineFactory.getPipeline('text/x-mediawiki/full');
-                       parser.once( 'document', function ( document ) {
-                               // Don't cache requests when wt is set in case 
somebody uses
-                               // GET for wikitext parsing
-                               setHeader(res, env, 'Cache-Control', 
'private,no-cache,s-maxage=0' );
-                               sendRes( req.body.body ? document.body : 
document );
-                       });
-
-                       tmpCb = function ( err, src_and_metadata ) {
-                               if ( err ) {
-                                       env.log("fatal/request", err);
-                                       return;
-                               }
-
-                               // Set the source
-                               env.setPageSrcInfo( src_and_metadata );
-
-                               try {
-                                       parser.processToplevelDoc( wt );
-                               } catch ( e ) {
-                                       env.log("fatal", e);
-                                       return;
-                               }
-                       };
-
-                       if ( !res.local('pageName') || !oldid ) {
-                               // no pageName supplied; don't fetch the page 
source
-                               tmpCb( null, wt );
-                               return;
-                       }
-
-               } else {
-                       if ( oldid ) {
-                               env.log('info', 'starting parsing');
-
-                               if ( !req.headers.cookie ) {
-                                       setHeader(res, env, 'Cache-Control', 
's-maxage=2592000' );
-                               } else {
-                                       // Don't cache requests with a session
-                                       setHeader(res, env, 'Cache-Control', 
'private,no-cache,s-maxage=0' );
-                               }
-
-                               // Indicate the MediaWiki revision in a header 
as well for
-                               // ease of extraction in clients.
-                               setHeader(res, env, 'content-revision-id', 
oldid);
-
-                               tmpCb = parse.bind( null, env, req, res, 
function ( req, res, src, doc ) {
-                                       sendRes( doc.documentElement );
-                               });
-                       } else {
-                               // Don't cache requests with no oldid
-                               setHeader(res, env, 'Cache-Control', 
'private,no-cache,s-maxage=0' );
-                               tmpCb = function ( err, src_and_metadata ) {
-                                       if ( err ) {
-                                               env.log("fatal/request", err);
-                                               return;
-                                       }
-
-                                       // Set the source
-                                       env.setPageSrcInfo( src_and_metadata );
-                                       oldid = env.page.meta.revision.revid;
-
-                                       var path = "/";
-                                       if ( v2 ) {
-                                               path += [
-                                                       "v2",
-                                                       url.parse( 
env.conf.parsoid.interwikiMap.get( prefix ) ).host,
-                                                       encodeURIComponent( 
target ),
-                                                       v2.format,
-                                                       oldid
-                                               ].join("/");
-                                       } else {
-                                               path += [
-                                                       prefix,
-                                                       encodeURIComponent( 
target ) + "?oldid=" + oldid
-                                               ].join("/");
-                                       }
-
-                                       // Redirect to oldid
-                                       relativeRedirect({ "path": path, "res": 
res, "env": env });
-                                       env.log("info", "redirected to 
revision", env.page.meta.revision.revid);
-                               };
-                       }
-               }
-
-               var tpr = new TemplateRequest( env, target, oldid );
-               tpr.once( 'src', tmpCb );
-
-               function sendRes( doc ) {
-                       try {
-                               setHeader(res, env, 'X-Parsoid-Performance', 
env.getPerformanceHeader());
-                               if ( v2 && v2.format === "pagebundle" ) {
-                                       var dp = 
doc.ownerDocument.getElementById('mw-data-parsoid');
-                                       dp.parentNode.removeChild(dp);
-                                       jsonResponse(res, env, {
-                                               html: DU.serializeNode( doc ),
-                                               "data-parsoid": 
JSON.parse(dp.text)
-                                       });
-                               } else {
-                                       setHeader(res, env, 'Content-Type', 
'text/html; charset=UTF-8');
-                                       endResponse(res, env,  
DU.serializeNode( doc ));
-                               }
-                               env.log("info", "completed parsing in", 
env.performance.duration, "ms");
-                       } catch (e) {
-                               env.log("fatal/request", e);
-                       }
-               }
-       }
-
-       // Attempt to define a new version of the API
-       // /v2/{domain}/{title}/{format}/{revision}
-
-       var supportedFormats = new Set([ "pagebundle", "html" ]);
-
-       function v2Middle( req, res, next ) {
-               function errOut(err) {
-                       // FIXME: provide more consistent error handling.
-                       sendResponse( res, {}, err, 404 );
-               }
-
-               var iwp = parsoidConfig.reverseIWMap.get( req.params.domain );
-               if ( !iwp ) {
-                       return errOut("Invalid domain.");
-               }
-               res.local('iwp', iwp);
-
-               res.local('format', req.params.format || "html");
-               if ( !supportedFormats.has( res.local('format') ) ) {
-                       return errOut("Invalid format.");
-               }
-
-               res.local('pageName', req.params.title);
-               res.local('oldid', req.params.revision || null);
-               next();
-       }
-
-       app.get('/v2/:domain/:title/:format?/:revision?', v2Middle, 
parserEnvMw, function(req, res) {
-               var v2 = { format: res.local("format") };
-               if ( v2.format === "pagebundle" ) {
-                       res.local('env').conf.parsoid.storeDataParsoid = true;
-               }
-               wt2html( req, res, null, v2 );
-       });
-
-       // Regular article parsing
-       app.get(new RegExp('/(' + getInterwikiRE() + ')/(.*)'), interParams, 
parserEnvMw, function(req, res) {
-               wt2html( req, res );
-       });
-
-       // Regular article serialization using POST
-       app.post(new RegExp('/(' + getInterwikiRE() + ')/(.*)'), interParams, 
parserEnvMw, function(req, res) {
-               // parse html or wt
-               if ( req.body.wt ) {
-                       wt2html( req, res, req.body.wt );
-               } else {
-                       html2wt( req, res, req.body.html || req.body.content || 
'' );
-               }
-       });
-
-       var version = {
-               name: pkg.name,
-               version: pkg.version
-       };
-
-       function gitVersion( cb ) {
-               fs.exists( path.join( __dirname, '/../.git' ), function ( 
exists ) {
-                       if ( !exists ) {
-                               cb();
-                               return;
-                       }
-                       childProc.exec(
-                               'git rev-parse HEAD',
-                               function ( error, stdout, stderr ) {
-                                       if ( !error ) {
-                                               version.sha = stdout.slice(0, 
-1);
-                                       }
-                                       cb();
-                       });
-               });
-       }
-
-       /**
-       * Return Parsoid version based on package.json + git sha1 if available
-       */
-       app.get( "/_version", function ( req, res ) {
-               res.json( version );
-       });
-
-       // Get host and port from the environment, if available
-       // VCAP_APP_PORT is for appfog.com support
-       var port = parsoidConfig.serverPort ||
-               process.env.VCAP_APP_PORT || process.env.PORT || 8000;
-       // default bind all
-       var host = parsoidConfig.serverInterface || process.env.INTERFACE;
-
-       app.on( 'error', function( err ) {
+       // Catch errors
+       app.on('error', function( err ) {
                if ( err.errno === "EADDRINUSE" ) {
                        console.error( "Port %d is already in use. Exiting.", 
port );
                        cluster.worker.disconnect();
@@ -775,12 +67,49 @@
                }
        });
 
-       gitVersion( function () {
-               app.listen( port, host );
-               console.log( ' - ' + instanceName + ' ready on ' +
-                       (host||'') + ':' + port );
-       });
 
+       // Routes
+
+       var i = routes.interParams,
+               p = routes.parserEnvMw,
+               v = routes.v2Middle;
+
+       function re(str) { return new RegExp(str); }
+
+       // Regexp that matches to all interwikis accepted by the API.
+       var iwRe = parsoidConfig.interwikiRegexp;
+
+       app.get( '/', routes.home );
+       app.get( "/_version", routes.version );
+       app.get( /^\/robots.txt$/, routes.robots );
+
+       app.get(  re( '^/((?:_rt|_rtve)/)?(' + iwRe + '):(.*)$' ), 
routes.redirectOldStyle );
+       app.get(  re('/_html/(?:(' + iwRe + ')/(.*))?'), i, p, 
routes.html2wtForm );
+       app.get(  re('/_wikitext/(?:(' + iwRe + ')/(.*))?'), i, p, 
routes.wt2htmlForm );
+       app.get(  re('/_rt/(?:(' + iwRe + ')/(.*))?'), i, p, 
routes.roundtripTesting );
+       app.get(  re('/_rtve/(' + iwRe + ')/(.*)'), i, p, 
routes.roundtripTestingNL );
+       app.get(  re('/_rtselser/(' + iwRe + ')/(.*)'), i, p, 
routes.roundtripSelser );
+       app.get(  re('/_rtform/(?:(' + iwRe + ')/(.*))?'), i, p, 
routes.get_rtForm );
+       app.post( re('/_rtform/(?:(' + iwRe + ')/(.*))?'), i, p, 
routes.post_rtForm );
+       app.get(  re('/(' + iwRe + ')/(.*)'), i, p, routes.wt2html );
+       app.post( re('/(' + iwRe + ')/(.*)'), i, p, routes.html2wt );
+
+       // Attempt to define a new version of the API
+       // /v2/{domain}/{title}/{format}/{revision}
+
+       app.get('/v2/:domain/:title/:format?/:revision?', v, p, 
routes.v2_wt2html );
+
+
+       // Get host and port from the environment, if available
+       // VCAP_APP_PORT is for appfog.com support
+       var port = parsoidConfig.serverPort ||
+               process.env.VCAP_APP_PORT || process.env.PORT || 8000;
+       // default bind all
+       var host = parsoidConfig.serverInterface || process.env.INTERFACE;
+
+       app.listen( port, host );
+
+       console.log( " - %s ready on %s:%s", instanceName, host || "", port );
 }
 
 module.exports = {
diff --git a/api/routes.js b/api/routes.js
new file mode 100644
index 0000000..f2c9ddb
--- /dev/null
+++ b/api/routes.js
@@ -0,0 +1,614 @@
+"use strict";
+
+require('es6-shim');
+
+var path = require('path'),
+       fs = require('fs'),
+       url = require('url'),
+       util = require('util'),
+       childProc = require('child_process'),
+       domino = require('domino'),
+       pkg = require('../package.json'),
+       apiUtils = require('./utils');
+
+
+// relative includes
+var mp = '../lib/';
+
+var MWParserEnvironment = require( mp + 'mediawiki.parser.environment.js' 
).MWParserEnvironment,
+       WikitextSerializer = require( mp + 'mediawiki.WikitextSerializer.js' 
).WikitextSerializer,
+       SelectiveSerializer = require( mp + 'mediawiki.SelectiveSerializer.js' 
).SelectiveSerializer,
+       LogData = require( mp + 'LogData.js' ).LogData,
+       Util = require( mp + 'mediawiki.Util.js' ).Util,
+       DU = require( mp + 'mediawiki.DOMUtils.js' ).DOMUtils,
+       ApiRequest = require( mp + 'mediawiki.ApiRequest.js' ),
+       Diff = require( mp + 'mediawiki.Diff.js' ).Diff;
+
+var ParsoidCacheRequest = ApiRequest.ParsoidCacheRequest,
+       TemplateRequest = ApiRequest.TemplateRequest;
+
+module.exports = function( parsoidConfig ) {
+
+var routes = {};
+
+
+// Helpers
+
+var Serializer = parsoidConfig.useSelser ? SelectiveSerializer : 
WikitextSerializer;
+
+var supportedFormats = new Set([ "pagebundle", "html" ]);
+
+function action( res ) {
+       return [ "", res.local('iwp'), res.local('pageName') ].join( "/" );
+}
+
+var roundTripDiff = function( selser, req, res, env, document ) {
+       var out = [];
+
+       var finalCB =  function () {
+               var i;
+               out = out.join('');
+
+               // Strip selser trigger comment
+               out = out.replace(/<!--rtSelserEditTestComment-->\n*$/, '');
+
+               // Emit base href so all relative urls resolve properly
+               var hNodes = document.body.firstChild.childNodes;
+               var headNodes = "";
+               for (i = 0; i < hNodes.length; i++) {
+                       if (hNodes[i].nodeName.toLowerCase() === 'base') {
+                               headNodes += DU.serializeNode(hNodes[i]);
+                               break;
+                       }
+               }
+
+               var bNodes = document.body.childNodes;
+               var bodyNodes = "";
+               for (i = 0; i < bNodes.length; i++) {
+                       bodyNodes += DU.serializeNode(bNodes[i]);
+               }
+
+               var htmlSpeChars = apiUtils.htmlSpecialChars(out);
+
+               var src = env.page.src.replace(/\n(?=\n)/g, '\n ');
+               out = out.replace(/\n(?=\n)/g, '\n ');
+
+               var patch = Diff.convertChangesToXML( Diff.diffLines(src, out) 
);
+
+               apiUtils.setHeader(res, env, 'X-Parsoid-Performance', 
env.getPerformanceHeader());
+
+               apiUtils.renderResponse(res, env, "roundtrip", {
+                       headers: headNodes,
+                       bodyNodes: bodyNodes,
+                       htmlSpeChars: htmlSpeChars,
+                       patch: patch,
+                       reqUrl: req.url
+               });
+
+               env.log("info", "completed parsing in", 
env.performance.duration, "ms");
+       };
+
+       // Re-parse the HTML to uncover foster-parenting issues
+       document = domino.createDocument(document.outerHTML);
+
+       var Serializer = selser ? SelectiveSerializer : WikitextSerializer;
+       new Serializer({ env: env }).serializeDOM(
+               document.body,
+               function( chunk ) { out.push(chunk); },
+               finalCB
+       );
+};
+
+function handleCacheRequest( env, req, res, cb, src, cacheErr, cacheSrc ) {
+       var errorHandlingCB = function ( src, err, doc ) {
+               if ( err ) {
+                       env.log("fatal/request", err);
+                       return;
+               }
+               cb( req, res, src, doc );
+       };
+
+       if ( cacheErr ) {
+               // No luck with the cache request, just proceed as normal.
+               Util.parse(env, errorHandlingCB, null, src);
+               return;
+       }
+       // Extract transclusion and extension content from the DOM
+       var expansions = DU.extractExpansions(DU.parseHTML(cacheSrc));
+
+       // Figure out what we can reuse
+       var parsoidHeader = JSON.parse(req.headers['x-parsoid'] || '{}');
+       if (parsoidHeader.cacheID) {
+               if (parsoidHeader.mode === 'templates') {
+                       // Transclusions need to be updated, so don't reuse 
them.
+                       expansions.transclusions = {};
+               } else if (parsoidHeader.mode === 'files') {
+                       // Files need to be updated, so don't reuse them.
+                       expansions.files = {};
+               }
+       }
+
+       // pass those expansions into Util.parse to prime the caches.
+       //console.log('expansions:', expansions);
+       Util.parse(env, errorHandlingCB, null, src, expansions);
+}
+
+var parse = function ( env, req, res, cb, err, src_and_metadata ) {
+       if ( err ) {
+               env.log("fatal/request", err);
+               return;
+       }
+
+       // Set the source
+       env.setPageSrcInfo( src_and_metadata );
+
+       // Now env.page.meta.title has the canonical title, and
+       // env.page.meta.revision.parentid has the predecessor oldid
+
+       // See if we can reuse transclusion or extension expansions.
+       if (env.conf.parsoid.parsoidCacheURI &&
+                       // And don't parse twice for recursive parsoid requests
+                       ! req.headers['x-parsoid-request'])
+       {
+               // Try to retrieve a cached copy of the content so that we can 
recycle
+               // template and / or extension expansions.
+               var parsoidHeader = JSON.parse(req.headers['x-parsoid'] || 
'{}'),
+                       // If we get a prevID passed in in X-Parsoid (from our 
PHP
+                       // extension), use that explicitly. Otherwise default 
to the
+                       // parentID.
+                       cacheID = parsoidHeader.cacheID ||
+                               env.page.meta.revision.parentid,
+                       cacheRequest = new ParsoidCacheRequest(env,
+                               env.page.meta.title, cacheID);
+               cacheRequest.once('src',
+                               handleCacheRequest.bind(null, env, req, res, 
cb, env.page.src));
+       } else {
+               handleCacheRequest(env, req, res, cb, env.page.src, "Recursive 
request", null);
+       }
+};
+
+function html2wt( req, res, html ) {
+       var env = res.local('env');
+       env.page.id = req.body.oldid || null;
+
+       if ( env.conf.parsoid.allowCORS ) {
+               // allow cross-domain requests (CORS) so that parsoid service
+               // can be used by third-party sites
+               apiUtils.setHeader(res, env, 'Access-Control-Allow-Origin',
+                                          env.conf.parsoid.allowCORS );
+       }
+
+       var html2wtCb = function () {
+               var doc;
+               try {
+                       doc = DU.parseHTML( html.replace( /\r/g, '' ) );
+               } catch ( e ) {
+                       env.log("fatal", e, "There was an error in the HTML5 
parser!");
+                       return;
+               }
+
+               try {
+                       var out = [];
+                       new Serializer( { env: env, oldid: env.page.id } 
).serializeDOM(
+                               doc.body,
+                               function ( chunk ) {
+                                       out.push( chunk );
+                               }, function () {
+                                       apiUtils.setHeader(res, env, 
'Content-Type', 'text/x-mediawiki; charset=UTF-8' );
+                                       apiUtils.setHeader(res, env, 
'X-Parsoid-Performance', env.getPerformanceHeader() );
+                                       apiUtils.endResponse(res, env,  
out.join( '' ) );
+                               } );
+               } catch ( e ) {
+                       env.log("fatal", e);
+                       return;
+               }
+       };
+
+       if ( env.conf.parsoid.fetchWT ) {
+               var target = env.resolveTitle( env.normalizeTitle( 
env.page.name ), '' );
+               var tpr = new TemplateRequest( env, target, env.page.id );
+               tpr.once( 'src', function ( err, src_and_metadata ) {
+                       if ( err ) {
+                               env.log("error", "There was an error fetching 
the original wikitext for", target, err);
+                       } else {
+                               env.setPageSrcInfo( src_and_metadata );
+                       }
+                       html2wtCb();
+               } );
+       } else {
+               html2wtCb();
+       }
+}
+
+function wt2html( req, res, wt, v2 ) {
+       var env = res.local('env'),
+               prefix = res.local('iwp'),
+               oldid = res.local('oldid'),
+               target = env.resolveTitle( env.normalizeTitle( env.page.name ), 
'' );
+
+       // Set the timeout to 600 seconds..
+       req.connection.setTimeout( 600 * 1000 );
+
+       if ( env.conf.parsoid.allowCORS ) {
+               // allow cross-domain requests (CORS) so that parsoid service
+               // can be used by third-party sites
+               apiUtils.setHeader( res, env, 'Access-Control-Allow-Origin', 
env.conf.parsoid.allowCORS );
+       }
+
+       var tmpCb;
+       if ( wt ) {
+               wt = wt.replace( /\r/g, '' );
+               env.log('info', 'starting parsing');
+
+               // clear default page name
+               if ( !res.local('pageName') ) {
+                       env.page.name = '';
+               }
+
+               var parser = 
env.pipelineFactory.getPipeline('text/x-mediawiki/full');
+               parser.once( 'document', function ( document ) {
+                       // Don't cache requests when wt is set in case somebody 
uses
+                       // GET for wikitext parsing
+                       apiUtils.setHeader(res, env, 'Cache-Control', 
'private,no-cache,s-maxage=0' );
+                       sendRes( req.body.body ? document.body : document );
+               });
+
+               tmpCb = function ( err, src_and_metadata ) {
+                       if ( err ) {
+                               env.log("fatal/request", err);
+                               return;
+                       }
+
+                       // Set the source
+                       env.setPageSrcInfo( src_and_metadata );
+
+                       try {
+                               parser.processToplevelDoc( wt );
+                       } catch ( e ) {
+                               env.log("fatal", e);
+                               return;
+                       }
+               };
+
+               if ( !res.local('pageName') || !oldid ) {
+                       // no pageName supplied; don't fetch the page source
+                       tmpCb( null, wt );
+                       return;
+               }
+
+       } else {
+               if ( oldid ) {
+                       env.log('info', 'starting parsing');
+
+                       if ( !req.headers.cookie ) {
+                               apiUtils.setHeader(res, env, 'Cache-Control', 
's-maxage=2592000' );
+                       } else {
+                               // Don't cache requests with a session
+                               apiUtils.setHeader(res, env, 'Cache-Control', 
'private,no-cache,s-maxage=0' );
+                       }
+
+                       // Indicate the MediaWiki revision in a header as well 
for
+                       // ease of extraction in clients.
+                       apiUtils.setHeader(res, env, 'content-revision-id', 
oldid);
+
+                       tmpCb = parse.bind( null, env, req, res, function ( 
req, res, src, doc ) {
+                               sendRes( doc.documentElement );
+                       });
+               } else {
+                       // Don't cache requests with no oldid
+                       apiUtils.setHeader(res, env, 'Cache-Control', 
'private,no-cache,s-maxage=0' );
+                       tmpCb = function ( err, src_and_metadata ) {
+                               if ( err ) {
+                                       env.log("fatal/request", err);
+                                       return;
+                               }
+
+                               // Set the source
+                               env.setPageSrcInfo( src_and_metadata );
+                               oldid = env.page.meta.revision.revid;
+
+                               var path = "/";
+                               if ( v2 ) {
+                                       path += [
+                                               "v2",
+                                               url.parse( 
env.conf.parsoid.interwikiMap.get( prefix ) ).host,
+                                               encodeURIComponent( target ),
+                                               v2.format,
+                                               oldid
+                                       ].join("/");
+                               } else {
+                                       path += [
+                                               prefix,
+                                               encodeURIComponent( target ) + 
"?oldid=" + oldid
+                                       ].join("/");
+                               }
+
+                               // Redirect to oldid
+                               apiUtils.relativeRedirect({
+                                       "path": path,
+                                       "res": res,
+                                       "env": env
+                               });
+                               env.log("info", "redirected to revision", 
env.page.meta.revision.revid);
+                       };
+               }
+       }
+
+       var tpr = new TemplateRequest( env, target, oldid );
+       tpr.once( 'src', tmpCb );
+
+       function sendRes( doc ) {
+               try {
+                       apiUtils.setHeader(res, env, 'X-Parsoid-Performance', 
env.getPerformanceHeader());
+                       if ( v2 && v2.format === "pagebundle" ) {
+                               var dp = 
doc.ownerDocument.getElementById('mw-data-parsoid');
+                               dp.parentNode.removeChild(dp);
+                               apiUtils.jsonResponse(res, env, {
+                                       html: DU.serializeNode( doc ),
+                                       "data-parsoid": JSON.parse(dp.text)
+                               });
+                       } else {
+                               apiUtils.setHeader(res, env, 'Content-Type', 
'text/html; charset=UTF-8');
+                               apiUtils.endResponse(res, env,  
DU.serializeNode( doc ));
+                       }
+                       env.log("info", "completed parsing in", 
env.performance.duration, "ms");
+               } catch (e) {
+                       env.log("fatal/request", e);
+               }
+       }
+}
+
+// Middlewares
+
+routes.interParams = function( req, res, next ) {
+       res.local('iwp', req.params[0] || parsoidConfig.defaultWiki || '');
+       res.local('pageName', req.params[1] || '');
+       res.local('oldid', req.query.oldid || null);
+       next();
+};
+
+routes.parserEnvMw = function( req, res, next ) {
+       MWParserEnvironment.getParserEnv( parsoidConfig, null, 
res.local('iwp'), res.local('pageName'), req.headers.cookie, function ( err, 
env ) {
+               function errCB( res, env, logData, callback ) {
+                       try {
+                               if ( !env.responseSent ) {
+                                       apiUtils.setHeader(res, env, 
'Content-Type', 'text/plain; charset=UTF-8' );
+                                       apiUtils.sendResponse(res, env, 
logData.fullMsg(), logData.code || 500);
+                                       if ( typeof callback === 'function' ) {
+                                               res.on('finish', callback);
+                                       }
+                                       return;
+                               }
+                       } catch (e) {
+                               console.log( e.stack || e );
+                               res.end();
+                       }
+                       if ( typeof callback === 'function' ) {
+                               callback();
+                       }
+               }
+
+               if ( err ) {
+                       return errCB(res, {}, new LogData(null, "error", err));
+               }
+
+               env.logger.registerBackend(/fatal(\/.*)?/, errCB.bind(this, 
res, env));
+               res.local("env", env);
+               next();
+       });
+};
+
+routes.v2Middle = function( req, res, next ) {
+       function errOut(err) {
+               // FIXME: provide more consistent error handling.
+               apiUtils.sendResponse( res, {}, err, 404 );
+       }
+
+       var iwp = parsoidConfig.reverseIWMap.get( req.params.domain );
+       if ( !iwp ) {
+               return errOut("Invalid domain.");
+       }
+       res.local('iwp', iwp);
+
+       res.local('format', req.params.format || "html");
+       if ( !supportedFormats.has( res.local('format') ) ) {
+               return errOut("Invalid format.");
+       }
+
+       res.local('pageName', req.params.title);
+       res.local('oldid', req.params.revision || null);
+       next();
+};
+
+// Routes
+
+routes.home = function( req, res ){
+       res.render('home');
+};
+
+// robots.txt: no indexing.
+routes.robots = function ( req, res ) {
+       res.end("User-agent: *\nDisallow: /\n");
+};
+
+function gitVersion( cb ) {
+       fs.exists( path.join( __dirname, '/../.git' ), function ( exists ) {
+               if ( !exists ) {
+                       cb();
+                       return;
+               }
+               childProc.exec(
+                       'git rev-parse HEAD',
+                       function ( error, stdout, stderr ) {
+                               if ( !error ) {
+                                       versionCache.sha = stdout.slice(0, -1);
+                               }
+                               cb();
+               });
+       });
+}
+
+// Return Parsoid version based on package.json + git sha1 if available
+var versionCache;
+routes.version = function( req, res ) {
+       if ( versionCache ) {
+               return res.json( versionCache );
+       }
+       versionCache = {
+               name: pkg.name,
+               version: pkg.version
+       };
+       gitVersion(function() {
+               res.json( versionCache );
+       });
+};
+
+// Redirects for old-style URL compatibility
+routes.redirectOldStyle = function( req, res ) {
+       if ( req.params[0] ) {
+               apiUtils.relativeRedirect({
+                       "path" : '/' + req.params[0] + req.params[1] + '/' + 
req.params[2],
+                       "res" : res,
+                       "code" : 301
+               });
+       } else {
+               apiUtils.relativeRedirect({
+                       "path" : '/' + req.params[1] + '/' + req.params[2],
+                       "res" : res,
+                       "code": 301
+               });
+       }
+       res.end();
+};
+
+// Form-based HTML DOM -> wikitext interface for manual testing
+routes.html2wtForm = function( req, res ) {
+       var env = res.local('env');
+       apiUtils.renderResponse(res, env, "form", {
+               title: "Your HTML DOM:",
+               action: action(res),
+               name: "html"
+       });
+};
+
+// Form-based wikitext -> HTML DOM interface for manual testing
+routes.wt2htmlForm = function( req, res ) {
+       var env = res.local('env');
+       apiUtils.renderResponse(res, env, "form", {
+               title: "Your wikitext:",
+               action: action(res),
+               name: "wt"
+       });
+};
+
+// Round-trip article testing
+routes.roundtripTesting = function(req, res) {
+       var env = res.local('env');
+       var target = env.resolveTitle( env.normalizeTitle( env.page.name ), '' 
);
+
+       req.connection.setTimeout(300 * 1000);
+       env.log('info', 'starting parsing');
+
+       var oldid = null;
+       if ( req.query.oldid ) {
+               oldid = req.query.oldid;
+       }
+       var tpr = new TemplateRequest( env, target, oldid );
+       tpr.once('src', parse.bind( tpr, env, req, res, roundTripDiff.bind( 
null, false ) ));
+};
+
+// Round-trip article testing with newline stripping for editor-created HTML
+// simulation
+routes.roundtripTestingNL = function(req, res) {
+       var env = res.local('env');
+       var target = env.resolveTitle( env.normalizeTitle( env.page.name ), '' 
);
+
+       env.log('info', 'starting parsing');
+       var oldid = null;
+       if ( req.query.oldid ) {
+               oldid = req.query.oldid;
+       }
+       var tpr = new TemplateRequest( env, target, oldid ),
+               cb = function ( req, res, src, document ) {
+                       // strip newlines from the html
+                       var html = document.innerHTML.replace(/[\r\n]/g, ''),
+                               newDocument = DU.parseHTML(html);
+                       roundTripDiff( false, req, res, src, newDocument );
+               };
+
+       tpr.once('src', parse.bind( tpr, env, req, res, cb ));
+};
+
+// Round-trip article testing with selser over re-parsed HTML.
+routes.roundtripSelser = function(req, res) {
+       var env = res.local('env');
+       var target = env.resolveTitle( env.normalizeTitle( env.page.name ), '' 
);
+
+       env.log('info', 'starting parsing');
+       var oldid = null;
+       if ( req.query.oldid ) {
+               oldid = req.query.oldid;
+       }
+       var tpr = new TemplateRequest( env, target, oldid ),
+               tprCb = function ( req, res, src, document ) {
+                       var newDocument = DU.parseHTML( 
DU.serializeNode(document) ),
+                               newNode = 
newDocument.createComment('rtSelserEditTestComment');
+                       newDocument.body.appendChild(newNode);
+                       roundTripDiff( true, req, res, src, newDocument );
+               };
+
+       tpr.once( 'src', parse.bind( tpr, env, req, res, tprCb ) );
+};
+
+// Form-based round-tripping for manual testing
+routes.get_rtForm = function( req, res ) {
+       var env = res.local('env');
+       apiUtils.renderResponse(res, env, "form", {
+               title: "Your wikitext:",
+               action: "/_rtform/" + res.local('pageName'),
+               name: "content"
+       });
+};
+
+// Form-based round-tripping for manual testing
+routes.post_rtForm = function( req, res ) {
+       var env = res.local('env');
+       apiUtils.setHeader(res, env, 'Content-Type', 'text/html; 
charset=UTF-8');
+       // we don't care about \r, and normalize everything to \n
+       parse( env, req, res, roundTripDiff.bind( null, false ), null, {
+               revision: { '*': req.body.content.replace(/\r/g, '') }
+       });
+};
+
+// Regular article parsing
+routes.wt2html = function(req, res) {
+       wt2html( req, res );
+};
+
+// Regular article serialization using POST
+routes.html2wt = function(req, res) {
+       // parse html or wt
+       if ( req.body.wt ) {
+               wt2html( req, res, req.body.wt );
+       } else {
+               html2wt( req, res, req.body.html || req.body.content || '' );
+       }
+};
+
+
+// v2 Routes
+
+
+// Regular article parsing
+routes.v2_wt2html = function(req, res) {
+       var v2 = { format: res.local("format") };
+       if ( v2.format === "pagebundle" ) {
+               res.local('env').conf.parsoid.storeDataParsoid = true;
+       }
+       wt2html( req, res, null, v2 );
+};
+
+
+return routes;
+
+};
diff --git a/api/utils.js b/api/utils.js
new file mode 100644
index 0000000..d46a737
--- /dev/null
+++ b/api/utils.js
@@ -0,0 +1,105 @@
+"use strict";
+
+var apiUtils = module.exports = {};
+
+
+/**
+ * Send a redirect response with optional code and a relative URL
+ *
+ * (Returns if a response has already been sent.)
+ * This is not strictly HTTP spec conformant, but works in most clients. More
+ * importantly, it works both behind proxies and on the internal network.
+ */
+apiUtils.relativeRedirect = function(args) {
+       if (!args.code) {
+               args.code = 302; // moved temporarily
+       }
+
+       if (args.res && args.env && args.env.responseSent ) {
+               return;
+       } else {
+               args.res.writeHead(args.code, {
+                       'Location': args.path
+               });
+               args.res.end();
+       }
+};
+
+/**
+ * Set header, but only if response hasn't been sent.
+ *
+ * @method
+ * @param {MWParserEnvironment} env
+ * @param {Response} res The response object from our routing function.
+ * @property {Function} Serializer
+ */
+apiUtils.setHeader = function(res, env) {
+       if (env.responseSent) {
+               return;
+       } else {
+               res.setHeader.apply(res, Array.prototype.slice.call(arguments, 
2));
+       }
+};
+
+/**
+ * End response, but only if response hasn't been sent.
+ *
+ * @method
+ * @param {MWParserEnvironment} env
+ * @param {Response} res The response object from our routing function.
+ * @property {Function} Serializer
+ */
+apiUtils.endResponse = function(res, env) {
+       if (env.responseSent) {
+               return;
+       } else {
+               env.responseSent = true;
+               res.end.apply(res, Array.prototype.slice.call(arguments, 2));
+               env.log("end/response");
+       }
+};
+
+/**
+ * Send response, but only if response hasn't been sent.
+ *
+ * @method
+ * @param {MWParserEnvironment} env
+ * @param {Response} res The response object from our routing function.
+ * @property {Function} Serializer
+ */
+apiUtils.sendResponse = function(res, env) {
+       if (env.responseSent) {
+               return;
+       } else {
+               env.responseSent = true;
+               res.send.apply(res, Array.prototype.slice.call(arguments, 2));
+       }
+};
+
+/**
+ * Render response, but only if response hasn't been sent.
+ */
+apiUtils.renderResponse = function(res, env) {
+       if (env.responseSent) {
+               return;
+       } else {
+               env.responseSent = true;
+               res.render.apply(res, Array.prototype.slice.call(arguments, 2));
+       }
+};
+
+apiUtils.jsonResponse = function(res, env) {
+       if (env.responseSent) {
+               return;
+       } else {
+               env.responseSent = true;
+               res.json.apply(res, Array.prototype.slice.call(arguments, 2));
+       }
+};
+
+apiUtils.htmlSpecialChars = function( s ) {
+       return s.replace(/&/g,'&amp;')
+               .replace(/</g,'&lt;')
+               .replace(/"/g,'&quot;')
+               .replace(/'/g,'&#039;');
+};

-- 
To view, visit https://gerrit.wikimedia.org/r/164365
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I9a2a573e6afe14c911d42d4d5ecd048eff7f1865
Gerrit-PatchSet: 2
Gerrit-Project: mediawiki/services/parsoid
Gerrit-Branch: master
Gerrit-Owner: Arlolra <[email protected]>
Gerrit-Reviewer: Cscott <[email protected]>
Gerrit-Reviewer: Marcoil <[email protected]>
Gerrit-Reviewer: Subramanya Sastry <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to