Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | // -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- |
michael@0 | 2 | |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | Components.utils.import("resource://gre/modules/Services.jsm"); |
michael@0 | 8 | |
michael@0 | 9 | var gDebug = 0; |
michael@0 | 10 | var gLineCount = 0; |
michael@0 | 11 | var gStartTargetLine = 0; |
michael@0 | 12 | var gEndTargetLine = 0; |
michael@0 | 13 | var gTargetNode = null; |
michael@0 | 14 | |
michael@0 | 15 | var gEntityConverter = null; |
michael@0 | 16 | var gWrapLongLines = false; |
michael@0 | 17 | const gViewSourceCSS = 'resource://gre-resources/viewsource.css'; |
michael@0 | 18 | const NS_XHTML = 'http://www.w3.org/1999/xhtml'; |
michael@0 | 19 | |
michael@0 | 20 | // These are markers used to delimit the selection during processing. They |
michael@0 | 21 | // are removed from the final rendering, but we pick space-like characters for |
michael@0 | 22 | // safety (and futhermore, these are known to be mapped to a 0-length string |
michael@0 | 23 | // in transliterate.properties). It is okay to set start=end, we use findNext() |
michael@0 | 24 | // U+200B ZERO WIDTH SPACE |
michael@0 | 25 | const MARK_SELECTION_START = '\u200B\u200B\u200B\u200B\u200B'; |
michael@0 | 26 | const MARK_SELECTION_END = '\u200B\u200B\u200B\u200B\u200B'; |
michael@0 | 27 | |
michael@0 | 28 | function onLoadViewPartialSource() |
michael@0 | 29 | { |
michael@0 | 30 | // check the view_source.wrap_long_lines pref |
michael@0 | 31 | // and set the menuitem's checked attribute accordingly |
michael@0 | 32 | gWrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines"); |
michael@0 | 33 | document.getElementById("menu_wrapLongLines").setAttribute("checked", gWrapLongLines); |
michael@0 | 34 | document.getElementById("menu_highlightSyntax") |
michael@0 | 35 | .setAttribute("checked", |
michael@0 | 36 | Services.prefs.getBoolPref("view_source.syntax_highlight")); |
michael@0 | 37 | |
michael@0 | 38 | if (window.arguments[3] == 'selection') |
michael@0 | 39 | viewPartialSourceForSelection(window.arguments[2]); |
michael@0 | 40 | else |
michael@0 | 41 | viewPartialSourceForFragment(window.arguments[2], window.arguments[3]); |
michael@0 | 42 | |
michael@0 | 43 | gBrowser.droppedLinkHandler = function (event, url, name) { |
michael@0 | 44 | viewSource(url) |
michael@0 | 45 | event.preventDefault(); |
michael@0 | 46 | } |
michael@0 | 47 | |
michael@0 | 48 | window.content.focus(); |
michael@0 | 49 | } |
michael@0 | 50 | |
michael@0 | 51 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 52 | // view-source of a selection with the special effect of remapping the selection |
michael@0 | 53 | // to the underlying view-source output |
michael@0 | 54 | function viewPartialSourceForSelection(selection) |
michael@0 | 55 | { |
michael@0 | 56 | var range = selection.getRangeAt(0); |
michael@0 | 57 | var ancestorContainer = range.commonAncestorContainer; |
michael@0 | 58 | var doc = ancestorContainer.ownerDocument; |
michael@0 | 59 | |
michael@0 | 60 | var startContainer = range.startContainer; |
michael@0 | 61 | var endContainer = range.endContainer; |
michael@0 | 62 | var startOffset = range.startOffset; |
michael@0 | 63 | var endOffset = range.endOffset; |
michael@0 | 64 | |
michael@0 | 65 | // let the ancestor be an element |
michael@0 | 66 | if (ancestorContainer.nodeType == Node.TEXT_NODE || |
michael@0 | 67 | ancestorContainer.nodeType == Node.CDATA_SECTION_NODE) |
michael@0 | 68 | ancestorContainer = ancestorContainer.parentNode; |
michael@0 | 69 | |
michael@0 | 70 | // for selectAll, let's use the entire document, including <html>...</html> |
michael@0 | 71 | // @see nsDocumentViewer::SelectAll() for how selectAll is implemented |
michael@0 | 72 | try { |
michael@0 | 73 | if (ancestorContainer == doc.body) |
michael@0 | 74 | ancestorContainer = doc.documentElement; |
michael@0 | 75 | } catch (e) { } |
michael@0 | 76 | |
michael@0 | 77 | // each path is a "child sequence" (a.k.a. "tumbler") that |
michael@0 | 78 | // descends from the ancestor down to the boundary point |
michael@0 | 79 | var startPath = getPath(ancestorContainer, startContainer); |
michael@0 | 80 | var endPath = getPath(ancestorContainer, endContainer); |
michael@0 | 81 | |
michael@0 | 82 | // clone the fragment of interest and reset everything to be relative to it |
michael@0 | 83 | // note: it is with the clone that we operate/munge from now on. Also note |
michael@0 | 84 | // that we clone into a data document to prevent images in the fragment from |
michael@0 | 85 | // loading and the like. The use of importNode here, as opposed to adoptNode, |
michael@0 | 86 | // is _very_ important. |
michael@0 | 87 | // XXXbz wish there were a less hacky way to create an untrusted document here |
michael@0 | 88 | var isHTML = (doc.createElement("div").tagName == "DIV"); |
michael@0 | 89 | var dataDoc = isHTML ? |
michael@0 | 90 | ancestorContainer.ownerDocument.implementation.createHTMLDocument("") : |
michael@0 | 91 | ancestorContainer.ownerDocument.implementation.createDocument("", "", null); |
michael@0 | 92 | ancestorContainer = dataDoc.importNode(ancestorContainer, true); |
michael@0 | 93 | startContainer = ancestorContainer; |
michael@0 | 94 | endContainer = ancestorContainer; |
michael@0 | 95 | |
michael@0 | 96 | // Only bother with the selection if it can be remapped. Don't mess with |
michael@0 | 97 | // leaf elements (such as <isindex>) that secretly use anynomous content |
michael@0 | 98 | // for their display appearance. |
michael@0 | 99 | var canDrawSelection = ancestorContainer.hasChildNodes(); |
michael@0 | 100 | if (canDrawSelection) { |
michael@0 | 101 | var i; |
michael@0 | 102 | for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) { |
michael@0 | 103 | startContainer = startContainer.childNodes.item(startPath[i]); |
michael@0 | 104 | } |
michael@0 | 105 | for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) { |
michael@0 | 106 | endContainer = endContainer.childNodes.item(endPath[i]); |
michael@0 | 107 | } |
michael@0 | 108 | |
michael@0 | 109 | // add special markers to record the extent of the selection |
michael@0 | 110 | // note: |startOffset| and |endOffset| are interpreted either as |
michael@0 | 111 | // offsets in the text data or as child indices (see the Range spec) |
michael@0 | 112 | // (here, munging the end point first to keep the start point safe...) |
michael@0 | 113 | var tmpNode; |
michael@0 | 114 | if (endContainer.nodeType == Node.TEXT_NODE || |
michael@0 | 115 | endContainer.nodeType == Node.CDATA_SECTION_NODE) { |
michael@0 | 116 | // do some extra tweaks to try to avoid the view-source output to look like |
michael@0 | 117 | // ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection). |
michael@0 | 118 | // To get a neat output, the idea here is to remap the end point from: |
michael@0 | 119 | // 1. ...<tag>]... to ...]<tag>... |
michael@0 | 120 | // 2. ...]</tag>... to ...</tag>]... |
michael@0 | 121 | if ((endOffset > 0 && endOffset < endContainer.data.length) || |
michael@0 | 122 | !endContainer.parentNode || !endContainer.parentNode.parentNode) |
michael@0 | 123 | endContainer.insertData(endOffset, MARK_SELECTION_END); |
michael@0 | 124 | else { |
michael@0 | 125 | tmpNode = dataDoc.createTextNode(MARK_SELECTION_END); |
michael@0 | 126 | endContainer = endContainer.parentNode; |
michael@0 | 127 | if (endOffset == 0) |
michael@0 | 128 | endContainer.parentNode.insertBefore(tmpNode, endContainer); |
michael@0 | 129 | else |
michael@0 | 130 | endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling); |
michael@0 | 131 | } |
michael@0 | 132 | } |
michael@0 | 133 | else { |
michael@0 | 134 | tmpNode = dataDoc.createTextNode(MARK_SELECTION_END); |
michael@0 | 135 | endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset)); |
michael@0 | 136 | } |
michael@0 | 137 | |
michael@0 | 138 | if (startContainer.nodeType == Node.TEXT_NODE || |
michael@0 | 139 | startContainer.nodeType == Node.CDATA_SECTION_NODE) { |
michael@0 | 140 | // do some extra tweaks to try to avoid the view-source output to look like |
michael@0 | 141 | // ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection). |
michael@0 | 142 | // To get a neat output, the idea here is to remap the start point from: |
michael@0 | 143 | // 1. ...<tag>[... to ...[<tag>... |
michael@0 | 144 | // 2. ...[</tag>... to ...</tag>[... |
michael@0 | 145 | if ((startOffset > 0 && startOffset < startContainer.data.length) || |
michael@0 | 146 | !startContainer.parentNode || !startContainer.parentNode.parentNode || |
michael@0 | 147 | startContainer != startContainer.parentNode.lastChild) |
michael@0 | 148 | startContainer.insertData(startOffset, MARK_SELECTION_START); |
michael@0 | 149 | else { |
michael@0 | 150 | tmpNode = dataDoc.createTextNode(MARK_SELECTION_START); |
michael@0 | 151 | startContainer = startContainer.parentNode; |
michael@0 | 152 | if (startOffset == 0) |
michael@0 | 153 | startContainer.parentNode.insertBefore(tmpNode, startContainer); |
michael@0 | 154 | else |
michael@0 | 155 | startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling); |
michael@0 | 156 | } |
michael@0 | 157 | } |
michael@0 | 158 | else { |
michael@0 | 159 | tmpNode = dataDoc.createTextNode(MARK_SELECTION_START); |
michael@0 | 160 | startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset)); |
michael@0 | 161 | } |
michael@0 | 162 | } |
michael@0 | 163 | |
michael@0 | 164 | // now extract and display the syntax highlighted source |
michael@0 | 165 | tmpNode = dataDoc.createElementNS(NS_XHTML, 'div'); |
michael@0 | 166 | tmpNode.appendChild(ancestorContainer); |
michael@0 | 167 | |
michael@0 | 168 | // the load is aynchronous and so we will wait until the view-source DOM is done |
michael@0 | 169 | // before drawing the selection. |
michael@0 | 170 | if (canDrawSelection) { |
michael@0 | 171 | window.document.getElementById("content").addEventListener("load", drawSelection, true); |
michael@0 | 172 | } |
michael@0 | 173 | |
michael@0 | 174 | // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl) |
michael@0 | 175 | var loadFlags = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE; |
michael@0 | 176 | getWebNavigation().loadURIWithBase((isHTML ? |
michael@0 | 177 | "view-source:data:text/html;charset=utf-8," : |
michael@0 | 178 | "view-source:data:application/xml;charset=utf-8,") |
michael@0 | 179 | + encodeURIComponent(tmpNode.innerHTML), |
michael@0 | 180 | loadFlags, null, null, null, |
michael@0 | 181 | Services.io.newURI(doc.baseURI, null, null)); |
michael@0 | 182 | } |
michael@0 | 183 | |
michael@0 | 184 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 185 | // helper to get a path like FIXptr, but with an array instead of the "tumbler" notation |
michael@0 | 186 | // see FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm |
michael@0 | 187 | function getPath(ancestor, node) |
michael@0 | 188 | { |
michael@0 | 189 | var n = node; |
michael@0 | 190 | var p = n.parentNode; |
michael@0 | 191 | if (n == ancestor || !p) |
michael@0 | 192 | return null; |
michael@0 | 193 | var path = new Array(); |
michael@0 | 194 | if (!path) |
michael@0 | 195 | return null; |
michael@0 | 196 | do { |
michael@0 | 197 | for (var i = 0; i < p.childNodes.length; i++) { |
michael@0 | 198 | if (p.childNodes.item(i) == n) { |
michael@0 | 199 | path.push(i); |
michael@0 | 200 | break; |
michael@0 | 201 | } |
michael@0 | 202 | } |
michael@0 | 203 | n = p; |
michael@0 | 204 | p = n.parentNode; |
michael@0 | 205 | } while (n != ancestor && p); |
michael@0 | 206 | return path; |
michael@0 | 207 | } |
michael@0 | 208 | |
michael@0 | 209 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 210 | // using special markers left in the serialized source, this helper makes the |
michael@0 | 211 | // underlying markup of the selected fragment to automatically appear as selected |
michael@0 | 212 | // on the inflated view-source DOM |
michael@0 | 213 | function drawSelection() |
michael@0 | 214 | { |
michael@0 | 215 | gBrowser.contentDocument.title = |
michael@0 | 216 | gViewSourceBundle.getString("viewSelectionSourceTitle"); |
michael@0 | 217 | |
michael@0 | 218 | // find the special selection markers that we added earlier, and |
michael@0 | 219 | // draw the selection between the two... |
michael@0 | 220 | var findService = null; |
michael@0 | 221 | try { |
michael@0 | 222 | // get the find service which stores the global find state |
michael@0 | 223 | findService = Components.classes["@mozilla.org/find/find_service;1"] |
michael@0 | 224 | .getService(Components.interfaces.nsIFindService); |
michael@0 | 225 | } catch(e) { } |
michael@0 | 226 | if (!findService) |
michael@0 | 227 | return; |
michael@0 | 228 | |
michael@0 | 229 | // cache the current global find state |
michael@0 | 230 | var matchCase = findService.matchCase; |
michael@0 | 231 | var entireWord = findService.entireWord; |
michael@0 | 232 | var wrapFind = findService.wrapFind; |
michael@0 | 233 | var findBackwards = findService.findBackwards; |
michael@0 | 234 | var searchString = findService.searchString; |
michael@0 | 235 | var replaceString = findService.replaceString; |
michael@0 | 236 | |
michael@0 | 237 | // setup our find instance |
michael@0 | 238 | var findInst = gBrowser.webBrowserFind; |
michael@0 | 239 | findInst.matchCase = true; |
michael@0 | 240 | findInst.entireWord = false; |
michael@0 | 241 | findInst.wrapFind = true; |
michael@0 | 242 | findInst.findBackwards = false; |
michael@0 | 243 | |
michael@0 | 244 | // ...lookup the start mark |
michael@0 | 245 | findInst.searchString = MARK_SELECTION_START; |
michael@0 | 246 | var startLength = MARK_SELECTION_START.length; |
michael@0 | 247 | findInst.findNext(); |
michael@0 | 248 | |
michael@0 | 249 | var selection = content.getSelection(); |
michael@0 | 250 | if (!selection.rangeCount) |
michael@0 | 251 | return; |
michael@0 | 252 | |
michael@0 | 253 | var range = selection.getRangeAt(0); |
michael@0 | 254 | |
michael@0 | 255 | var startContainer = range.startContainer; |
michael@0 | 256 | var startOffset = range.startOffset; |
michael@0 | 257 | |
michael@0 | 258 | // ...lookup the end mark |
michael@0 | 259 | findInst.searchString = MARK_SELECTION_END; |
michael@0 | 260 | var endLength = MARK_SELECTION_END.length; |
michael@0 | 261 | findInst.findNext(); |
michael@0 | 262 | |
michael@0 | 263 | var endContainer = selection.anchorNode; |
michael@0 | 264 | var endOffset = selection.anchorOffset; |
michael@0 | 265 | |
michael@0 | 266 | // reset the selection that find has left |
michael@0 | 267 | selection.removeAllRanges(); |
michael@0 | 268 | |
michael@0 | 269 | // delete the special markers now... |
michael@0 | 270 | endContainer.deleteData(endOffset, endLength); |
michael@0 | 271 | startContainer.deleteData(startOffset, startLength); |
michael@0 | 272 | if (startContainer == endContainer) |
michael@0 | 273 | endOffset -= startLength; // has shrunk if on same text node... |
michael@0 | 274 | range.setEnd(endContainer, endOffset); |
michael@0 | 275 | |
michael@0 | 276 | // show the selection and scroll it into view |
michael@0 | 277 | selection.addRange(range); |
michael@0 | 278 | // the default behavior of the selection is to scroll at the end of |
michael@0 | 279 | // the selection, whereas in this situation, it is more user-friendly |
michael@0 | 280 | // to scroll at the beginning. So we override the default behavior here |
michael@0 | 281 | try { |
michael@0 | 282 | getSelectionController().scrollSelectionIntoView( |
michael@0 | 283 | Ci.nsISelectionController.SELECTION_NORMAL, |
michael@0 | 284 | Ci.nsISelectionController.SELECTION_ANCHOR_REGION, |
michael@0 | 285 | true); |
michael@0 | 286 | } |
michael@0 | 287 | catch(e) { } |
michael@0 | 288 | |
michael@0 | 289 | // restore the current find state |
michael@0 | 290 | findService.matchCase = matchCase; |
michael@0 | 291 | findService.entireWord = entireWord; |
michael@0 | 292 | findService.wrapFind = wrapFind; |
michael@0 | 293 | findService.findBackwards = findBackwards; |
michael@0 | 294 | findService.searchString = searchString; |
michael@0 | 295 | findService.replaceString = replaceString; |
michael@0 | 296 | |
michael@0 | 297 | findInst.matchCase = matchCase; |
michael@0 | 298 | findInst.entireWord = entireWord; |
michael@0 | 299 | findInst.wrapFind = wrapFind; |
michael@0 | 300 | findInst.findBackwards = findBackwards; |
michael@0 | 301 | findInst.searchString = searchString; |
michael@0 | 302 | } |
michael@0 | 303 | |
michael@0 | 304 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 305 | // special handler for markups such as MathML where reformatting the output is |
michael@0 | 306 | // helpful |
michael@0 | 307 | function viewPartialSourceForFragment(node, context) |
michael@0 | 308 | { |
michael@0 | 309 | gTargetNode = node; |
michael@0 | 310 | if (gTargetNode && gTargetNode.nodeType == Node.TEXT_NODE) |
michael@0 | 311 | gTargetNode = gTargetNode.parentNode; |
michael@0 | 312 | |
michael@0 | 313 | // walk up the tree to the top-level element (e.g., <math>, <svg>) |
michael@0 | 314 | var topTag; |
michael@0 | 315 | if (context == 'mathml') |
michael@0 | 316 | topTag = 'math'; |
michael@0 | 317 | else |
michael@0 | 318 | throw 'not reached'; |
michael@0 | 319 | var topNode = gTargetNode; |
michael@0 | 320 | while (topNode && topNode.localName != topTag) |
michael@0 | 321 | topNode = topNode.parentNode; |
michael@0 | 322 | if (!topNode) |
michael@0 | 323 | return; |
michael@0 | 324 | |
michael@0 | 325 | // serialize |
michael@0 | 326 | var title = gViewSourceBundle.getString("viewMathMLSourceTitle"); |
michael@0 | 327 | var wrapClass = gWrapLongLines ? ' class="wrap"' : ''; |
michael@0 | 328 | var source = |
michael@0 | 329 | '<!DOCTYPE html>' |
michael@0 | 330 | + '<html>' |
michael@0 | 331 | + '<head><title>' + title + '</title>' |
michael@0 | 332 | + '<link rel="stylesheet" type="text/css" href="' + gViewSourceCSS + '">' |
michael@0 | 333 | + '<style type="text/css">' |
michael@0 | 334 | + '#target { border: dashed 1px; background-color: lightyellow; }' |
michael@0 | 335 | + '</style>' |
michael@0 | 336 | + '</head>' |
michael@0 | 337 | + '<body id="viewsource"' + wrapClass |
michael@0 | 338 | + ' onload="document.title=\''+title+'\';document.getElementById(\'target\').scrollIntoView(true)">' |
michael@0 | 339 | + '<pre>' |
michael@0 | 340 | + getOuterMarkup(topNode, 0) |
michael@0 | 341 | + '</pre></body></html>' |
michael@0 | 342 | ; // end |
michael@0 | 343 | |
michael@0 | 344 | // display |
michael@0 | 345 | gBrowser.loadURI("data:text/html;charset=utf-8," + encodeURIComponent(source)); |
michael@0 | 346 | } |
michael@0 | 347 | |
michael@0 | 348 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 349 | function getInnerMarkup(node, indent) { |
michael@0 | 350 | var str = ''; |
michael@0 | 351 | for (var i = 0; i < node.childNodes.length; i++) { |
michael@0 | 352 | str += getOuterMarkup(node.childNodes.item(i), indent); |
michael@0 | 353 | } |
michael@0 | 354 | return str; |
michael@0 | 355 | } |
michael@0 | 356 | |
michael@0 | 357 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 358 | function getOuterMarkup(node, indent) { |
michael@0 | 359 | var newline = ''; |
michael@0 | 360 | var padding = ''; |
michael@0 | 361 | var str = ''; |
michael@0 | 362 | if (node == gTargetNode) { |
michael@0 | 363 | gStartTargetLine = gLineCount; |
michael@0 | 364 | str += '</pre><pre id="target">'; |
michael@0 | 365 | } |
michael@0 | 366 | |
michael@0 | 367 | switch (node.nodeType) { |
michael@0 | 368 | case Node.ELEMENT_NODE: // Element |
michael@0 | 369 | // to avoid the wide gap problem, '\n' is not emitted on the first |
michael@0 | 370 | // line and the lines before & after the <pre id="target">...</pre> |
michael@0 | 371 | if (gLineCount > 0 && |
michael@0 | 372 | gLineCount != gStartTargetLine && |
michael@0 | 373 | gLineCount != gEndTargetLine) { |
michael@0 | 374 | newline = '\n'; |
michael@0 | 375 | } |
michael@0 | 376 | gLineCount++; |
michael@0 | 377 | if (gDebug) { |
michael@0 | 378 | newline += gLineCount; |
michael@0 | 379 | } |
michael@0 | 380 | for (var k = 0; k < indent; k++) { |
michael@0 | 381 | padding += ' '; |
michael@0 | 382 | } |
michael@0 | 383 | str += newline + padding |
michael@0 | 384 | + '<<span class="start-tag">' + node.nodeName + '</span>'; |
michael@0 | 385 | for (var i = 0; i < node.attributes.length; i++) { |
michael@0 | 386 | var attr = node.attributes.item(i); |
michael@0 | 387 | if (!gDebug && attr.nodeName.match(/^[-_]moz/)) { |
michael@0 | 388 | continue; |
michael@0 | 389 | } |
michael@0 | 390 | str += ' <span class="attribute-name">' |
michael@0 | 391 | + attr.nodeName |
michael@0 | 392 | + '</span>=<span class="attribute-value">"' |
michael@0 | 393 | + unicodeTOentity(attr.nodeValue) |
michael@0 | 394 | + '"</span>'; |
michael@0 | 395 | } |
michael@0 | 396 | if (!node.hasChildNodes()) { |
michael@0 | 397 | str += '/>'; |
michael@0 | 398 | } |
michael@0 | 399 | else { |
michael@0 | 400 | str += '>'; |
michael@0 | 401 | var oldLine = gLineCount; |
michael@0 | 402 | str += getInnerMarkup(node, indent + 2); |
michael@0 | 403 | if (oldLine == gLineCount) { |
michael@0 | 404 | newline = ''; |
michael@0 | 405 | padding = ''; |
michael@0 | 406 | } |
michael@0 | 407 | else { |
michael@0 | 408 | newline = (gLineCount == gEndTargetLine) ? '' : '\n'; |
michael@0 | 409 | gLineCount++; |
michael@0 | 410 | if (gDebug) { |
michael@0 | 411 | newline += gLineCount; |
michael@0 | 412 | } |
michael@0 | 413 | } |
michael@0 | 414 | str += newline + padding |
michael@0 | 415 | + '</<span class="end-tag">' + node.nodeName + '</span>>'; |
michael@0 | 416 | } |
michael@0 | 417 | break; |
michael@0 | 418 | case Node.TEXT_NODE: // Text |
michael@0 | 419 | var tmp = node.nodeValue; |
michael@0 | 420 | tmp = tmp.replace(/(\n|\r|\t)+/g, " "); |
michael@0 | 421 | tmp = tmp.replace(/^ +/, ""); |
michael@0 | 422 | tmp = tmp.replace(/ +$/, ""); |
michael@0 | 423 | if (tmp.length != 0) { |
michael@0 | 424 | str += '<span class="text">' + unicodeTOentity(tmp) + '</span>'; |
michael@0 | 425 | } |
michael@0 | 426 | break; |
michael@0 | 427 | default: |
michael@0 | 428 | break; |
michael@0 | 429 | } |
michael@0 | 430 | |
michael@0 | 431 | if (node == gTargetNode) { |
michael@0 | 432 | gEndTargetLine = gLineCount; |
michael@0 | 433 | str += '</pre><pre>'; |
michael@0 | 434 | } |
michael@0 | 435 | return str; |
michael@0 | 436 | } |
michael@0 | 437 | |
michael@0 | 438 | //////////////////////////////////////////////////////////////////////////////// |
michael@0 | 439 | function unicodeTOentity(text) |
michael@0 | 440 | { |
michael@0 | 441 | const charTable = { |
michael@0 | 442 | '&': '&<span class="entity">amp;</span>', |
michael@0 | 443 | '<': '&<span class="entity">lt;</span>', |
michael@0 | 444 | '>': '&<span class="entity">gt;</span>', |
michael@0 | 445 | '"': '&<span class="entity">quot;</span>' |
michael@0 | 446 | }; |
michael@0 | 447 | |
michael@0 | 448 | function charTableLookup(letter) { |
michael@0 | 449 | return charTable[letter]; |
michael@0 | 450 | } |
michael@0 | 451 | |
michael@0 | 452 | function convertEntity(letter) { |
michael@0 | 453 | try { |
michael@0 | 454 | var unichar = gEntityConverter.ConvertToEntity(letter, entityVersion); |
michael@0 | 455 | var entity = unichar.substring(1); // extract '&' |
michael@0 | 456 | return '&<span class="entity">' + entity + '</span>'; |
michael@0 | 457 | } catch (ex) { |
michael@0 | 458 | return letter; |
michael@0 | 459 | } |
michael@0 | 460 | } |
michael@0 | 461 | |
michael@0 | 462 | if (!gEntityConverter) { |
michael@0 | 463 | try { |
michael@0 | 464 | gEntityConverter = |
michael@0 | 465 | Components.classes["@mozilla.org/intl/entityconverter;1"] |
michael@0 | 466 | .createInstance(Components.interfaces.nsIEntityConverter); |
michael@0 | 467 | } catch(e) { } |
michael@0 | 468 | } |
michael@0 | 469 | |
michael@0 | 470 | const entityVersion = Components.interfaces.nsIEntityConverter.entityW3C; |
michael@0 | 471 | |
michael@0 | 472 | var str = text; |
michael@0 | 473 | |
michael@0 | 474 | // replace chars in our charTable |
michael@0 | 475 | str = str.replace(/[<>&"]/g, charTableLookup); |
michael@0 | 476 | |
michael@0 | 477 | // replace chars > 0x7f via nsIEntityConverter |
michael@0 | 478 | str = str.replace(/[^\0-\u007f]/g, convertEntity); |
michael@0 | 479 | |
michael@0 | 480 | return str; |
michael@0 | 481 | } |