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