Bug 1067325 - MathML source in tab. r=mconley

This commit is contained in:
J. Ryan Stinnett 2015-05-23 18:17:50 -05:00
Родитель 23ee447382
Коммит a9e07e1d86
5 изменённых файлов: 241 добавлений и 202 удалений

Просмотреть файл

@ -1012,8 +1012,14 @@ nsContextMenu.prototype = {
inBackground: false inBackground: false
}); });
let viewSourceBrowser = gBrowser.getBrowserForTab(tab); let viewSourceBrowser = gBrowser.getBrowserForTab(tab);
top.gViewSourceUtils.viewSourceFromSelectionInBrowser(reference, if (aContext == "selection") {
viewSourceBrowser); top.gViewSourceUtils
.viewSourceFromSelectionInBrowser(reference, viewSourceBrowser);
} else {
top.gViewSourceUtils
.viewSourceFromFragmentInBrowser(reference, aContext,
viewSourceBrowser);
}
} else { } else {
// unused (and play nice for fragments generated via XSLT too) // unused (and play nice for fragments generated via XSLT too)
var docUrl = null; var docUrl = null;

Просмотреть файл

@ -13,15 +13,17 @@ XPCOMUtils.defineLazyModuleGetter(this, "Services",
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm"); "resource://gre/modules/Deprecated.jsm");
const NS_XHTML = 'http://www.w3.org/1999/xhtml'; const NS_XHTML = "http://www.w3.org/1999/xhtml";
const VIEW_SOURCE_CSS = "resource://gre-resources/viewsource.css";
const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
// These are markers used to delimit the selection during processing. They // These are markers used to delimit the selection during processing. They
// are removed from the final rendering. // are removed from the final rendering.
// We use noncharacter Unicode codepoints to minimize the risk of clashing // We use noncharacter Unicode codepoints to minimize the risk of clashing
// with anything that might legitimately be present in the document. // with anything that might legitimately be present in the document.
// U+FDD0..FDEF <noncharacters> // U+FDD0..FDEF <noncharacters>
const MARK_SELECTION_START = '\uFDD0'; const MARK_SELECTION_START = "\uFDD0";
const MARK_SELECTION_END = '\uFDEF'; const MARK_SELECTION_END = "\uFDEF";
this.EXPORTED_SYMBOLS = ["ViewSourceBrowser"]; this.EXPORTED_SYMBOLS = ["ViewSourceBrowser"];
@ -115,6 +117,23 @@ ViewSourceBrowser.prototype = {
return this.browser.webNavigation; return this.browser.webNavigation;
}, },
/**
* Getter for whether long lines should be wrapped.
*/
get wrapLongLines() {
return Services.prefs.getBoolPref("view_source.wrap_long_lines");
},
/**
* A getter for the view source string bundle.
*/
get bundle() {
if (this._bundle) {
return this._bundle;
}
return this._bundle = Services.strings.createBundle(BUNDLE_URL);
},
/** /**
* Loads the source for a URL while applying some optional features if * Loads the source for a URL while applying some optional features if
* enabled. * enabled.
@ -339,4 +358,185 @@ ViewSourceBrowser.prototype = {
} while (n != ancestor && p); } while (n != ancestor && p);
return path; return path;
}, },
/**
* Load the view source browser from a fragment of some document, as in
* markups such as MathML where reformatting the output is helpful.
*
* @param aNode
* Some element within the fragment of interest.
* @param aContext
* A string denoting the type of fragment. Currently, "mathml" is the
* only accepted value.
*/
loadViewSourceFromFragment(node, context) {
var Node = node.ownerDocument.defaultView.Node;
this._lineCount = 0;
this._startTargetLine = 0;
this._endTargetLine = 0;
this._targetNode = node;
if (this._targetNode && this._targetNode.nodeType == Node.TEXT_NODE)
this._targetNode = this._targetNode.parentNode;
// walk up the tree to the top-level element (e.g., <math>, <svg>)
var topTag;
if (context == "mathml")
topTag = "math";
else
throw "not reached";
var topNode = this._targetNode;
while (topNode && topNode.localName != topTag)
topNode = topNode.parentNode;
if (!topNode)
return;
// serialize
var title = this.bundle.GetStringFromName("viewMathMLSourceTitle");
var wrapClass = this.wrapLongLines ? ' class="wrap"' : '';
var source =
'<!DOCTYPE html>'
+ '<html>'
+ '<head><title>' + title + '</title>'
+ '<link rel="stylesheet" type="text/css" href="' + VIEW_SOURCE_CSS + '">'
+ '<style type="text/css">'
+ '#target { border: dashed 1px; background-color: lightyellow; }'
+ '</style>'
+ '</head>'
+ '<body id="viewsource"' + wrapClass
+ ' onload="document.title=\''+title+'\'; document.getElementById(\'target\').scrollIntoView(true)">'
+ '<pre>'
+ this._getOuterMarkup(topNode, 0)
+ '</pre></body></html>'
; // end
// display
this.browser.loadURI("data:text/html;charset=utf-8," +
encodeURIComponent(source));
},
_getInnerMarkup(node, indent) {
var str = '';
for (var i = 0; i < node.childNodes.length; i++) {
str += this._getOuterMarkup(node.childNodes.item(i), indent);
}
return str;
},
_getOuterMarkup(node, indent) {
var Node = node.ownerDocument.defaultView.Node;
var newline = "";
var padding = "";
var str = "";
if (node == this._targetNode) {
this._startTargetLine = this._lineCount;
str += '</pre><pre id="target">';
}
switch (node.nodeType) {
case Node.ELEMENT_NODE: // Element
// to avoid the wide gap problem, '\n' is not emitted on the first
// line and the lines before & after the <pre id="target">...</pre>
if (this._lineCount > 0 &&
this._lineCount != this._startTargetLine &&
this._lineCount != this._endTargetLine) {
newline = "\n";
}
this._lineCount++;
for (var k = 0; k < indent; k++) {
padding += " ";
}
str += newline + padding
+ '&lt;<span class="start-tag">' + node.nodeName + '</span>';
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes.item(i);
if (attr.nodeName.match(/^[-_]moz/)) {
continue;
}
str += ' <span class="attribute-name">'
+ attr.nodeName
+ '</span>=<span class="attribute-value">"'
+ this._unicodeToEntity(attr.nodeValue)
+ '"</span>';
}
if (!node.hasChildNodes()) {
str += "/&gt;";
}
else {
str += "&gt;";
var oldLine = this._lineCount;
str += this._getInnerMarkup(node, indent + 2);
if (oldLine == this._lineCount) {
newline = "";
padding = "";
}
else {
newline = (this._lineCount == this._endTargetLine) ? "" : "\n";
this._lineCount++;
}
str += newline + padding
+ '&lt;/<span class="end-tag">' + node.nodeName + '</span>&gt;';
}
break;
case Node.TEXT_NODE: // Text
var tmp = node.nodeValue;
tmp = tmp.replace(/(\n|\r|\t)+/g, " ");
tmp = tmp.replace(/^ +/, "");
tmp = tmp.replace(/ +$/, "");
if (tmp.length != 0) {
str += '<span class="text">' + this._unicodeToEntity(tmp) + '</span>';
}
break;
default:
break;
}
if (node == this._targetNode) {
this._endTargetLine = this._lineCount;
str += '</pre><pre>';
}
return str;
},
_unicodeToEntity(text) {
const charTable = {
'&': '&amp;<span class="entity">amp;</span>',
'<': '&amp;<span class="entity">lt;</span>',
'>': '&amp;<span class="entity">gt;</span>',
'"': '&amp;<span class="entity">quot;</span>'
};
function charTableLookup(letter) {
return charTable[letter];
}
function convertEntity(letter) {
try {
var unichar = this._entityConverter
.ConvertToEntity(letter, entityVersion);
var entity = unichar.substring(1); // extract '&'
return '&amp;<span class="entity">' + entity + '</span>';
} catch (ex) {
return letter;
}
}
if (!this._entityConverter) {
try {
this._entityConverter = Cc["@mozilla.org/intl/entityconverter;1"]
.createInstance(Ci.nsIEntityConverter);
} catch(e) { }
}
const entityVersion = Ci.nsIEntityConverter.entityW3C;
var str = text;
// replace chars in our charTable
str = str.replace(/[<>&"]/g, charTableLookup);
// replace chars > 0x7f via nsIEntityConverter
str = str.replace(/[^\0-\u007f]/g, convertEntity);
return str;
},
}; };

Просмотреть файл

@ -6,22 +6,12 @@
Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/Services.jsm");
var gDebug = 0; function onLoadViewPartialSource() {
var gLineCount = 0;
var gStartTargetLine = 0;
var gEndTargetLine = 0;
var gTargetNode = null;
var gEntityConverter = null;
var gWrapLongLines = false;
const gViewSourceCSS = 'resource://gre-resources/viewsource.css';
function onLoadViewPartialSource()
{
// check the view_source.wrap_long_lines pref // check the view_source.wrap_long_lines pref
// and set the menuitem's checked attribute accordingly // and set the menuitem's checked attribute accordingly
gWrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines"); let wrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
document.getElementById("menu_wrapLongLines").setAttribute("checked", gWrapLongLines); document.getElementById("menu_wrapLongLines")
.setAttribute("checked", wrapLongLines);
document.getElementById("menu_highlightSyntax") document.getElementById("menu_highlightSyntax")
.setAttribute("checked", .setAttribute("checked",
Services.prefs.getBoolPref("view_source.syntax_highlight")); Services.prefs.getBoolPref("view_source.syntax_highlight"));
@ -29,186 +19,7 @@ function onLoadViewPartialSource()
if (window.arguments[3] == 'selection') if (window.arguments[3] == 'selection')
viewSourceChrome.loadViewSourceFromSelection(window.arguments[2]); viewSourceChrome.loadViewSourceFromSelection(window.arguments[2]);
else else
viewPartialSourceForFragment(window.arguments[2], window.arguments[3]); viewSourceChrome.loadViewSourceFromFragment(window.arguments[2], window.arguments[3]);
window.content.focus(); window.content.focus();
} }
////////////////////////////////////////////////////////////////////////////////
// special handler for markups such as MathML where reformatting the output is
// helpful
function viewPartialSourceForFragment(node, context)
{
gTargetNode = node;
if (gTargetNode && gTargetNode.nodeType == Node.TEXT_NODE)
gTargetNode = gTargetNode.parentNode;
// walk up the tree to the top-level element (e.g., <math>, <svg>)
var topTag;
if (context == 'mathml')
topTag = 'math';
else
throw 'not reached';
var topNode = gTargetNode;
while (topNode && topNode.localName != topTag)
topNode = topNode.parentNode;
if (!topNode)
return;
// serialize
var title = gViewSourceBundle.getString("viewMathMLSourceTitle");
var wrapClass = gWrapLongLines ? ' class="wrap"' : '';
var source =
'<!DOCTYPE html>'
+ '<html>'
+ '<head><title>' + title + '</title>'
+ '<link rel="stylesheet" type="text/css" href="' + gViewSourceCSS + '">'
+ '<style type="text/css">'
+ '#target { border: dashed 1px; background-color: lightyellow; }'
+ '</style>'
+ '</head>'
+ '<body id="viewsource"' + wrapClass
+ ' onload="document.title=\''+title+'\';document.getElementById(\'target\').scrollIntoView(true)">'
+ '<pre>'
+ getOuterMarkup(topNode, 0)
+ '</pre></body></html>'
; // end
// display
gBrowser.loadURI("data:text/html;charset=utf-8," + encodeURIComponent(source));
}
////////////////////////////////////////////////////////////////////////////////
function getInnerMarkup(node, indent) {
var str = '';
for (var i = 0; i < node.childNodes.length; i++) {
str += getOuterMarkup(node.childNodes.item(i), indent);
}
return str;
}
////////////////////////////////////////////////////////////////////////////////
function getOuterMarkup(node, indent) {
var newline = '';
var padding = '';
var str = '';
if (node == gTargetNode) {
gStartTargetLine = gLineCount;
str += '</pre><pre id="target">';
}
switch (node.nodeType) {
case Node.ELEMENT_NODE: // Element
// to avoid the wide gap problem, '\n' is not emitted on the first
// line and the lines before & after the <pre id="target">...</pre>
if (gLineCount > 0 &&
gLineCount != gStartTargetLine &&
gLineCount != gEndTargetLine) {
newline = '\n';
}
gLineCount++;
if (gDebug) {
newline += gLineCount;
}
for (var k = 0; k < indent; k++) {
padding += ' ';
}
str += newline + padding
+ '&lt;<span class="start-tag">' + node.nodeName + '</span>';
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes.item(i);
if (!gDebug && attr.nodeName.match(/^[-_]moz/)) {
continue;
}
str += ' <span class="attribute-name">'
+ attr.nodeName
+ '</span>=<span class="attribute-value">"'
+ unicodeTOentity(attr.nodeValue)
+ '"</span>';
}
if (!node.hasChildNodes()) {
str += '/&gt;';
}
else {
str += '&gt;';
var oldLine = gLineCount;
str += getInnerMarkup(node, indent + 2);
if (oldLine == gLineCount) {
newline = '';
padding = '';
}
else {
newline = (gLineCount == gEndTargetLine) ? '' : '\n';
gLineCount++;
if (gDebug) {
newline += gLineCount;
}
}
str += newline + padding
+ '&lt;/<span class="end-tag">' + node.nodeName + '</span>&gt;';
}
break;
case Node.TEXT_NODE: // Text
var tmp = node.nodeValue;
tmp = tmp.replace(/(\n|\r|\t)+/g, " ");
tmp = tmp.replace(/^ +/, "");
tmp = tmp.replace(/ +$/, "");
if (tmp.length != 0) {
str += '<span class="text">' + unicodeTOentity(tmp) + '</span>';
}
break;
default:
break;
}
if (node == gTargetNode) {
gEndTargetLine = gLineCount;
str += '</pre><pre>';
}
return str;
}
////////////////////////////////////////////////////////////////////////////////
function unicodeTOentity(text)
{
const charTable = {
'&': '&amp;<span class="entity">amp;</span>',
'<': '&amp;<span class="entity">lt;</span>',
'>': '&amp;<span class="entity">gt;</span>',
'"': '&amp;<span class="entity">quot;</span>'
};
function charTableLookup(letter) {
return charTable[letter];
}
function convertEntity(letter) {
try {
var unichar = gEntityConverter.ConvertToEntity(letter, entityVersion);
var entity = unichar.substring(1); // extract '&'
return '&amp;<span class="entity">' + entity + '</span>';
} catch (ex) {
return letter;
}
}
if (!gEntityConverter) {
try {
gEntityConverter =
Components.classes["@mozilla.org/intl/entityconverter;1"]
.createInstance(Components.interfaces.nsIEntityConverter);
} catch(e) { }
}
const entityVersion = Components.interfaces.nsIEntityConverter.entityW3C;
var str = text;
// replace chars in our charTable
str = str.replace(/[<>&"]/g, charTableLookup);
// replace chars > 0x7f via nsIEntityConverter
str = str.replace(/[^\0-\u007f]/g, convertEntity);
return str;
}

Просмотреть файл

@ -19,8 +19,8 @@ const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
// We use noncharacter Unicode codepoints to minimize the risk of clashing // We use noncharacter Unicode codepoints to minimize the risk of clashing
// with anything that might legitimately be present in the document. // with anything that might legitimately be present in the document.
// U+FDD0..FDEF <noncharacters> // U+FDD0..FDEF <noncharacters>
const MARK_SELECTION_START = '\uFDD0'; const MARK_SELECTION_START = "\uFDD0";
const MARK_SELECTION_END = '\uFDEF'; const MARK_SELECTION_END = "\uFDEF";
let global = this; let global = this;
@ -69,7 +69,8 @@ let ViewSourceContent = {
get isViewSource() { get isViewSource() {
let uri = content.document.documentURI; let uri = content.document.documentURI;
return uri.startsWith("view-source:"); return uri.startsWith("view-source:") ||
(uri.startsWith("data:") && uri.includes("MathML"));
}, },
get isAboutBlank() { get isAboutBlank() {

Просмотреть файл

@ -109,6 +109,27 @@ var gViewSourceUtils = {
viewSourceBrowser.loadViewSourceFromSelection(aSelection); viewSourceBrowser.loadViewSourceFromSelection(aSelection);
}, },
/**
* Displays view source for a MathML fragment from some document in the
* provided <browser>. This allows for non-window display methods, such as a
* tab from Firefox. The caller that manages the <browser> is responsible for
* ensuring the companion frame script, viewSource-content.js, has been loaded
* for the <browser>.
*
* @param aNode
* Some element within the fragment of interest.
* @param aContext
* A string denoting the type of fragment. Currently, "mathml" is the
* only accepted value.
* @param aViewSourceInBrowser
* The browser to display the view source in.
*/
viewSourceFromFragmentInBrowser: function(aNode, aContext,
aViewSourceInBrowser) {
let viewSourceBrowser = new ViewSourceBrowser(aViewSourceInBrowser);
viewSourceBrowser.loadViewSourceFromFragment(aNode, aContext);
},
// Opens the interval view source viewer // Opens the interval view source viewer
_openInInternalViewer: function(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber) _openInInternalViewer: function(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber)
{ {