Clump has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/218342

Change subject: Initial code dump Change-Id: 
Ia912882ee14ffc32e798543a7dacbcfd2804375e
......................................................................

Initial code dump
Change-Id: Ia912882ee14ffc32e798543a7dacbcfd2804375e
---
A DebugTemplates.alias.php
A DebugTemplates.php
A SpecialDebugTemplates.php
A ext.debugTemplates.css
A ext.debugTemplates.js
A i18n/en.json
A i18n/qqq.json
7 files changed, 2,504 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DebugTemplates 
refs/changes/42/218342/1

diff --git a/DebugTemplates.alias.php b/DebugTemplates.alias.php
new file mode 100644
index 0000000..5d7ec6d
--- /dev/null
+++ b/DebugTemplates.alias.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * Aliases for DebugTemplates
+ *
+ * @file
+ * @ingroup Extensions
+ */
+
+$specialPageAliases = array();
+
+/** English
+ * @author Clark Verbrugge
+ */
+$specialPageAliases['en'] = array(
+       'DebugTemplates' => array( 'DebugTemplates', 'Debug Templates' ),
+);
\ No newline at end of file
diff --git a/DebugTemplates.php b/DebugTemplates.php
new file mode 100644
index 0000000..07aa8ad
--- /dev/null
+++ b/DebugTemplates.php
@@ -0,0 +1,48 @@
+<?php
+# Alert the user that this is not a valid access point to MediaWiki if they 
try to access the special pages file directly.
+if ( !defined( 'MEDIAWIKI' ) ) {
+       echo <<<EOT
+To install my extension, put the following line in LocalSettings.php:
+require_once( "\$IP/extensions/DebugTemplates/DebugTemplates.php" );
+EOT;
+       exit( 1 );
+}
+
+$wgExtensionCredits['specialpage'][] = array(
+       'path' => __FILE__,
+       'name' => 'DebugTemplates',
+       'author' => 'Clark Verbrugge',
+    'license-name' => 'CC BY-SA 3.0',
+       'url' => '',
+       'descriptionmsg' => 'debugtemplates-desc',
+       'version' => '0.5',
+);
+
+$wgAutoloadClasses['SpecialDebugTemplates'] = __DIR__ . 
'/SpecialDebugTemplates.php';
+$wgMessagesDirs['DebugTemplates'] = __DIR__ . "/i18n";
+$wgExtensionMessagesFiles['DebugTemplatesAlias'] = __DIR__ . 
'/DebugTemplates.alias.php';
+$wgSpecialPages['DebugTemplates'] = 'SpecialDebugTemplates';
+
+$wgResourceModules['ext.debugTemplates'] = array(
+       'scripts' => array( 'ext.debugTemplates.js' ),
+       'styles' => 'ext.debugTemplates.css',
+
+       // error and warning messages used in the javascript
+       'messages' => array( 'debugtemplates-error-parse',
+               'debugtemplates-error-button',
+               'debugtemplates-error-eval',
+               'debugtemplates-error-arg-eval',
+               'debugtemplates-warning-template-not-a-template',
+               'debugtemplates-warning-template-not-found',
+               'debugtemplates-error-template-name',
+               'debugtemplates-error-template-revisions',
+               'debugtemplates-error-template-page',
+               'debugtemplates-args-constructed',
+               'debugtemplates-args-eval-all' ),
+
+       // no dependencies
+       'dependencies' => array(  ),
+
+       'localBasePath' => __DIR__,
+       'remoteExtPath' => 'DebugTemplates'
+);
diff --git a/SpecialDebugTemplates.php b/SpecialDebugTemplates.php
new file mode 100644
index 0000000..63a9d52
--- /dev/null
+++ b/SpecialDebugTemplates.php
@@ -0,0 +1,216 @@
+<?php
+if( !defined( 'MEDIAWIKI' ) ) {
+       die( "This is not a valid access point.\n" );
+}
+
+/*
+ * This is the code that creates the special page.
+ *
+ * @author Clark Verbrugge
+ * @Licence CC BY-SA 3.0
+ */
+class SpecialDebugTemplates extends SpecialPage {
+       
+       function __construct() {
+               parent::__construct( 'debugtemplates' );
+       }
+       
+       /**
+        * Construct and return the special page.
+        *
+        * @param string|null $subpage Name of the subpage if any
+        */
+       function execute( $subpage ) {
+               global $wgParser,$wgOut;
+               
+               $wgOut->addModules( 'ext.debugTemplates' );
+               
+               $this->setHeaders();
+               
+               if ($subpage!='') {
+                       $input = $this->getPage($subpage);
+                       $titleStr = $subpage;
+               } else {
+                       $input = '';
+                       $titleStr = '';
+               }
+               
+               $out = $this->getOutput();
+               $out->addWikiMsg( 'debugtemplates-intro' );
+               $out->addHTML( $this->makeForm( $titleStr, $input ) );
+       }
+       
+       /**
+        * Generate the overall page structure.  This is partly a form to enter 
the starting text etc, and
+        * partly some elements afterward that provide the interactive 
debugging environment.
+        *
+        * @param string $title Initial value of the context title field
+        * @param string $input Initial value of the input textarea
+        * @return string
+        */
+       private function makeForm( $title, $input ) {
+               global $wgScriptPath,$wgServer;
+               $self = $this->getPageTitle();
+               $request = $this->getRequest();
+               $user = $this->getUser();
+               
+               $form = "<div><fieldset><legend>" . $this->msg( 
'debugtemplates-form' )->escaped() . "</legend>\n";
+               
+               $form .= '<div style="display:inline-block;width:40%;">';
+               
+               // Create a field holding the url for api.php calls.  This is 
set to readonly, but still
+               // presented and even used so it could be changed if ever 
cross-site scripting is possible.
+               $form .= '<p>' . Xml::inputLabel(
+                       $this->msg( 'debugtemplates-api' )->plain(),
+                       'wpAPIPrefix',
+                       'dt-api',
+                       60,
+                       $wgServer . $wgScriptPath . '/api.php',
+                       array( 'autofocus' => '', 'class' => 
'mw-ui-input-inline', 'style' => 'width:100%;', 'readonly' => 'readonly')
+               ) . '</p>';
+               
+               // Entry of the context title of the page that will be debugged.
+               $form .= '<p>' . Xml::inputLabel(
+                       $this->msg( 'debugtemplates-title' )->plain(),
+                       'wpContextTitle',
+                       'dt-title',
+                       60,
+                       $title,
+                       array( 'autofocus' => '', 'class' => 
'mw-ui-input-inline', 'style' => 'width:100%;')
+               ) . '</p></div>';
+               
+               // The main input area for entering and editing the text being 
debugged.
+               $form .= '<div style="display:inline-block;width:70%;"><h2>'
+                       . $this->msg( 'debugtemplates-input' )->text() . 
'</h2>';
+               $form .= Xml::textarea(
+                       'dt-input',
+                       $input,
+                       1,
+                       15,
+                       array( 'id' => 'dt-input')
+               ) . '</div>';
+               
+               // Next to the editable input is an array of input parameters, 
along with some buttons for operating on them.
+               $form .= $this->makeArgTable();
+               
+               // Ok that's everything in the first main div of input data.
+               $form .= "</fieldset></div>";
+               
+               // Make an error-message output pane.
+               $form .= '<span class="dt-error" id="dt-error"></span>';
+               
+               // The interactive debug area starts with a few buttons that do 
and configure things.
+               $form .= "<h2>" . $this->msg( 'debugtemplates-output' 
)->escaped() . "</h2>\n";
+               $form .= $this->makeDebugButtons();
+               
+               // Next comes the stack trace.
+               $form .= $this->makeBreadCrumbs();
+               
+               // Finally, the actual interactive main debug pane.
+               $form .= $this->makeDebugPane();
+               
+               return $form;
+       }
+       
+       /**
+        * Generate the interactive debug pane itself.
+        *
+        * @return string
+        */
+       private function makeDebugPane( ) {
+               return '<div class="dt-debug-output-wrapper" 
style="width:100%;">'
+                       . '<div style="width:100%;" class="dt-debug-output" 
id="dt-output"></div></div>';
+       }
+       
+       
+       /**
+        * Generate the parameter table and controls.
+        *
+        * @return string
+        */
+       private function makeArgTable( ) {
+               return '<div 
style="padding-left:10px;display:inline-block;width:25%;vertical-align:top;">'
+                       . '<h2>'
+                       . $this->msg( 'debugtemplates-args-title' )->text()
+                       . '<input type="button" id="dt-args-set-toggle" 
style="margin-left:10px;" value="'
+                       . $this->msg( 'debugtemplates-args-set-toggle' )->text()
+                       . '"><input type="button" id="dt-args-value-clear" 
style="margin-left:10px;" value="'
+                       . $this->msg( 'debugtemplates-args-value-clear' 
)->text()
+                       . '"></h2><div style="width:100%;overflow:auto;" 
id="dt-argtable-wrapper">'
+                       . '<table id="dt-argtable" cellpadding="2" 
cellspacing="2" style="width:100%;">'
+                       . '<thead><tr><th id="dt-arg-set"><span>'
+                       . $this->msg( 'debugtemplates-args-set' )->text()
+                       . '</span></th><th><span>'
+                       . $this->msg( 'debugtemplates-args-name' )->text()
+                       . '</span></th style="width:100%;"><th><span>'
+                       . $this->msg( 'debugtemplates-args-value' )->text()
+                       . '</span></th style="width:100%;"><th><span>'
+                       . $this->msg( 'debugtemplates-args-eval' )->text()
+                       . '</span></th></tr></thead></table></div></div>';
+       }
+       
+       
+       /**
+        * Generate the various buttons that control the debugging area.
+        *
+        * @return string
+        */
+       private function makeDebugButtons( ) {
+               return '<input type="button" id="dt-eval" value="'
+                       . $this->msg( 'debugtemplates-eval' )->text()
+                       . '"><input type="button" id="dt-undo" 
style="margin-left:10px;" disabled="disabled" value="'
+                       . $this->msg( 'debugtemplates-undo' )->text()
+                       . '"><input type="button" id="dt-reset" 
style="margin-left:10px;" disabled="disabled" value="'
+                       . $this->msg( 'debugtemplates-reset' )->text()
+                       . '"><span 
style="vertical-align:middle;margin-left:2em;"><span class="dt-radio-label">'
+                       . $this->msg( 'debugtemplates-radio-intro' )->text()
+                       . '</span><input type="radio" id="dt-radio-select" 
name="dt-radio-debug" class="dt-radio-buttons">'
+                       . '<label class="dt-radio-label" for="dt-radio-select">'
+                       . $this->msg( 'debugtemplates-radio-select' )->text()
+                       . '</label><input type="radio" checked 
id="dt-radio-eval" name="dt-radio-debug" class="dt-radio-buttons">'
+                       . '<label class="dt-radio-label" for="dt-radio-eval">'
+                       . $this->msg( 'debugtemplates-radio-eval' )->text()
+                       . '</label><input type="radio" id="dt-radio-descend" 
name="dt-radio-debug" class="dt-radio-buttons">'
+                       . '<label class="dt-radio-label" 
for="dt-radio-descend">'
+                       . $this->msg( 'debugtemplates-radio-descend' )->text()
+                       . '</label></span><br>';
+       }
+       
+       
+       /**
+        * Generate breadcrumbs (stack) output area.
+        *
+        * @return string
+        */
+       private function makeBreadCrumbs() {
+               return '<div id="dt-crumbs" class="dt-crumbs" title="'
+                       . $this->msg( 'debugtemplates-crumb-title' )->text()
+                       . '"></div>';
+       }
+       
+       /**
+        * Get a page content.  Used to initialize the input if a subpage is 
provided.
+        *
+        * @param string $t The title of the page
+        * @return string The page content, or an empty string
+        */
+       function getPage($t) {
+               $title = Title::newFromText($t);
+               if(is_object($title)) {
+                       $r = Revision::newFromTitle($title);
+                       if(is_object($r))
+                               return $r->getText();
+               }
+               return "";
+       }
+       
+       /**
+        * Returns the special page group name.  It should be in the same place 
as the existing
+        * ExpandTemplates special page.
+        *
+        * @return string
+        */
+       protected function getGroupName() {
+               return 'wiki';
+       }
+}
diff --git a/ext.debugTemplates.css b/ext.debugTemplates.css
new file mode 100644
index 0000000..015d127
--- /dev/null
+++ b/ext.debugTemplates.css
@@ -0,0 +1,142 @@
+/* The debug bar for reporting error messages. */
+.dt-error {
+  font-size: 9pt;
+  color: #DD3333;
+}
+
+/* A little button to clear the message area. */
+.dt-error-button {
+  float: right;
+  transform: scale(0.8);
+}
+
+/* For the value of an unset parameter in the parameter list. */
+.dt-arg-set-no {
+  color: #888888;
+}
+
+/* For columns in the parameter table that should be centered. */
+.dt-arg-centered {
+  text-align: center;
+}
+
+/* The stack trace area, and properties of non-leaf entries. */
+.dt-crumbs {
+  font-family: monospace;
+  font-weight: bold;
+  background-color: #EEEEEE;
+  margin-top: 10px;
+  margin-bottom: 5px;
+}
+.dt-crumb-visited {
+  color: #2222DD;
+  cursor: pointer;
+}
+
+/* The radio buttons and labels above the debug pane. */
+.dt-radio-buttons {
+  margin-left: 0.5em;
+  vertical-align: middle;
+}
+.dt-radio-label {
+  vertical-align: bottom;
+}
+
+/* The interactive debug pane wrapper. */
+.dt-debug-output-wrapper {
+  background-color: #eeeeee;
+  border: 1px solid #888888;
+  white-space:pre-wrap;
+}
+
+/* The interactive debug pane wrapper itself. */
+.dt-debug-output {
+  padding:3px;
+}
+
+/* All of the nodes in the interactive debug pane have this style. */
+.dt-node { }
+
+/* Specific styles for specific kinds of nodes according to their role in the 
parse tree. */
+.dt-node-template { }
+.dt-node-arg { }
+.dt-node-title { }
+.dt-node-value { }
+.dt-node-name { }
+.dt-node-tplarg { }
+.dt-node-comment {
+  color: #777777
+}
+.dt-node-ignore {
+  color: #777777
+}
+.dt-node-multiline { }
+.dt-node-ext {
+  color: red;
+}
+.dt-node-ext-nowiki {
+  color: #A52A2A;
+}
+.dt-node-ext-pre {
+  font-family: Courier,monospace;
+  color: black;
+  background-color: #FFFACD;
+}
+
+/* Mouseover styles for the various components of the nodes. */
+.dt-emphasize, .dt-emphasize-template-in, .dt-emphasize-template-out {
+  font-weight: bold;
+  font-stretch: ultra-condensed;
+}
+.dt-emphasize-template-name {
+  color: #006400;
+}
+.dt-emphasize-template-args {
+  color: #4169E1;
+}
+.dt-emphasize-template-pipe {
+  font-weight: bold;
+  color: black;
+}
+.dt-emphasize-tplarg-in, .dt-emphasize-tplarg-out {
+  font-weight: bold;
+  font-stretch: ultra-condensed;
+}
+.dt-emphasize-tplarg-name {
+  color: #800080;
+}
+.dt-emphasize-tplarg-args {
+  color: #8B4513;
+}
+.dt-emphasize-tplarg-pipe {
+  font-weight: bold;
+  color:black;
+}
+
+/* Temporarily unshown nodes. */
+.dt-node-invisible {
+  display: none;
+}
+
+/* A node that has been evaluated to base wikitext, normal and on mouseover. */
+.dt-node-evaluated {
+  font-family: Courier,monospace;
+}
+.dt-node-emphasize-evaluated {
+  text-decoration: underline;
+}
+
+/* For the debug pane, to indicate it is currently evaluating something. */
+.dt-busy {
+  cursor: wait ! important;
+}
+/* For the debug pane, to indicate descend mode. */
+.dt-descend {
+  cursor: pointer;
+}
+
+/* Used during the brief highlighting of text being replaced due to an eval. */
+.dt-fading {
+  background-color: rgba(255,0,0,0.4);
+}
+
diff --git a/ext.debugTemplates.js b/ext.debugTemplates.js
new file mode 100644
index 0000000..8313ce0
--- /dev/null
+++ b/ext.debugTemplates.js
@@ -0,0 +1,1994 @@
+/**
+ * JS code for all the interactive parts of the special page
+ *
+ * @author Clark Verbrugge
+ * @license CC BY-SA 3.0
+**/
+
+
+/**
+ * *************************
+ * Global variables section.
+ *****************************
+**/
+
+// For indexing the displayed HTML nodes with unique numbers used in 
highlighting.
+var nindex = 0;
+// Array of parameter representations.  Each is an object with name and row 
fields.
+var params = [];
+// For indexexing AST nodes with unique numbers.
+var xindex = 0;
+// Mapping from nindex to xindex.
+var nTox = [];
+// Root of the AST.
+var ast;
+// Array of things done for undoing.  Each entry is a string or an array of 
strings.
+var lastUndo = [];
+// Stack of previous states (frames) from descending into templates.
+var nestingStack = [];
+// A flag used to help debounce.
+var busy = false;
+
+// Constant: maximum length of the lastUndo array.
+var maxUndos = 1000;
+// Constant: timeout interval in ms used in making sequences of API calls.
+var apiCallInterval = 30;
+// Constant: the checkmark symbol used for a set parameter.
+var argy = '\u2714';
+// Constant: the x symbol used for an unset parameter.
+var argn = '\u2718';
+// Constant: symbol used to represent the initial frame in the crumbs list.
+var firstcrumb = '\u2a00';
+// Constant: symbol used to represent the link from one crumb to another
+var nextCrumb = '\u27ff';
+
+// A reference to the undo button, to avoid having to look it up each time.
+var undoButtonNode;
+// A reference to the undo-all button, to avoid having to look it up each time.
+var resetButtonNode;
+
+/**
+ * ******************
+ * Utility functions.
+ * ******************
+**/
+
+/**
+ * Adds the given string to the error message area.
+ *
+ * @param {string} s
+ *
+**/
+function debugNote( s ) {
+    debugNoteHTML( document.createTextNode ( s ) );
+}
+
+/**
+ * Adds the given dom structure to the error message area.
+ *
+ * Also ensures length does not exceed a bound, and installs a clear button on 
the first message.
+ *
+ * @param {HTMLElement} s
+**/
+function debugNoteHTML( s ) {
+    var d = document.getElementById( 'dt-error' );
+    while ( d.childNodes.length > 10 ) {
+        d.removeChild( d.firstChild.nextSibling );
+    }
+    if ( !document.getElementById( 'dt-error-button' ) ) {
+        var b = document.createElement( 'input' );
+        b.type = 'button';
+        b.id = 'dt-error-button';
+        b.value = mw.message( 'debugtemplates-error-button' );
+        b.className = 'dt-error-button';
+        b.addEventListener( 'click', debugNoteClear, false );
+        d.appendChild( b );
+    }
+    d.appendChild( s );
+    d.appendChild( document.createElement( 'br' ) );
+}
+
+/**
+ * Clear the debug message area.
+**/
+function debugNoteClear() {
+    var d = document.getElementById( 'dt-error' );
+    while ( d.childNodes.length > 0 ) {
+        d.removeChild( d.firstChild );
+    }
+}
+
+/**
+ * Set the output pane to something, discarding all previous content.
+ *
+ * @param {HTMLElement|null} x
+**/
+function setOutput( x ) {
+    debugNoteClear();
+    var dout = document.getElementById( 'dt-output' );
+    while ( dout.hasChildNodes() ) {
+        dout.removeChild( dout.lastChild );
+    }
+    if ( x ) {
+        dout.appendChild( x );
+    }
+}
+
+/**
+ * Perform a POST operation.
+ *
+ * @param {string} url
+ * @param {string} params Assumed to be URI-encoded.
+ * @param {function} callback Callback upong completion. It will receive 1 or 
2 arguments; if everything
+ *  was ok then it receives "OK" and the result, and if not then it receives 
an error message.
+**/
+function doPost( url, params, callback ) {
+    var x = new XMLHttpRequest();
+    x.open( "POST", url, true );
+    x.setRequestHeader( "Content-type", "application/x-www-form-urlencoded" );
+    x.setRequestHeader( "Content-length", params.length );
+    x.setRequestHeader( "Connection", "close" );
+    x.setRequestHeader( "Api-User-Agent", "DebugTemplatesExtension/1.0" );
+
+    x.onreadystatechange = function() {
+        if ( x.readyState == 4 ) {
+            if ( x.status == 200 ) {
+                callback( "OK", x.responseText );
+            } else {
+                callback( "An error has occured making the request" );
+            }
+        }
+    };
+    //debugNote("sending "+url+" and " + params);
+    x.send(params);
+}
+
+/**
+ * Asks the wiki API to parse the given text into XML.
+ *
+ * @param {string} t The string to parse; it will be URI-encoded.
+ * @param {function} callback Receives 1 or 2 args with the JSON-encoded 
result, as per doPost.
+**/
+function apiParse( t, callback ) {
+    var args = "action=expandtemplates&format=json&prop=parsetree";
+    var title = document.getElementById( 'dt-title' ).value;
+    if ( title ) {
+        args = args + "&title=" + encodeURIComponent( title );
+    }
+    args = args + "&text=" + encodeURIComponent( t );
+    //debugNote("Action is "+action);
+    var url = document.getElementById( 'dt-api' ).value;
+    doPost( url, args, callback );
+}
+
+/**
+ * Asks the wiki API to parse the given text into wikitext.
+ *
+ * @param {string} t The string to parse; it will be URI-encoded.
+ * @param {function} callback Receives 1 or 2 args with the JSON-encoded 
result, as per doPost.
+**/
+function apiEval( t, callback ) {
+    var args = 
"action=expandtemplates&format=json&prop=wikitext&includecomments=";
+    var title = document.getElementById( 'dt-title' ).value;
+    if ( title ) {
+        args = args + "&title=" + encodeURIComponent( title );
+    }
+    args = args + "&text=" + encodeURIComponent( t );
+    //debugNote("Action is "+action);
+    var url = document.getElementById( 'dt-api' ).value;
+    doPost( url, args, callback );
+}
+
+/**
+ * Asks the wiki API to return the raw content of the given page.
+ *
+ * @param {string} t The page title to parse; it will be URI-encoded.
+ * @param {function} callback Receives 1 or 2 args with the JSON-encoded 
result, as per doPost.
+**/
+function apiGetPage( t, callback ) {
+    var args = 
"action=query&format=json&prop=revisions&rvprop=content&titles=" +
+            encodeURIComponent( t );
+    var url = document.getElementById( 'dt-api' ).value;
+    doPost( url, args, callback);
+}
+
+/**
+ * Asks the wiki API to return the full name of the template being invoked.
+ *
+ * @param {string} t Template name.
+ * @param {function} callback Receives 1 or 2 args with the JSON-encoded 
result, as per doPost.
+**/
+function apiGetTemplateName( t, callback ) {
+    var args = 
"action=parse&format=json&prop=templates&contentmodel=wikitext&text="
+            + encodeURIComponent( t );
+    var url = document.getElementById( 'dt-api' ).value;
+    doPost( url, args, callback );
+}
+
+/**
+ * Retrieves an XML parser, or null if it cannot find one.
+ *
+ * @return {function|null}
+**/
+function getXMLParser() {
+    if ( typeof window.DOMParser != "undefined" ) {
+        return function( xmlStr ) {
+            return ( new window.DOMParser() ).parseFromString( xmlStr, 
"text/xml" );
+        };
+    }
+    return null;
+}
+
+/**
+ * Returns an HTML element for the given text.
+ *
+ * This may be a single text node, or a span with <br>'s inserted to mimic 
linebreaks.
+ *
+ * @param {string} txt Input plain text, possibly with linebreaks.
+ * @param {string|null} cname Optional class name to put on the multi-line 
structure.
+ * @return {HTMLElement}
+**/
+function textWithLinebreaks( txt, cname ) {
+    var s = txt.split( '\n' );
+    if ( s.length <= 1 ) {
+        return document.createTextNode( s[0] );
+    }
+    var h = document.createElement( 'span' );
+    if ( cname ) {
+        h.className = cname;
+    }
+    h.appendChild( document.createTextNode( s[0] ) );
+    for ( var i = 1; i < s.length; i++ ) {
+        h.appendChild( document.createElement( 'br' ) );
+        h.appendChild( document.createTextNode( s[i] ) );
+    }
+    return h;
+}
+
+/**
+ * Determines in which mode a mouse-click should be interpretted.
+ *
+ * @return {string} One of "nothing", "eval", or "descend".
+**/
+function getMouseClickMode() {
+    if ( document.getElementById( 'dt-radio-eval' ).checked ) {
+        return 'eval';
+    }
+    if ( document.getElementById( 'dt-radio-descend' ).checked ) {
+        return 'descend';
+    }
+    return "nothing";
+}
+
+/**
+ * Set or unset the busy flag
+ *
+ * A true flag indicates that a potentially long, asynchronous operation is in 
place and other ones should
+ * not be allowed to proceed until it is done.
+ *
+ * @param {boolean} b
+**/
+function setBusy( b ) {
+    if ( b && !busy ) {
+        busy = true;
+        document.getElementById( 'dt-output' ).classList.add( 'dt-busy' );
+    } else if ( !b && busy ) {
+        busy = false;
+        document.getElementById( 'dt-output' ).classList.remove( 'dt-busy' );
+    }
+}
+
+/**
+ * A fancy fade-in effect to make where text is replaced real obvious.
+ *
+ * @param {HTMLElement} p The element to apply it to.
+**/
+function fader( p ) {
+    // We will create a padding that shrinks over time. This is the initial 
padding.
+    var bp = '12';
+    p.style.padding = bp + 'px';
+    p.classList.add( 'dt-fading' );
+    var intervaltag = window.setInterval( function() {
+        var c = parseInt( p.style.padding, 10 );
+        c--;
+        if ( c <= 0 ) {
+            if ( intervaltag ) {
+                window.clearInterval( intervaltag );
+            }
+            intervaltag = null;
+            p.style.padding = '';
+            p.classList.remove( 'dt-fading' );
+        } else {
+            p.style.padding = String(c) + 'px';
+        }
+    }, 50);
+}
+
+/**
+ * ***************************************
+ * Creating the debug pane and param list.
+ * ***************************************
+**/
+
+/**
+ * Main update routine to process changed input text.
+ *
+ * @param {string} text The new input text to process.
+ * @param {Object|null} newparams An optional set of parameters and their 
defined values which should be
+ *  included in the list of input parameters constructed.
+**/
+function updateFromNewInput( text, newparams ) {
+    setBusy ( false );
+    if ( text === '' ) {
+        updateFromXML( '' );
+    } else {
+        apiParse( text, function( k, t ) {
+            if ( k == "OK" ) {
+                var result = window.JSON.parse( t );
+                if ( result.expandtemplates && 
result.expandtemplates.parsetree ) {
+                    updateFromXML( result.expandtemplates.parsetree, newparams 
);
+                } else {
+                    updateFromXML( '' );
+                    if ( !result.error || result.error.code!="notext" )
+                        debugNote( mw.message( 'debugtemplates-error-parse' ) 
+ t );
+                }
+            } else {
+                updateFromXML( '' );
+                debugNote( mw.message( 'debugtemplates-error-parse' ) + k );
+            }
+        });
+    }
+}
+
+/**
+ * Cleans up the given text for transcluding by parsing through the 
includeonly, onlyinclude, and
+ * noinclude tags and extracting what would be included.
+ *
+ * @param {string} text
+ * @return {string}
+**/
+function transcludeText( text ) {
+    return transcludeOnlyInclude( text );
+}
+
+/**
+ * Clean up the given text by extracting any <onlyinclude> blocks, and then 
processing
+ * the remainder for includeonly and noinclude.
+ *
+ * @param {string} text
+ * @param {boolean} imeanit If true this indicates to return nothing if no 
<onlyinclude> blocks are
+ *  found.  Used in recursive calls.
+ * @return {string}
+**/
+function transcludeOnlyInclude( text, imeanit ) {
+    var re = new RegExp( '^((?:.|\\n)*?)(<onlyinclude\\s*/?>)((?:.|\\n)*)$', 
'i' );
+    var m = re.exec( text );
+    if ( !m ) {
+        // No onlyinclude tag found
+        if ( imeanit ) {
+            return '';
+        }
+        return transcludeNoAndOnly( text );
+    }
+    if ( m[2].indexOf( '\\/>' ) > 0) {
+        // Singleton tag
+        return transcludeOnlyInclude( m[3], true );
+    }
+    // Look for a closing tag
+    // Note this is more restrictive, and not case-insensitive
+    var reEnd = new RegExp( '^((?:.|\\n)*?)(</onlyinclude>)((?:.|\\n)*)$', '' 
);
+    var mm = reEnd.exec( m[3] );
+    if ( !mm ) {
+        // No closing tag---opening tag doesn't count then
+        if ( imeanit ) {
+            return '';
+        }
+        return transcludeNoAndOnly( text );
+    }
+    // Ok, found some included text (contained in mm[1]), look for any more
+    return transcludeNoAndOnly( mm[1] ) + transcludeOnlyInclude( mm[3], true );
+}
+
+/**
+ * Clean up the given text by removing <noinclude> blocks and discarding 
<includeonly> tags.
+ *
+ * @param {string} text
+ * @return {string}
+**/
+function transcludeNoAndOnly( text ) {
+    var rc = '';
+    var re = new RegExp( '^((?:.|\\n)*?)(<noinclude\\s*/?>)((?:.|\\n)*)$', 'i' 
);
+    var reIO = new RegExp( '<includeonly\\s*/?>', 'ig' );
+    var m = re.exec( text );
+    if ( !m ) {
+        // No noinclude tags
+        return text.replace( reIO, '' ).replace( '</includeonly>', '' );
+    }
+    var singleTag = m[2].indexOf( '\\/>' ) > 0;
+
+    // Certainly have the text prior to the tag
+    rc = m[1].replace( reIO, '' ).replace( '</includeonly>', '' );
+
+    // We have an outer noinclude.  Look for a closing tag, discard the 
enclosed text,
+    //  and recurse on the remainder.
+    // Note more restrictive, and not case-insensitive
+    var reEnd = new RegExp( '^((?:.|\\n)*?)(</noinclude>)((?:.|\\n)*)$', '' );
+    var mm = reEnd.exec( m[3] );
+    if ( !mm ) {
+        // No closing tag---assume it continues to the end
+        return rc;
+    }
+    return rc + transcludeNoAndOnly( mm[3] );
+}
+
+
+/**
+ * Callback function which updates the view given the XML derived from the raw 
input.
+ *
+ * @param {string} x Well-formed XML as a string
+ * @param {Object|null} inheritparams An optional set of parameters and their 
defined values which should
+ *  be included in the list of input parameters constructed.
+**/
+function updateFromXML( x, inheritparams ) {
+    var i,pname;
+    // First parse the xml and build an AST
+    ast = ( x==='' ) ? null : getXMLParser()( x );
+
+    // Wipe out the global var of previous params and define a new set
+    params = [];
+
+    // Now extract all the parameters in the AST so we can build our parameter 
list
+    var newparams = {};
+    var astparams = null;
+    if (ast) {
+        astparams = ast.getElementsByTagName( 'tplarg' );
+        if ( astparams ) {
+            for ( i = 0; i < astparams.length; i++ ) {
+                pname = getParamName( astparams[i], i );
+                if ( newparams[ pname ] === undefined ) {
+                    newparams[ pname ] = true;
+                    params.push( { name: pname, row: 0, used: true} );
+                }
+            }
+        }
+    }
+    // Add in any in inheritparams
+    if ( inheritparams ) {
+        for ( var p in inheritparams ) {
+            pname = p.trim();
+            if ( newparams[ pname ] === undefined ) {
+                newparams[ pname ] = true;
+                params.push( { name: pname, row: 0 } );
+            }
+        }
+    }
+    // Now sort them alphabetically
+    params.sort( function ( a, b ) {
+        return a.name.localeCompare( b.name );
+    } );
+    // Create the mapping from AST nodes to their entry in the param array
+    if ( astparams ) {
+        for ( i=0; i < astparams.length; i++ ) {
+            pname = getParamName( astparams[i], i );
+            // Look for it in our list of params
+            for ( var j = 0; j < params.length; j++ ) {
+                if ( params[ j ].name == pname) {
+                    // Set the 'pindex' property to the row number
+                    astparams[ i ].setAttribute( 'pindex', j );
+                    break;
+                }
+            }
+        }
+    }
+
+    // Construct the params array
+    updateParams( params, inheritparams );
+    // Construct the output
+    htmlFromAST( ast );
+}
+
+/**
+ * Extracts a parameter name from a <tplarg> AST node.
+ *
+ * @param {XMLElement} node
+ * @param {number|string} i Unique index used to help form a unique name when 
the parameter name is a
+ *  constructed one.
+ * @return {string}
+**/
+function getParamName( node, i ) {
+    // This should not happen...
+    if ( !node.firstChild.firstChild ) {
+        return '';
+    }
+    // If the name itself is tree then we cannot determine the name until it 
has been fully parsed, so we
+    //  make something up using the unique index number given.
+    if ( node.firstChild.childNodes.length > 1 || 
node.firstChild.firstChild.nodeValue === null ) {
+        return '<' + mw.message( 'debugtemplates-args-constructed' ) + i + '>';
+    }
+    return node.firstChild.firstChild.nodeValue.trim();
+}
+
+/**
+ * Retrieves the manual value the user has associated with a parameter in the 
displayed list of
+ * parameters.
+ *
+ * @param {number} pindex Row number of the corresponding parameter in the 
params array
+ * @return {string|null} May return an empty string, so null is used to 
indicate a parameter that has not
+ *  been set
+**/
+function getParamText( pindex ) {
+    var rownum = params[ pindex ].row;
+    var row = document.getElementById( 'dt-argtable-row-number-' + rownum );
+    var ptext = row.cells[ 2 ].firstChild;
+    if ( ptext.classList.contains( 'dt-arg-set-yes' ) ) {
+        return ptext.value;
+    }
+    return null;
+}
+
+/**
+ * Retrieves the DOM cell associated with the manual value of a parameter in 
the displayed list of
+ * parameters.
+ *
+ * @param {string} name The name of the parameter
+ * @param {HTMLElement|null} argtable The DOM node for the argtable <tbody>
+ * @return {HTMLElement|null} Can return null if not found
+**/
+function getParamValue( name, argtable ) {
+    if ( !argtable ) {
+        return null;
+    }
+    for ( var i = 0; i < argtable.rows.length; i++ ) {
+        var celln = argtable.rows[ i ].cells[ 1 ].firstChild;
+        if ( celln.nodeValue == name ) {
+            return argtable.rows[i].cells[2].firstChild;
+        }
+    }
+    return null;
+}
+
+/**
+ * Reconstructs the list of available parameters being displayed.
+ *
+ * @param {object} params The new set of parameters
+ * @param {object|null} inheritparams The set of name -> value mappings that 
should initialize the displayed value
+**/
+function updateParams( params, inheritparams ) {
+    var argtable = document.getElementById( 'dt-argtable' );
+    var eall = mw.message( 'debugtemplates-args-eval-all' );
+    // We are going to replace the entire table body and wipe out the existing 
one
+    var new_tbody = document.createElement( 'tbody' );
+    // There may not be a tbody, so it can be null
+    var old_tbody = argtable.getElementsByTagName( 'tbody' );
+    if ( old_tbody ) {
+        old_tbody = old_tbody[ 0 ];
+    }
+
+    // Now construct each row from a param
+    for ( var i = 0; i < params.length; i++ ) {
+        var oldval = getParamValue( params[ i ].name, old_tbody );
+        var row = document.createElement( 'tr' );
+
+        // First cell is the set/unset status
+        var c = document.createElement( 'td' );
+        c.className = 'dt-arg-centered';
+        // Create a toggle-able boolean value
+        var span = document.createElement( 'span' );
+        if ( ( inheritparams && inheritparams [ params[ i ].name ] !== 
undefined ) ||
+             ( oldval !== null && oldval.classList.contains( 'dt-arg-set-yes' 
) ) ) {
+                 span.appendChild( document.createTextNode( argy ) );
+        } else {
+            span.appendChild( document.createTextNode( argn ) );
+        }
+        span.addEventListener( 'click', paramSetHandler, false );
+        c.appendChild( span );
+        row.appendChild( c );
+
+        // Then create the parameter name
+        c = document.createElement( 'td' );
+        c.appendChild( document.createTextNode( params[ i ].name ) );
+        row.appendChild( c );
+
+        // The parameter value is a textarea since it can be large, multiline 
text
+        c = document.createElement( 'td' );
+        span = document.createElement( 'textarea' );
+        if ( ( inheritparams && inheritparams[ params[ i ].name ] !== 
undefined ) ||
+             ( oldval !== null && oldval.classList.contains( 'dt-arg-set-yes' 
) ) ) {
+                 span.setAttribute( 'class', 'dt-arg-set-yes' );
+        } else {
+            span.setAttribute( 'class', 'dt-arg-set-no' );
+        }
+        if ( inheritparams && inheritparams[ params[ i ].name ] !== undefined) 
{
+            span.value = inheritparams[ params[ i ].name ];
+        } else if ( oldval !== null ) {
+            span.value = oldval.value;
+        } else {
+            span.value = '';
+        }
+        span.style.width = "95%";
+        c.appendChild( span );
+        row.appendChild( c );
+
+        // Then create the eval-all-instances button
+        c = document.createElement( 'td' );
+        c.className = 'dt-arg-centered';
+        span = document.createElement( 'input' );
+        span.setAttribute( 'type', 'button' );
+        span.setAttribute( 'value', eall );
+        span.addEventListener( 'click', paramEval, false);
+        c.appendChild( span );
+        row.appendChild( c );
+
+        // Ensure the params entry's row field is correct
+        row.setAttribute( 'id', 'dt-argtable-row-number-' + i );
+        if ( !params[ i ].used )
+            row.classList.add( 'dt-arg-unused' );
+        new_tbody.appendChild( row );
+        params[ i ].row = i;
+    }
+    var prev = argtable.getElementsByTagName( 'tbody' )[ 0 ];
+    if ( prev ) {
+        argtable.replaceChild( new_tbody, prev );
+    } else {
+        argtable.appendChild( new_tbody );
+    }
+}
+
+/**
+ * Main entry point to construct the DOM tree from the AST and install it in 
the output pane.
+ *
+ * @param {XMLElement|null} ast
+**/
+function htmlFromAST( ast ) {
+    // Reset our maximum index values and mappings between unique id numbers
+    nindex = 0;
+    nTox = {};
+    xindex = 0;
+    // No undo is possible after this
+    resetButtonNode.setAttribute( 'disabled', 'disabled' );
+    undoButtonNode.setAttribute( 'disabled', 'disabled' );
+    if ( ast && ast.documentElement ) {
+        var oh = htmlFromAST_r( ast.documentElement );
+        setOutput( oh );
+    }
+}
+
+/**
+ * Recursive entry point to construct the DOM tree from the AST.
+ *
+ * @param {XMLElement|undefined} node
+ * @return {HTMLElement}
+**/
+function htmlFromAST_r( node ) {
+    var h, i, span, next;
+    // This shouldn't happen but we'll be defensive
+    if ( node === undefined ) {
+        h = document.createElement( 'span' );
+        h.className = 'dt-error';
+        h.appendChild( document.createTextNode( 'Undefined' ) );
+        return h;
+    }
+    switch( node.tagName ) {
+    case 'root':
+        // The <root> contains a list of elements
+        h = document.createElement( 'span' );
+        h.className = 'dt-node';
+        // A special id for the root DOM node
+        h.setAttribute( 'id', 'dt-id-root' );
+        span = document.createElement( 'span' );
+        span.className = 'dt-node';
+        i = nindex++;
+        node.id = xindex++;
+        span.setAttribute( 'id', 'dt-id-' + i );
+        h.appendChild(span);
+        for ( i = 0; i < node.childNodes.length; i++ ) {
+            span.appendChild( htmlFromAST_r( node.childNodes[ i ] ) );
+        }
+        break;
+    case 'template':
+        h = htmlFromAST_r_template( node );
+        break;
+    case 'part':
+        // A <part> has a <name> and a <value>, possibly separated by an "="
+        if ( node.childNodes.length != 2 && node.childNodes.length != 3) {
+            // I don't think this is possible
+            h = document.createElement( 'span' );
+            h.className = 'dt-error';
+            h.appendChild( document.createNode( 'improper argument structure: 
' +
+                                                node.childNodes.length ) );
+        } else {
+            // First child is the name
+            h = document.createElement( 'span' );
+            h.className = 'dt-node dt-node-arg';
+            h.appendChild( htmlFromAST_r( node.firstChild ) );
+            next = node.firstChild.nextSibling;
+            if (next.tagName != 'value' ) {
+                // The "=" sign
+                h.appendChild( htmlFromAST_r( next ) );
+                next = next.nextSibling;
+            }
+            h.appendChild( htmlFromAST_r( next ) );
+        }
+        break;
+    case 'title':
+        h = document.createElement( 'span' );
+        h.className = 'dt-node dt-node-title';
+        for ( i = 0; i < node.childNodes.length; i++ ) {
+            h.appendChild( htmlFromAST_r( node.childNodes[ i ] ) );
+        }
+        break;
+    case 'value':
+        h = document.createElement( 'span' );
+        h.className = 'dt-node dt-node-value';
+        for ( i = 0; i < node.childNodes.length; i++ ) {
+            h.appendChild( htmlFromAST_r( node.childNodes[ i ] ) );
+        }
+        break;
+    case 'name':
+        h = document.createElement( 'span' );
+        h.className = 'dt-node dt-node-name';
+        for ( i = 0; i < node.childNodes.length; i++ ) {
+            h.appendChild( htmlFromAST_r( node.childNodes[ i ] ) );
+        }
+        break;
+    case 'tplarg':
+        h = htmlFromAST_r_tplarg( node );
+        break;
+    case 'comment':
+        h = document.createElement( 'span' );
+        h.className = 'dt-node dt-node-comment';
+        h.appendChild( htmlFromAST_r( node.firstChild ) );
+        break;
+    case 'ignore':
+        // These wrap <includeonly> and </onlyinclude> and their closers
+        if ( node.childNodes.length > 0 ) {
+            h = document.createElement( 'span' );
+            h.className = 'dt-node dt-node-ignore';
+            for ( i = 0; i < node.childNodes.length; i++ ) {
+                h.appendChild( htmlFromAST_r( node.childNodes[ i ] ) );
+            }
+        }
+        break;
+    case 'ext':
+        // The only nodes we recognize are nowiki and pre
+        if ( node.firstChild &&
+             node.firstChild.tagName == 'name' &&
+             node.firstChild.firstChild &&
+             node.firstChild.firstChild.nodeType == 3 &&
+             (node.firstChild.firstChild.nodeValue == 'nowiki' ||
+              node.firstChild.firstChild.nodeValue == 'pre' ) ) {
+                  // Should have a <name>, an <attribute>, an <inner> and 
optionally a <close>
+                  h = document.createElement( 'span' );
+                  var extname = node.firstChild.firstChild.nodeValue;
+                  h.className = 'dt-node dt-node-ext dt-node-ext-' + extname;
+                  h.appendChild( document.createTextNode( '<' + extname ) );
+                  next = node.firstChild.nextSibling;
+                  if (next && next.tagName == 'attr' ) {
+                      if ( next.firstChild ) {
+                          h.appendChild( document.createTextNode( ' ' + 
next.firstChild.nodeValue ) );
+                      }
+                      next = next.nextSibling;
+                  }
+                  h.appendChild(document.createTextNode( '>' ) );
+                  if ( next && next.tagName == 'inner' ) {
+                      if ( next.firstChild ) {
+                          h.appendChild( document.createTextNode( 
next.firstChild.nodeValue ) );
+                      }
+                  }
+                  h.appendChild( document.createTextNode( '</' + extname + '>' 
) );
+                  break;
+              }
+        // For unrecognized cases of the ext tag, let it run into the default
+    default:
+        if ( node.nodeType != 3 ) {
+            // Something we don't parse, so just represent it literally
+            h = document.createElement( 'span' );
+            h.className = 'dt-node';
+            if ( node.childNodes.length == 0 ) {
+                h.appendChild( document.createTextNode( '<' + node.tagName + 
'/>' ) );
+            } else {
+                h.appendChild( document.createTextNode( '<' + node.tagName + 
'>' ) );
+                for ( i = 0; i < node.childNodes.length; i++ ) {
+                    h.appendChild( htmlFromAST_r( node.childNodes[ i ] ) );
+                }
+                h.appendChild( document.createTextNode( '</' + node.tagName + 
'>' ) );
+            }
+        } else if ( node.nodeValue === null || node.nodeValue === undefined) {
+            h = document.createTextNode( '' );
+        } else {
+            // Plain old text
+            h = textWithLinebreaks( node.nodeValue, 'dt-node 
dt-node-multiline' );
+        }
+        break;
+    }
+    return h;
+}
+
+
+/**
+ * Recursive entry point to construct the DOM tree from a <template> AST node.
+ *
+ * @param {XMLElement} node
+ * @return {HTMLElement}
+**/
+function htmlFromAST_r_template( node ) {
+    var i;
+    // Give the input AST node a unique number
+    node.id = xindex++;
+
+    // A <template> has a <title> followed by a list of <part>s for the 
arguments
+    var espans = [];
+    var h = document.createElement( 'span' );
+    h.className = 'dt-node dt-node-template';
+    var tid = nindex++;
+    h.setAttribute( 'id', 'dt-id-' + tid );
+
+    // The opening braces
+    var span = document.createElement( 'span' );
+    span.appendChild( document.createTextNode( '{{' ) );
+    var inid = nindex++;
+    span.setAttribute( 'id', 'dt-id-' + inid );
+    h.appendChild( span );
+    espans.push( span );
+
+    // Record the mapping in the global nTox array
+    nTox[ 'dt-id-' + inid ] = node.id;
+    span.addEventListener( 'click', evalText, false );
+
+    // The template name
+    span = document.createElement( 'span' );
+    var nid = nindex++;
+    span.setAttribute( 'id', 'dt-id-' + nid );
+    span.appendChild( htmlFromAST_r( node.firstChild ) );
+    h.appendChild( span );
+    espans.push( span );
+
+    // A span for all arguments
+    span = document.createElement( 'span' );
+    var aid = nindex++;
+    span.setAttribute( 'id', 'dt-id-' + aid );
+
+    var pipeIds = [];
+    for ( i = 1; i < node.childNodes.length; i++ ) {
+        var pspan = document.createElement( 'span' );
+        var pid = nindex++;
+        pspan.setAttribute( 'id', 'dt-id-' + pid );
+        pipeIds.push( pid );
+        pspan.appendChild( document.createTextNode( '|' ) );
+        span.appendChild( pspan );
+        span.appendChild( htmlFromAST_r( node.childNodes[ i ] ) );
+    }
+    h.appendChild( span );
+    espans.push( span );
+
+    // Closing braces
+    span = document.createElement( 'span' );
+    var outid = nindex++;
+    span.setAttribute( 'id', 'dt-id-' + outid );
+    span.appendChild( document.createTextNode( '}}' ) );
+    h.appendChild( span );
+    espans.push( span );
+
+    // Record ids so the emphasize listener can find all the right pieces
+    var pipeList = pipeIds.join( ' ' );
+    for ( i = 0; i <espans.length; i++ ) {
+        espans[ i ].setAttribute( 'dt-emph-template-out', outid );
+        espans[ i ].setAttribute( 'dt-emph-template-in', inid );
+        espans[ i ].setAttribute( 'dt-emph-template-name', nid );
+        espans[ i ].setAttribute( 'dt-emph-template-args', aid );
+        espans[ i ].setAttribute( 'dt-emph-template-pipes', pipeList );
+        espans[ i ].addEventListener( 'mouseover', emphasizeTemplate );
+        espans[ i ].addEventListener( 'mouseout', emphasizeTemplate );
+    }
+    return h;
+}
+
+/**
+ * Emphasize handler for template structures.
+**/
+function emphasizeTemplate() {
+    var outSpan = document.getElementById( 'dt-id-' + this.getAttribute( 
'dt-emph-template-out' ) );
+    var inSpan = document.getElementById( 'dt-id-' + this.getAttribute( 
'dt-emph-template-in' ) );
+    var nameSpan = document.getElementById( 'dt-id-' + this.getAttribute( 
'dt-emph-template-name' ) );
+    var argSpan = document.getElementById( 'dt-id-' + this.getAttribute( 
'dt-emph-template-args' ) );
+    var pipes = this.getAttribute( 'dt-emph-template-pipes' );
+
+    outSpan.classList.toggle( 'dt-emphasize-template-out' );
+    inSpan.classList.toggle( 'dt-emphasize-template-in' );
+    nameSpan.classList.toggle( 'dt-emphasize-template-name' );
+    argSpan.classList.toggle( 'dt-emphasize-template-args' );
+
+    pipes = pipes.split( ' ' );
+    for (var i = 0; i < pipes.length; i++ ) {
+        if ( pipes[ i ] === '' ) {
+            continue;
+        }
+        document.getElementById( 'dt-id-' + pipes[ i ] ).classList.toggle( 
'dt-emphasize-template-pipe' );
+    }
+}
+
+/**
+ * Recursive entry point to construct the DOM tree from a <tplarg> AST node.
+ *
+ * @param {XMLElement} node
+ * @return {HTMLElement}
+**/
+function htmlFromAST_r_tplarg( node ) {
+    var i;
+    // Give the input AST node a unique number
+    node.id = xindex++;
+
+    // Like templates, a <tplarg> has a <title> followed by a list of <part>s 
for the arguments
+    var espans = [];
+    var h = document.createElement( 'span' );
+    h.className = 'dt-node dt-node-tplarg';
+    var tid = nindex++;
+    h.setAttribute( 'id', 'dt-id-' + tid );
+
+    // The opening braces
+    var span = document.createElement( 'span' );
+    span.appendChild( document.createTextNode( '{{{' ) );
+    var inid = nindex++;
+    span.setAttribute( 'id', 'dt-id-' + inid );
+    h.appendChild( span );
+    espans.push( span );
+
+    // Record the mapping
+    nTox[ 'dt-id-' + inid ] = node.id;
+    span.addEventListener( 'click', evalText, false );
+
+    // The parameter name
+    span = document.createElement( 'span' );
+    var nid = nindex++;
+    span.setAttribute( 'id', 'dt-id-' + nid );
+    span.appendChild( htmlFromAST_r( node.firstChild ) );
+    h.appendChild( span );
+    espans.push( span );
+
+    // Any parameter arguments (although at most 1 is meaningful I think)
+    span = document.createElement( 'span' );
+    var aid = nindex++;
+    span.setAttribute( 'id', 'dt-id-' + aid );
+
+    var pipeIds = [];
+    for ( i = 1; i < node.childNodes.length; i++ ) {
+        var pspan = document.createElement( 'span' );
+        var pid = nindex++;
+        pspan.setAttribute( 'id', 'dt-id-' + pid );
+        pipeIds.push( pid );
+        pspan.appendChild( document.createTextNode( '|' ) );
+        span.appendChild( pspan );
+        span.appendChild( htmlFromAST_r( node.childNodes[ i ] ) );
+    }
+    h.appendChild( span );
+    espans.push( span );
+
+    // Closing braces
+    span = document.createElement( 'span' );
+    var outid = nindex++;
+    span.setAttribute( 'id', 'dt-id-' + outid );
+    span.appendChild( document.createTextNode( '}}}' ) );
+    h.appendChild( span );
+    espans.push( span );
+
+    // Record ids so the emphasize listener can find all the right pieces
+    var pipeList = pipeIds.join( ' ' );
+
+    for ( i = 0; i < espans.length; i++ ) {
+        espans[ i ].setAttribute( 'dt-emph-tplarg-out', outid );
+        espans[ i ].setAttribute( 'dt-emph-tplarg-in', inid );
+        espans[ i ].setAttribute( 'dt-emph-tplarg-name', nid );
+        espans[ i ].setAttribute( 'dt-emph-tplarg-args', aid );
+        espans[ i ].setAttribute( 'dt-emph-tplarg-pipes', pipeList );
+        espans[ i ].addEventListener( 'mouseover', emphasizeTplarg );
+        espans[ i ].addEventListener( 'mouseout', emphasizeTplarg );
+    }
+    return h;
+}
+
+/**
+ * Emphasize handler for tplarg structures.
+**/
+function emphasizeTplarg() {
+    var outSpan = document.getElementById( 'dt-id-' + this.getAttribute( 
'dt-emph-tplarg-out' ) );
+    var inSpan = document.getElementById( 'dt-id-' + this.getAttribute( 
'dt-emph-tplarg-in' ) );
+    var nameSpan = document.getElementById( 'dt-id-' + this.getAttribute( 
'dt-emph-tplarg-name' ) );
+    var argSpan = document.getElementById( 'dt-id-' + this.getAttribute( 
'dt-emph-tplarg-args' ) );
+    var pipes = this.getAttribute( 'dt-emph-tplarg-pipes' );
+
+    outSpan.classList.toggle( 'dt-emphasize-tplarg-out' );
+    inSpan.classList.toggle( 'dt-emphasize-tplarg-in' );
+    nameSpan.classList.toggle( 'dt-emphasize-tplarg-name' );
+    argSpan.classList.toggle( 'dt-emphasize-tplarg-args' );
+
+    pipes = pipes.split( ' ' );
+    for ( var i = 0; i < pipes.length; i++ ) {
+        if ( pipes[ i ] === '' ) {
+            continue;
+        }
+        document.getElementById( 'dt-id-' + pipes[ i ] ).classList.toggle( 
'dt-emphasize-tplarg-pipe' );
+    }
+}
+
+/**
+ * **************************
+ * Extracting text functions.
+ * **************************
+**/
+
+/**
+ * Main and recursive entry point for converting an AST node into equivalent 
wikitext.
+ *
+ * @param {XMLElement} node
+ * @return {string}
+**/
+function textFromAST( node ) {
+    var i;
+    var txt = '';
+
+    switch( node.tagName ) {
+    case 'part':
+    case 'title':
+    case 'value':
+    case 'name':
+    case 'comment':
+    case 'ignore':
+    case 'root':
+        // These elements just wrap their children
+        for ( i = 0; i <node.childNodes.length; i++ ) {
+            txt += textFromAST( node.childNodes[ i ] );
+        }
+        break;
+    case 'template':
+        txt = '{{' + textFromAST( node.firstChild );
+        for ( i = 1; i < node.childNodes.length; i++ ) {
+            txt += '|';
+            txt += textFromAST( node.childNodes[ i ] );
+        }
+        txt += '}}';
+        break;
+    case 'tplarg':
+        // First check for manual settings of this parameter
+        var pindex = node.getAttribute( 'pindex' );
+        txt = getParamText( pindex );
+        if (txt === null) {
+            // It is unset so construct the usual text
+            txt = '{{{' + textFromAST( node.firstChild );
+            for ( i = 1; i < node.childNodes.length; i++ ) {
+                txt += '|';
+                txt += textFromAST( node.childNodes[ i ] );
+            }
+            txt += '}}}';
+        }
+        break;
+    case 'ext':
+        // Only recognize nowiki and pre
+        if (node.firstChild &&
+            node.firstChild.tagName == 'name' &&
+            node.firstChild.firstChild &&
+            node.firstChild.firstChild.nodeType == 3 &&
+            (node.firstChild.firstChild.nodeValue == 'nowiki' ||
+             node.firstChild.firstChild.nodeValue == 'pre' ) ) {
+                 // Should have a <name>, an <attribute>, an <inner> and 
optionally a <close>
+                 var extname = node.firstChild.firstChild.nodeValue;
+                 txt += '<' + extname;
+                 var next = node.firstChild.nextSibling;
+                 if (next && next.tagName == 'attr' ) {
+                     if (next.firstChild) {
+                         txt += ' ' + next.firstChild.nodeValue;
+                     }
+                     next = next.nextSibling;
+                 }
+                 txt += '>';
+                 if (next && next.tagName == 'inner' ) {
+                     if (next.firstChild) {
+                         txt += next.firstChild.nodeValue;
+                     }
+                 }
+                 txt += '</' + extname + '>';
+                 break;
+             }
+        // Unrecognized case of the ext tag which we let run into the default
+    default:
+        if ( node.nodeType != 3 ) {
+            if ( node.childNodes.length == 0 ) {
+                txt = '<' + node.tagName + '/>';
+            } else {
+                txt = '<' + node.tagName + '>';
+                for ( i = 0; i < node.childNodes.length; i++ ) {
+                    txt += textFromAST( node.childNodes[ i ] );
+                }
+                txt += '</' + node.tagName + '>';
+            }
+        } else if ( node.nodeValue === null || node.nodeValue === undefined) {
+            txt = '';
+        } else {
+            txt = node.nodeValue;
+        }
+        break;
+    }
+    return txt;
+}
+
+/**
+ * Handler attached to a template start.  It either does nothing, or replaces 
the template text with its
+ * evaluated version, or descends into the template call, depending on the 
click mode.
+**/
+function evalText() {
+    if ( busy ) {
+        return;
+    }
+    setBusy( true );
+
+    var mode = getMouseClickMode();
+    if (mode == 'nothing' ) {
+        setBusy( false );
+        return;
+    }
+    // For templates we have 2 different modes to consider
+    if ( this.parentNode.classList.contains( 'dt-node-template' ) ) {
+        if (mode == 'descend' ) {
+            descendInto( this );
+            return;
+        }
+    }
+    // First check if it is already evaluated and we're just redoing it
+    if ( this.parentNode.lastChild.classList.contains( 'dt-node-evaluated' ) ) 
{
+        for ( var j = 0; j < this.parentNode.childNodes.length; j++ ) {
+            this.parentNode.childNodes[ j ].classList.toggle( 
'dt-node-invisible' );
+        }
+        setBusy( false );
+        return;
+    }
+
+    // Map this node to its AST equivalent
+    var n = this.id;
+    var x = nTox[ n ];
+    var astNode = ast.getElementById( x );
+    // Get the AST text
+    var txt = textFromAST( astNode );
+    if ( txt == '' ) {
+        // Empty strings do not need an evaluation
+        evalTextDisplay( txt, n );
+    } else {
+        // We call the API to parse it into wikitext
+        apiEval( txt, function( k, t ) {
+            if ( k == "OK" ) {
+                var result = window.JSON.parse( t );
+                if ( result.expandtemplates && result.expandtemplates.wikitext 
!== undefined ) {
+                    evalTextDisplay( result.expandtemplates.wikitext, n );
+                } else {
+                    debugNote( mw.message('debugtemplates-error-eval') + t );
+                    setBusy( false );
+                }
+            } else {
+                debugNote( mw.message('debugtemplates-error-eval') + k );
+                setBusy( false );
+            }
+        });
+    }
+}
+
+/**
+ * Callback after parsing text to display it.
+ *
+ * @param {string} t The text to display
+ * @param {string} node The id of the node that had the evalText handler
+ * @param {boolean} more A flag to indicate whether the busy flag should be 
turned off after
+**/
+function evalTextDisplay( t, n, more ) {
+    //debugNote('evalled text: "'+t+'"');
+    var node = document.getElementById( n );
+    if ( node ) {
+        // It should be the first child of the node we want to replace
+        node = node.parentNode;
+        if ( node.lastChild.classList.contains( 'dt-node-evaluated' ) ) {
+            // Already evaluated
+            if ( !more )
+                setBusy( false );
+            return;
+        }
+        // Make all current children invisible
+        for ( var i = 0; i < node.childNodes.length; i++ ) {
+            node.childNodes[ i ].classList.toggle( 'dt-node-invisible' );
+        }
+        // And add a new visible span containing the evaluated text
+        var span = document.createElement( 'span' );
+        span.appendChild( textWithLinebreaks( t ) );
+        span.className = 'dt-node-evaluated';
+        span.addEventListener( 'click', unevalText, false );
+        span.addEventListener( 'mouseover', emphasizeEvalText );
+        span.addEventListener( 'mouseout', emphasizeEvalText );
+        node.appendChild( span );
+        // A new undo event is possible now
+        resetButtonNode.removeAttribute( 'disabled' );
+        undoButtonNode.removeAttribute( 'disabled' );
+        if ( lastUndo.length >= maxUndos ) {
+            lastUndo.shift();
+        }
+        if ( more ) {
+            // If we're not done then just accumulate this undo with the 
previous
+            var prevDid = lastUndo[ lastUndo.length - 1 ];
+            prevDid.push( node.id );
+        } else {
+            lastUndo.push( node.id );
+        }
+        // And do a fancy fade-in effect
+        fader( span );
+    }
+    if ( !more ) {
+        setBusy( false );
+    }
+}
+
+/**
+ * Handler to un-show evaluated text and restore the original.
+ *
+**/
+function unevalText() {
+    if ( busy ) {
+        return;
+    }
+    setBusy( true );
+    var mode = getMouseClickMode();
+    if ( mode == 'nothing' ) {
+        setBusy( false );
+        return;
+    }
+    var node = this.parentNode;
+    for ( var i = 0; i < node.childNodes.length; i++ ) {
+        node.childNodes[ i ].classList.toggle( 'dt-node-invisible' );
+    }
+    setBusy( false );
+}
+
+/**
+ * Emphasize handler for evaluated text.
+**/
+function emphasizeEvalText() {
+    this.classList.toggle( 'dt-node-emphasize-evaluated' );
+}
+
+/**
+ * Handler for the parameter eval-all buttons.
+**/
+function paramEval() {
+    if ( busy ) {
+        return;
+    }
+    this.setAttribute( 'disabled', 'disabled' );
+    var row = this.parentNode.parentNode;
+    var pname = row.childNodes[ 1 ].firstChild;
+    if ( !pname ) {
+        return;
+    }
+    setBusy( true );
+    // We need to know our row number
+    var rown = row.id.replace( /[^0-9]*/g, '' );
+    // Look through all the parameters displayed
+    var instances = document.getElementsByClassName( 'dt-node-tplarg' );
+    // Start off a chain of individual lookups
+    if ( instances ) {
+        paramEvalNext( 0, instances, rown );
+    } else {
+        setBusy( false );
+    }
+}
+
+/**
+ * Chaining function to evaluate the next instance of a specific parameter.
+ *
+ * @param {number} i The index into the instances array
+ * @param {object} instances An array of HTMLElements
+ * @param {number} rown The row-index of the specific parameter in the 
displayed list
+**/
+function paramEvalNext( i, instances, rown ) {
+    var first = ( i == 0 ) ? true : false;
+    var continuing = false;
+    // Look for the next instance to evaluate from i onward
+    while ( i < instances.length ) {
+        // Verify this parameter is actually an instance of our specific 
parameter
+        // This is a bit round about.  We first get the id of the parameter, 
then
+        //  look it up in the node->AST map to get the matching AST node, and
+        //  then find the param entry from that AST node's pindex, and then
+        //  check if that's us.
+        var n = instances[ i ].firstChild.id;
+        var x = nTox[ n ];
+        var astNode = ast.getElementById( x );
+        var pindex = astNode.getAttribute( 'pindex' );
+        if ( params[ pindex ] && params[ pindex ].row == rown ) {
+            // Yes, found a matching param
+            // If this is the first one, setup the undo list
+            if ( first ) {
+                resetButtonNode.removeAttribute( 'disabled' );
+                undoButtonNode.removeAttribute( 'disabled' );
+                if ( lastUndo.length >= maxUndos ) {
+                    lastUndo.shift();
+                }
+                lastUndo.push( new Array() );
+                first = false;
+            }
+            // Get the parameter text
+            var txt = textFromAST( astNode );
+            if ( txt == '' ) {
+                // If empty string we can just show it
+                evalTextDisplay( txt, n, true );
+            } else {
+                // If non-empty, we need to evaluate it through the API
+                window.setTimeout( function( txt, n, i, instances, rown ) {
+                    apiEval( txt, function( k, t ) {
+                        var row;
+                        if ( k == "OK" ) {
+                            var result = window.JSON.parse( t );
+                            if ( result.expandtemplates && 
result.expandtemplates.wikitext !== undefined ) {
+                                evalTextDisplay( 
result.expandtemplates.wikitext, n, true );
+                                // Chain into an eval of the next one
+                                paramEvalNext( i + 1, instances, rown );
+                            } else {
+                                debugNote( mw.message( 
'debugtemplates-error-eval' ) + t );
+                                setBusy( false );
+                                row = document.getElementById( 
'dt-argtable-row-number-' + rown );
+                                row.childNodes[ 3 
].firstChild.removeAttribute( 'disabled' );
+                            }
+                        } else {
+                            debugNote( mw.message( 'debugtemplates-error-eval' 
) + k );
+                            setBusy( false );
+                            row = document.getElementById( 
'dt-argtable-row-number-' + rown );
+                            row.childNodes[ 3 ].firstChild.removeAttribute( 
'disabled' );
+                        }
+                    } );
+                }, apiCallInterval, txt, n, i, instances, rown );
+                continuing = true;
+                break;
+            }
+        }
+        i++;
+    }
+    if ( !continuing ) {
+        // Last one done
+        // Just check that the undo wasn't empty
+        var lastLastUndo = ( lastUndo.length > 0 ) ? lastUndo[ lastUndo.length 
- 1 ] : null;
+        if ( lastLastUndo &&
+             ( typeof lastLastUndo == 'object' ) &&
+             lastLastUndo.length == 0 ) {
+                 lastUndo.pop();
+             }
+        setBusy( false );
+        var row = document.getElementById( 'dt-argtable-row-number-' + rown );
+        row.childNodes[ 3 ].firstChild.removeAttribute( 'disabled' );
+    }
+}
+
+
+/**
+ * ***************************
+ * Descending into a template.
+ * ***************************
+**/
+
+/**
+ * Delegated handler from exalText for entering into a called template.
+ *
+ * Assumes the busy flag is set.
+ *
+ * @param {HTMLElement} node The node to which the evalText handler was 
attached
+ * @return {}
+**/
+function descendInto( node ) {
+    var i;
+    var n = node.id;
+    var x = nTox[ n ];
+    var astNode = ast.getElementById( x );
+    // Cannot descend into a parameter
+    if ( astNode.tagName != 'template' ) {
+        setBusy( false );
+        return;
+    }
+
+    // For each parameter, including the title, we need to evaluate them, so 
build up a list
+    var args = [ ];
+    for ( i = 0; i < astNode.childNodes.length; i++ ) {
+        // Titles and unnamed arguments are simple and just get expanded.  
Named ( arg=value ) args are
+        //  trickier and give us two pieces that need to be evaluated 
separately.
+        if ( astNode.childNodes[ i ].tagName == 'part' &&
+             astNode.childNodes[ i ].childNodes.length == 3 ) {
+                 // Also trim them, as whitespace around the arg name and 
value is discarded
+                 args.push( textFromAST( astNode.childNodes[ i ].firstChild 
).trim(  ) );
+                 args.push( textFromAST( astNode.childNodes[ i ].childNodes[ 2 
] ).trim(  ) );
+             } else {
+                 args.push( textFromAST( astNode.childNodes[ i ] ) );
+             }
+    }
+
+    // Define a counter in the closure so we have a global way of detecting 
when all the evals are done
+    var count = 0;
+
+    // And initialize the counter to the number of non-empty-string entries
+    for ( i = 0; i < args.length; i++ ) {
+        if ( args[ i ] == '' ) {
+            count++;
+        }
+    }
+
+    // Now evaluate each arg that is not the empty string
+    for ( i = 0; i < args.length; i++ ) {
+        if ( args[ i ] != '' ) {
+            // Use apiEval, but wrap it in a function to preserve individual i 
values
+            window.setTimeout( function( i ) {
+                apiEval( args[ i ], function( k, t ) {
+                    if ( k == "OK" ) {
+                        var result = window.JSON.parse( t );
+                        if ( result.expandtemplates &&
+                             result.expandtemplates.wikitext !== undefined ) {
+                                 args[ i ] = result.expandtemplates.wikitext;
+                                 count++;
+                                 // Once count is at max, all evals are done 
and we can display them
+                                 if ( count == args.length ) {
+                                     descendDisplay( astNode, args );
+                                 }
+                        } else {
+                            debugNote( mw.message( 
'debugtemplates-error-arg-eval' ) + t );
+                            setBusy( false );
+                        }
+                    } else {
+                        debugNote( mw.message( 'debugtemplates-error-arg-eval' 
) + k );
+                        setBusy( false );
+                    }
+                } );
+            }, apiCallInterval * i, i );
+        }
+    }
+}
+
+/**
+ * Callback upon descent, once all args have been evaluated.
+ *
+ * @param {XMLElement} node The AST node representing this template
+ * @param {object} args The ordered array of textual arguments, including the 
title and with named
+ *  arguments represented by 2 entries
+**/
+function descendDisplay( node, args ) {
+    // First we assemble our list of arguments, using the original AST node as 
a guideline
+    var newparams = {};
+    // This counter tracks unnamed argument indices
+    var pindex = 1;
+    // Index into the args array
+    var argi = 1;
+    for ( var i = 1; i < node.childNodes.length; i++ ) {
+        if ( node.childNodes[ i ].tagName == 'part' && node.childNodes[ i 
].childNodes.length == 3 ) {
+            // Named argument, consume two entries in the args array
+            argi++;
+            newparams[ args[ argi - 1 ] ] = args[ argi ];
+        } else {
+            // Indexed argument, assign to the next index
+            newparams[ pindex ] = args[ argi ];
+            pindex++;
+        }
+        argi++;
+    }
+
+    // var s = args[ 0 ]+': ';
+    // for ( var p in newparams ) {
+    //     s+= '[ '+p+' ]="'+newparams[ p ]+'",  ';
+    // }
+    //debugNote( 'Extracted: '+s );
+
+    // Ok, now we need to find the real name of the template page.  Note that 
this can be different from
+    // the name used in invoking it, as a namespace can be assumed, and there 
might be a ':' in front, or
+    // it may not actually exist (such as for parserfunctions)
+    window.setTimeout( function() {
+        apiGetTemplateName( '{{' + args[ 0 ] + '}}', function( k, t ) {
+            if ( k == "OK" ) {
+                var result = window.JSON.parse( t );
+                if ( result.parse && result.parse.templates && 
result.parse.templates[ 0 ] ) {
+                    var tplate = result.parse.templates[ 0 ];
+                    var tname = tplate[ '*' ];
+                    if ( tplate[ "exists" ] !== undefined ) {
+                        // Page actually exists and we have its full name
+                        loadTemplate( args[ 0 ], tname, newparams );
+                    } else {
+                        debugNote( mw.message( 
'debugtemplates-warning-template-not-a-template' ) +
+                                   tname );
+                        setBusy( false );
+                    }
+                } else {
+                    debugNote( mw.message( 
'debugtemplates-warning-template-not-found' ) + args[ 0 ] );
+                    setBusy( false );
+                }
+            } else {
+                debugNote( mw.message( 'debugtemplates-error-template-name' ) 
+ k );
+                setBusy( false );
+            }
+        } );
+    }, apiCallInterval);
+}
+
+/**
+ * The penultimate step in descending into a template.  Here we assume we have 
resolved the template name
+ * and arguments, and now we can load the actual page as our new input text.
+ *
+ * @param {string} tinv The template named used in the invocation
+ * @param {string} tname The full page name of the template
+ * @param {object} newparams The expanded arguments it will be invoked with
+**/
+function loadTemplate( tinv, tname, newparams ) {
+    apiGetPage( tname, function( k, t ) {
+        if ( k == "OK" ) {
+            var result = window.JSON.parse( t );
+            if ( result.query && result.query.pages ) {
+                for ( var p in result.query.pages ) {
+                    var pg = result.query.pages[ p ];
+                    if ( pg.revisions && pg.revisions[ 0 ] ) {
+                        finishDescent( tinv, newparams, pg.revisions[ 0 ][ '*' 
] );
+                    } else  {
+                        debugNote( mw.message( 
'debugtemplates-error-template-page' ) + tname );
+                        setBusy( false );
+                    }
+                }
+            } else {
+                debugNote( mw.message( 'debugtemplates-error-template-page' ) 
+ t );
+                setBusy( false );
+            }
+        } else {
+            debugNote( mw.message( 'debugtemplates-error-template-page' ) + k 
);
+            setBusy( false );
+        }
+    } );
+}
+
+/**
+ * Callback forming the last step in descending into a template.  This pushes 
a new crumb, and installs
+ * the new text.
+ *
+ * @param {string} t Template name used in the invoke
+ * @param {object} newparams The expanded parameters mapping names to values
+ * @param {string} text The template text itself
+**/
+function finishDescent( t, newparams, text ) {
+    //debugNote( 'descending into '+text );
+    pushCrumb( t );
+
+    //debugNote( 'transcluding: '+text );
+    text = transcludeText( text );
+    //debugNote( 'transcluded: '+text );
+
+    document.getElementById( 'dt-input' ).value = text;
+    updateFromNewInput( text, newparams );
+    setBusy( false );
+}
+
+/**
+ * ***********************
+ * Breadcrumbs management.
+ * ***********************
+**/
+
+/**
+ * Wipe out the list of breadcrumbs and reset it to the initial crumb.
+**/
+function clearCrumbs() {
+    var bc = document.getElementById( 'dt-crumbs' );
+    while ( bc.firstChild )
+        bc.removeChild( bc.firstChild );
+    nestingStack = [];
+    // An initial crumb is required
+    pushCrumb( firstcrumb );
+}
+
+/**
+ * Pushes a new crumb onto the end of the stack, turning the previous one into 
a history link.
+ *
+ * @param {string} t The crumb name
+**/
+function pushCrumb( t ) {
+    var bc = document.getElementById( 'dt-crumbs' );
+    var span = document.createElement( 'span' );
+    if ( bc.lastChild ) {
+        bc.lastChild.classList.add( 'dt-crumb-visited' );
+        bc.lastChild.addEventListener( 'click', crumbHandler, false );
+        var h = makeStackFrame();
+        nestingStack.push( h );
+        bc.lastChild.setAttribute( 'hindex', nestingStack.length - 1 );
+        bc.appendChild( document.createTextNode( nextCrumb ) );
+    }
+
+    span.appendChild( document.createTextNode( t ) );
+    bc.appendChild( span );
+}
+
+/**
+ * Pop the crumb stack to restore to the given crumb.
+ *
+ * @param {HTMLElement} c The crumb we want to go to
+**/
+function popToCrumb( c ) {
+    if ( busy ) {
+        return;
+    }
+    var bc = document.getElementById( 'dt-crumbs' );
+    // Remove children until we've rewound the stack to c
+    var ci = c.getAttribute( 'hindex' );
+    var s = '';
+    while ( bc.lastChild ) {
+        if ( bc.lastChild.nodeType == 3 || !bc.lastChild.hasAttribute( 
'hindex' ) ) {
+            bc.removeChild( bc.lastChild );
+        } else if ( bc.lastChild.getAttribute( 'hindex' ) != ci ) {
+            bc.removeChild( bc.lastChild );
+            nestingStack.pop();
+        } else {
+            // Found our crumb
+            c.classList.remove( 'dt-crumb-visited' );
+            c.removeAttribute( 'hindex' );
+            c.removeEventListener( 'click', crumbHandler, false );
+            // Get our history
+            restoreStackFrame( nestingStack.pop() );
+            break;
+        }
+    }
+}
+
+/**
+ * Construct a history of the current state to store in a crumb.
+ *
+ * @return {object} An associative array storing the state
+**/
+function makeStackFrame() {
+    // Preserve:
+    //  - input pane ( simple string )
+    //  - output pane  ( html: will be replaced )
+    //  - abstract syntax tree ( xml: will be replaced )
+    //  - mapping betwween html and ast nodes ( json: deep-copy )
+    //  - max indices for both ast nodes and html nodes ( simple data types )
+    //  - param list ( whole table body ) ( html: will be replaced )
+    //  - undo history ( json: deep-copy )
+    var h = { input: null,
+              output: null,
+              ast: ast,
+              nTox: window.JSON.stringify( nTox ),
+              nindex: nindex,
+              xindex: xindex,
+              params: null,
+              undo: window.JSON.stringify( lastUndo ) };
+    h.input = document.getElementById( 'dt-input' ).value;
+    h.output = document.getElementById( 'dt-output' ).firstChild;
+    var argtable = document.getElementById( 'dt-argtable' );
+    if ( argtable.getElementsByTagName( 'tbody' ) ) {
+        h.params = argtable.getElementsByTagName( 'tbody' )[ 0 ];
+    }
+    return h;
+}
+
+/**
+ * Restore the state to the given stack frame.
+ *
+ * @param {object} h A history previously constructed by makeStackFrame
+**/
+function restoreStackFrame( h ) {
+    // Restore:
+    //  - input pane data
+    //  - output pane html
+    //  - abstract syntax tree
+    //  - mapping betwween html and ast nodes
+    //  - max indices for both ast nodes and html nodes
+    //  - param list ( whole table body )
+    //  - undo history
+    ast = h.ast;
+    var k = h.nTox;
+    nTox = window.JSON.parse( k );
+    nindex = h.nindex;
+    xindex = h.xindex;
+    lastUndo = window.JSON.parse( h.undo );
+    document.getElementById( 'dt-input' ).value = h.input;
+    var dout = document.getElementById( 'dt-output' );
+    if ( !h.output ) {
+        while ( dout.lastChild ) {
+            dout.removeChild( dout.lastChild );
+        }
+    } else {
+        if ( dout.firstChild ) {
+            dout.replaceChild( h.output, dout.firstChild );
+        } else {
+            dout.appendChild( h.output );
+        }
+    }
+    var argtable = document.getElementById( 'dt-argtable' );
+    if ( !h.params ) {
+        h.params = document.createElement( 'tbody' );
+    }
+    var prev = argtable.getElementsByTagName( 'tbody' )[ 0 ];
+    if ( prev ) {
+        argtable.replaceChild( h.params, prev );
+    } else {
+        argtable.appendChild( h.params );
+    }
+    // Finally,  fix any lingering emphasis tags
+    var emps = [ 'dt-emphasize-template-out',
+                 'dt-emphasize-template-in',
+                 'dt-emphasize-template-name',
+                 'dt-emphasize-template-args',
+                 'dt-emphasize-template-pipe',
+                 'dt-emphasize-tplarg-out',
+                 'dt-emphasize-tplarg-in',
+                 'dt-emphasize-tplarg-name',
+                 'dt-emphasize-tplarg-args',
+                 'dt-emphasize-tplarg-pipe',
+                 'dt-node-emphasize-evaluated' ];
+    for ( var i = 0; i <emps.length; i++ ) {
+        var e = document.getElementsByClassName( emps[ i ] );
+        if ( e ) {
+            for ( var j = 0; j < e.length; j++ ) {
+                e[ j ].classList.remove( emps[ i ] );
+            }
+        }
+    }
+}
+
+/**
+ * Handler for when a crumb is clicked on.
+**/
+function crumbHandler() {
+    popToCrumb( this );
+}
+
+/**
+ * ****************
+ * Basic listeners.
+ * ****************
+**/
+
+/**
+ * Handler for toggling a parameter's set/unset state.
+**/
+function paramSetHandler() {
+    paramSetToggle( this.parentNode.parentNode );
+}
+
+/**
+ * Toggles or changes the set/unset state of a parameter.
+ *
+ * @param {HTMLElement} row Row DOM element of the parameter
+ * @param {string|null} tostate Either 'on', 'off' to set it to a specific 
value, or null to toggle
+**/
+function paramSetToggle( row, tostate ) {
+    var cset = row.cells[ 0 ].firstChild;
+    var cval = row.cells[ 2 ].firstChild;
+    if ( !tostate ) {
+        // Detect the state to decide how to change it
+        if ( cval.classList.contains( 'dt-arg-set-yes' ) ) {
+            tostate = "off";
+        } else {
+            tostate = "on";
+        }
+    }
+    if ( cval.classList.contains( 'dt-arg-set-yes' ) && tostate == 'off' ) {
+        cset.replaceChild( document.createTextNode( argn ), cset.firstChild );
+        cval.classList.remove( 'dt-arg-set-yes' );
+        cval.classList.add( 'dt-arg-set-no' );
+    } else if ( cval.classList.contains( 'dt-arg-set-no' ) && tostate == 'on' 
) {
+        cset.replaceChild( document.createTextNode( argy ), cset.firstChild );
+        cval.classList.add( 'dt-arg-set-yes' );
+        cval.classList.remove( 'dt-arg-set-no' );
+    }
+}
+
+/**
+ * Handler for if the API URL is changed.
+ *
+ * Technically this is a readonly field, so changing it should not be 
possible.  This is here for future
+ * flexibility.
+**/
+function apiHandler() {
+    updateFromNewInput( document.getElementById( 'dt-input' ).value );
+}
+
+/**
+ * Handler for when the page context title is changed.
+**/
+function titleHandler() {
+    updateFromNewInput( document.getElementById( 'dt-input' ).value );
+}
+
+/**
+ * Handler for when the input text is changed.
+**/
+function debugTextHandler() {
+    updateFromNewInput( this.value );
+}
+
+/**
+ * Handler to keep the parameter table the same height as the input text box.
+**/
+function resizeArgTable() {
+    document.getElementById( 'dt-argtable-wrapper' ).style.height =
+        document.getElementById( 'dt-input' ).clientHeight + 'px';
+}
+
+/**
+ * Handler for the undo-all button
+**/
+function resetButton() {
+    var i;
+    if ( busy ) {
+        return;
+    }
+    this.setAttribute( 'disabled', 'disabled' );
+    undoButtonNode.setAttribute( 'disabled', 'disabled' );
+    var evaluated = document.getElementsByClassName( 'dt-node-evaluated' );
+    if ( evaluated ) {
+        for ( i = evaluated.length - 1; i >= 0; i-- ) {
+            var n = evaluated[ i ].parentNode;
+            n.removeChild( evaluated[ i ] );
+        }
+    }
+    evaluated = document.getElementsByClassName( 'dt-node-invisible' );
+    if ( evaluated ) {
+        for ( i = evaluated.length - 1; i >= 0; i-- ) {
+            evaluated[ i ].classList.toggle( 'dt-node-invisible' );
+        }
+    }
+    lastUndo = [  ];
+}
+
+/**
+ * Handler for the undo button.
+**/
+function undoButton() {
+    // A sub-function to undo a single event
+    function undoSomething( undo ) {
+        var node = document.getElementById( undo );
+        if ( node.lastChild.classList.contains( 'dt-node-evaluated' ) ) {
+            node.removeChild( node.lastChild );
+            for ( var  i = 0; i < node.childNodes.length; i++ ) {
+                node.childNodes[ i ].classList.remove( 'dt-node-invisible' );
+            }
+        }
+    }
+    if ( busy ) {
+        return;
+    }
+    if ( lastUndo.length == 1 ) {
+        this.setAttribute( 'disabled', 'disabled' );
+        resetButtonNode.setAttribute( 'disabled', 'disabled' );
+    }
+    var toUndo = lastUndo.pop();
+    // Undo entries may be a single string,  or an array
+    if ( typeof toUndo == 'string' || typeof toUndo == 'number' ) {
+        undoSomething( toUndo );
+    } else {
+        for ( var j = 0; j < toUndo.length; j++ ) {
+            undoSomething( toUndo[ j ] );
+        }
+    }
+}
+
+/**
+ * Handler for the evaluate all button
+**/
+// handler for the evaluate all button
+function evalAllButton() {
+    if ( busy ) {
+        return;
+    }
+    setBusy( true );
+    if ( ast && ast.documentElement ) {
+        var txt = textFromAST( ast.documentElement );
+        // The root wrapper is always id 0
+        var n = 'dt-id-0';
+        apiEval( txt, function( k, t ) {
+            if ( k == "OK" ) {
+                var result = window.JSON.parse( t );
+                if ( result.expandtemplates && result.expandtemplates.wikitext 
!== undefined ) {
+                    evalTextDisplay( result.expandtemplates.wikitext, n );
+                    setBusy ( false );
+                } else {
+                    debugNote( mw.message( 'debugtemplates-error-eval' ) + t );
+                    setBusy ( false );
+                }
+            } else {
+                debugNote( mw.message( 'debugtemplates-error-eval' ) + k );
+                setBusy ( false );
+            }
+        } );
+    } else {
+        setBusy ( false );
+    }
+}
+
+/**
+ * Handler for the set/unset all button.
+**/
+function toggleSetButton() {
+    var i;
+    var args = document.getElementById( 'dt-argtable' );
+    var abody = args.getElementsByTagName( 'tbody' );
+    if ( abody && abody[ 0 ] ) {
+        abody = abody[ 0 ];
+        // First figure out if we're toggling them all on or off.  We assume 
on, unless they're all on
+        //  already, in which case it is off.
+        var setgoal = "off";
+        for ( i = 0; i < abody.rows.length; i++ ) {
+            if ( abody.rows[ i ].cells[ 2 ].firstChild.classList.contains( 
'dt-arg-set-no' ) ) {
+                setgoal = "on";
+                break;
+            }
+        }
+        for ( i = 0; i < abody.rows.length; i++ ) {
+            paramSetToggle( abody.rows[ i ], setgoal );
+        }
+    }
+}
+
+/**
+ * Handler for the clear all button.
+**/
+function clearAllButton() {
+    var args = document.getElementById( 'dt-argtable' );
+    var abody = args.getElementsByTagName( 'tbody' );
+    if ( abody && abody[ 0 ] ) {
+        abody = abody[ 0 ];
+        for ( var i = 0; i < abody.rows.length; i++ ) {
+            abody.rows[ i ].cells[ 2 ].firstChild.value = '';
+        }
+    }
+}
+
+/**
+ * Handler for the click mode radio buttons.
+**/
+function clickMode() {
+    var dout = document.getElementById( 'dt-output' );
+    if( getMouseClickMode() ==  'descend' ) {
+        if ( !dout.classList.contains( 'dt-descend' ) ) {
+            dout.classList.add( 'dt-descend' );
+        }
+    } else {
+        if ( dout.classList.contains( 'dt-descend' ) ) {
+            dout.classList.remove( 'dt-descend' );
+        }
+    }
+}
+
+/**
+ * Initialization.
+ *
+ * Setup the main handlers, clear the stack, kick off the initial parse.
+**/
+function init() {
+    var dtin = document.getElementById( 'dt-input' );
+    dtin.addEventListener( 'change', debugTextHandler, false );
+    document.getElementById( 'dt-api' ).addEventListener( 'change', 
apiHandler, false );
+    document.getElementById( 'dt-title' ).addEventListener( 'change', 
titleHandler, false );
+
+    undoButtonNode = document.getElementById( 'dt-undo' );
+    resetButtonNode = document.getElementById( 'dt-reset' );
+    resetButtonNode.addEventListener( 'click', resetButton, false );
+    undoButtonNode.addEventListener( 'click', undoButton, false );
+
+    document.getElementById( 'dt-eval' ).addEventListener( 'click', 
evalAllButton, false );
+    document.getElementById( 'dt-args-set-toggle' ).addEventListener( 'click', 
toggleSetButton, false );
+    document.getElementById( 'dt-args-value-clear' ).addEventListener( 
'click', clearAllButton, false );
+
+    document.getElementById( 'dt-radio-select' ).addEventListener( 'click', 
clickMode, false );
+    document.getElementById( 'dt-radio-eval' ).addEventListener( 'click', 
clickMode, false );
+    document.getElementById( 'dt-radio-descend' ).addEventListener( 'click', 
clickMode, false );
+
+    clearCrumbs();
+    setOutput();
+
+    resizeArgTable();
+    // Use mouseup rather than resize to capture the textarea resizing, as the 
user-resizable textarea
+    //  elements do not send resize events
+    document.getElementById( 'dt-input' ).addEventListener( 'mouseup', 
resizeArgTable, false );
+
+    // Set a minimum output height so it does not jump around quite so much
+    var mh = window.getComputedStyle( document.getElementById( 'dt-input' ) 
).lineHeight;
+    document.getElementById( 'dt-output' ).style.minHeight = mh;
+
+    // Make the cursor correct for whatever mode we're in
+    clickMode();
+    // Finally, parse the input
+    updateFromNewInput( dtin.value );
+}
+
+debugNoteClear();
+init();
diff --git a/i18n/en.json b/i18n/en.json
new file mode 100644
index 0000000..648f344
--- /dev/null
+++ b/i18n/en.json
@@ -0,0 +1,44 @@
+{
+    "@metadata": {
+        "authors": [
+            "Clark Verbrugge"
+        ]
+    },
+    "debugtemplates": "Debug Templates",
+    "debugtemplates-form": "Input Area",
+    "debugtemplates-desc": "A simple debugger for templates",
+    "debugtemplates-summary": "Adds a special page which provides an interface 
for debugging template expansions.",
+    "debugtemplates-api": "URL for making API calls (informational only)",
+    "debugtemplates-title": "Page title (optional, for context only)",
+    "debugtemplates-args-title": "Parameters",
+    "debugtemplates-args-name": "Name",
+    "debugtemplates-args-eval": "Eval All",
+    "debugtemplates-args-set": "Set?",
+    "debugtemplates-args-value": "Value",
+    "debugtemplates-args-eval-all": "Eval",
+    "debugtemplates-args-set-toggle": "Set/Unset All",
+    "debugtemplates-args-unused": "Hide/Show Unused",
+    "debugtemplates-args-value-clear": "Clear All",
+    "debugtemplates-args-constructed": "noname",
+    "debugtemplates-crumb-title": "Stack of templates you have descended into. 
You can click on an entry to return to that state.",
+    "debugtemplates-crumb-reset": "Clear the stack.",
+    "debugtemplates-error-arg-eval": "Error, could not evaluate argument to 
template: ",
+    "debugtemplates-error-button": "Clear Msgs",
+    "debugtemplates-error-eval": "Error, could not evaluate input text: ",
+    "debugtemplates-error-parse": "Error, could not parse input text: ",
+    "debugtemplates-error-template-name": "Error, could not determine template 
name: ",
+    "debugtemplates-error-template-page": "Error, could not load template 
page: ",
+    "debugtemplates-error-template-revisions": "Error, no page revisions exist 
for template: ",
+    "debugtemplates-eval": "Eval All",
+    "debugtemplates-input": "Input text",
+    "debugtemplates-intro": "This page gives you an interface for debugging 
the template expansion of some text.  Note that this is still a work in 
progress!!  But try it out by typing \"<nowiki>{{foo|abc}}</nowiki>\" into the 
input textarea below.",
+    "debugtemplates-radio-intro": "Clicking on template or parameter starts 
will do",
+    "debugtemplates-radio-select": "nothing, or",
+    "debugtemplates-radio-eval": "evaluate, or",
+    "debugtemplates-radio-descend": "descend (if a template).",
+    "debugtemplates-reset": "Undo All Evals",
+    "debugtemplates-undo": "Undo Eval",
+    "debugtemplates-warning-template-not-a-template": "Cannot find template: ",
+    "debugtemplates-warning-template-not-found": "Unknown or built-in 
template: ",
+    "debugtemplates-output": "Debugging Pane"
+}
diff --git a/i18n/qqq.json b/i18n/qqq.json
new file mode 100644
index 0000000..914453d
--- /dev/null
+++ b/i18n/qqq.json
@@ -0,0 +1,44 @@
+{
+    "@metadata": {
+        "authors": [
+            "Clark Verbrugge"
+        ]
+    },
+    "debugtemplates": "The name of the extension's entry in 
Special:SpecialPages.",
+    "debugtemplates-desc": "{{desc}}",
+    "debugtemplates-summary": "Description appearing on top of 
[[Special:DebugTemplates]].",
+    "debugtemplates-form": "A title label for the input area pane.",
+    "debugtemplates-api": "Text label asking for the Mediawiki API URL.",
+    "debugtemplates-title": "Text label asking for the page context title.",
+    "debugtemplates-args-title": "Title of parameter list.",
+    "debugtemplates-args-name": "Column title in parameter list for argument 
names.",
+    "debugtemplates-args-eval": "Column title in parameter list for the 
buttons that evaluate all instances of that parameter.",
+    "debugtemplates-args-set": "Column title in parameter list for whether an 
input argument is set or not.",
+    "debugtemplates-args-value": "Column title in parameter list for argument 
values.",
+    "debugtemplates-args-eval-all": "Button label for evaluating all 
parameters.",
+    "debugtemplates-args-set-toggle": "Button label for set/unset all 
parameters.",
+    "debugtemplates-args-unused": "Button label for hiding or showing all 
unused parameters.",
+    "debugtemplates-args-value-clear": "Button text for clearing all parameter 
values.",
+    "debugtemplates-args-constructed": "String used for parameter names that 
cannot be determined.",
+    "debugtemplates-crumb-title": "Mouse tooltip for the call stack display 
explaining what it is.",
+    "debugtemplates-crumb-reset": "Mouse tooltip for the button in the call 
stack display that clears the stack.",
+    "debugtemplates-error-arg-eval": "Error message when a template argument 
could not be evaluated.",
+    "debugtemplates-error-button": "Button label to clear error message log.",
+    "debugtemplates-error-eval": "Error message when the input text could not 
be evaluated.",
+    "debugtemplates-error-parse": "Error message when api call to parse the 
input failed for some reason.",
+    "debugtemplates-error-template-name": "Error message that the template 
name could not be determined.",
+    "debugtemplates-error-template-page": "Error message that the template 
page could not be loaded.",
+    "debugtemplates-error-template-revisions": "Error message that no content 
revisions were found for the template name.",
+    "debugtemplates-eval": "Button label for the evaluate all button.",
+    "debugtemplates-input": "Text label asking for the input text to debug.",
+    "debugtemplates-intro": "Introductory text explaining what this page 
does.",
+    "debugtemplates-radio-intro": "Sentence prefix to introduce mouse click 
modes.",
+    "debugtemplates-radio-select": "Continuation of radio-intro sentence, 
giving the radio button label to indicate that a mouse click will do nothing.",
+    "debugtemplates-radio-eval": "Continuation of radio-intro sentence, giving 
the tadio button label to indicate that a mouse click will evaluate a 
template.",
+    "debugtemplates-radio-descend": "Continuation of radio-intro sentence, 
giving the radio button label to indicate that a mouse click will descend into 
a template.",
+    "debugtemplates-reset": "Button label for the undo all evaluations 
button.",
+    "debugtemplates-undo": "Button label for the undo button.",
+    "debugtemplates-warning-template-not-a-template": "Warning message that a 
template invocation is to a non-template.",
+    "debugtemplates-warning-template-not-found": "Warning message that the 
template was not found as a wiki page.",
+    "debugtemplates-output": "Heading for the debug output pane display."
+}

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ia912882ee14ffc32e798543a7dacbcfd2804375e
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/DebugTemplates
Gerrit-Branch: master
Gerrit-Owner: Clump <[email protected]>

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

Reply via email to