1017 строки
25 KiB
JavaScript
1017 строки
25 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/* import-globals-from editor.js */
|
|
|
|
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
var { AppConstants } = ChromeUtils.import(
|
|
"resource://gre/modules/AppConstants.jsm"
|
|
);
|
|
|
|
// Each editor window must include this file
|
|
// Variables shared by all dialogs:
|
|
|
|
// Object to attach commonly-used widgets (all dialogs should use this)
|
|
var gDialog = {};
|
|
|
|
var kOutputEncodeBasicEntities =
|
|
Ci.nsIDocumentEncoder.OutputEncodeBasicEntities;
|
|
var kOutputEncodeHTMLEntities = Ci.nsIDocumentEncoder.OutputEncodeHTMLEntities;
|
|
var kOutputEncodeLatin1Entities =
|
|
Ci.nsIDocumentEncoder.OutputEncodeLatin1Entities;
|
|
var kOutputEncodeW3CEntities = Ci.nsIDocumentEncoder.OutputEncodeW3CEntities;
|
|
var kOutputFormatted = Ci.nsIDocumentEncoder.OutputFormatted;
|
|
var kOutputLFLineBreak = Ci.nsIDocumentEncoder.OutputLFLineBreak;
|
|
var kOutputSelectionOnly = Ci.nsIDocumentEncoder.OutputSelectionOnly;
|
|
var kOutputWrap = Ci.nsIDocumentEncoder.OutputWrap;
|
|
|
|
var gStringBundle;
|
|
var gFilePickerDirectory;
|
|
|
|
/** *********** Message dialogs ***************/
|
|
|
|
// Optional: Caller may supply text to substitute for "Ok" and/or "Cancel"
|
|
function ConfirmWithTitle(title, message, okButtonText, cancelButtonText) {
|
|
let okFlag = okButtonText
|
|
? Services.prompt.BUTTON_TITLE_IS_STRING
|
|
: Services.prompt.BUTTON_TITLE_OK;
|
|
let cancelFlag = cancelButtonText
|
|
? Services.prompt.BUTTON_TITLE_IS_STRING
|
|
: Services.prompt.BUTTON_TITLE_CANCEL;
|
|
|
|
return (
|
|
Services.prompt.confirmEx(
|
|
window,
|
|
title,
|
|
message,
|
|
okFlag * Services.prompt.BUTTON_POS_0 +
|
|
cancelFlag * Services.prompt.BUTTON_POS_1,
|
|
okButtonText,
|
|
cancelButtonText,
|
|
null,
|
|
null,
|
|
{ value: 0 }
|
|
) == 0
|
|
);
|
|
}
|
|
|
|
/** *********** String Utilities ***************/
|
|
|
|
function GetString(name) {
|
|
if (!gStringBundle) {
|
|
try {
|
|
gStringBundle = Services.strings.createBundle(
|
|
"chrome://editor/locale/editor.properties"
|
|
);
|
|
} catch (ex) {}
|
|
}
|
|
if (gStringBundle) {
|
|
try {
|
|
return gStringBundle.GetStringFromName(name);
|
|
} catch (e) {}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function GetFormattedString(aName, aVal) {
|
|
if (!gStringBundle) {
|
|
try {
|
|
gStringBundle = Services.strings.createBundle(
|
|
"chrome://editor/locale/editor.properties"
|
|
);
|
|
} catch (ex) {}
|
|
}
|
|
if (gStringBundle) {
|
|
try {
|
|
return gStringBundle.formatStringFromName(aName, [aVal]);
|
|
} catch (e) {}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function TrimStringLeft(string) {
|
|
if (!string) {
|
|
return "";
|
|
}
|
|
return string.trimLeft();
|
|
}
|
|
|
|
function TrimStringRight(string) {
|
|
if (!string) {
|
|
return "";
|
|
}
|
|
return string.trimRight();
|
|
}
|
|
|
|
// Remove whitespace from both ends of a string
|
|
function TrimString(string) {
|
|
if (!string) {
|
|
return "";
|
|
}
|
|
return string.trim();
|
|
}
|
|
|
|
function TruncateStringAtWordEnd(string, maxLength, addEllipses) {
|
|
// Return empty if string is null, undefined, or the empty string
|
|
if (!string) {
|
|
return "";
|
|
}
|
|
|
|
// We assume they probably don't want whitespace at the beginning
|
|
string = string.trimLeft();
|
|
if (string.length <= maxLength) {
|
|
return string;
|
|
}
|
|
|
|
// We need to truncate the string to maxLength or fewer chars
|
|
if (addEllipses) {
|
|
maxLength -= 3;
|
|
}
|
|
string = string.replace(RegExp("(.{0," + maxLength + "})\\s.*"), "$1");
|
|
|
|
if (string.length > maxLength) {
|
|
string = string.slice(0, maxLength);
|
|
}
|
|
|
|
if (addEllipses) {
|
|
string += "...";
|
|
}
|
|
return string;
|
|
}
|
|
|
|
// Replace all whitespace characters with supplied character
|
|
// E.g.: Use charReplace = " ", to "unwrap" the string by removing line-end chars
|
|
// Use charReplace = "_" when you don't want spaces (like in a URL)
|
|
function ReplaceWhitespace(string, charReplace) {
|
|
return string.trim().replace(/\s+/g, charReplace);
|
|
}
|
|
|
|
// Replace whitespace with "_" and allow only HTML CDATA
|
|
// characters: "a"-"z","A"-"Z","0"-"9", "_", ":", "-", ".",
|
|
// and characters above ASCII 127
|
|
function ConvertToCDATAString(string) {
|
|
return string
|
|
.replace(/\s+/g, "_")
|
|
.replace(/[^a-zA-Z0-9_\.\-\:\u0080-\uFFFF]+/g, "");
|
|
}
|
|
|
|
function GetSelectionAsText() {
|
|
try {
|
|
return GetCurrentEditor().outputToString(
|
|
"text/plain",
|
|
kOutputSelectionOnly
|
|
);
|
|
} catch (e) {}
|
|
|
|
return "";
|
|
}
|
|
|
|
/** *********** Get Current Editor and associated interfaces or info ***************/
|
|
const nsIPlaintextEditor = Ci.nsIPlaintextEditor;
|
|
const nsIHTMLEditor = Ci.nsIHTMLEditor;
|
|
const nsITableEditor = Ci.nsITableEditor;
|
|
const nsIEditorStyleSheets = Ci.nsIEditorStyleSheets;
|
|
const nsIEditingSession = Ci.nsIEditingSession;
|
|
|
|
function GetCurrentEditor() {
|
|
// Get the active editor from the <editor> tag
|
|
// XXX This will probably change if we support > 1 editor in main Composer window
|
|
// (e.g. a plaintext editor for HTMLSource)
|
|
|
|
// For dialogs: Search up parent chain to find top window with editor
|
|
var editor;
|
|
try {
|
|
var editorElement = GetCurrentEditorElement();
|
|
editor = editorElement.getEditor(editorElement.contentWindow);
|
|
|
|
// Do QIs now so editor users won't have to figure out which interface to use
|
|
// Using "instanceof" does the QI for us.
|
|
editor instanceof Ci.nsIPlaintextEditor;
|
|
editor instanceof Ci.nsIHTMLEditor;
|
|
} catch (e) {
|
|
dump(e) + "\n";
|
|
}
|
|
|
|
return editor;
|
|
}
|
|
|
|
function GetCurrentTableEditor() {
|
|
var editor = GetCurrentEditor();
|
|
return editor && editor instanceof nsITableEditor ? editor : null;
|
|
}
|
|
|
|
function GetCurrentEditorElement() {
|
|
var tmpWindow = window;
|
|
|
|
do {
|
|
// Get the <editor> element(s)
|
|
let editorItem = tmpWindow.document.querySelector("editor");
|
|
|
|
// This will change if we support > 1 editor element
|
|
if (editorItem) {
|
|
return editorItem;
|
|
}
|
|
|
|
tmpWindow = tmpWindow.opener;
|
|
} while (tmpWindow);
|
|
|
|
return null;
|
|
}
|
|
|
|
function GetCurrentCommandManager() {
|
|
try {
|
|
return GetCurrentEditorElement().commandManager;
|
|
} catch (e) {
|
|
dump(e) + "\n";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function GetCurrentEditorType() {
|
|
try {
|
|
return GetCurrentEditorElement().editortype;
|
|
} catch (e) {
|
|
dump(e) + "\n";
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function IsHTMLEditor() {
|
|
// We don't have an editorElement, just return false
|
|
if (!GetCurrentEditorElement()) {
|
|
return false;
|
|
}
|
|
|
|
var editortype = GetCurrentEditorType();
|
|
switch (editortype) {
|
|
case "html":
|
|
case "htmlmail":
|
|
return true;
|
|
|
|
case "text":
|
|
case "textmail":
|
|
return false;
|
|
|
|
default:
|
|
dump("INVALID EDITOR TYPE: " + editortype + "\n");
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function PageIsEmptyAndUntouched() {
|
|
return IsDocumentEmpty() && !IsDocumentModified() && !IsHTMLSourceChanged();
|
|
}
|
|
|
|
function IsInHTMLSourceMode() {
|
|
return gEditorDisplayMode == kDisplayModeSource;
|
|
}
|
|
|
|
function IsInPreviewMode() {
|
|
return gEditorDisplayMode == kDisplayModePreview;
|
|
}
|
|
|
|
// are we editing HTML (i.e. neither in HTML source mode, nor editing a text file)
|
|
function IsEditingRenderedHTML() {
|
|
return IsHTMLEditor() && !IsInHTMLSourceMode();
|
|
}
|
|
|
|
function IsWebComposer() {
|
|
return document.documentElement.id == "editorWindow";
|
|
}
|
|
|
|
function IsDocumentEditable() {
|
|
try {
|
|
return GetCurrentEditor().isDocumentEditable;
|
|
} catch (e) {}
|
|
return false;
|
|
}
|
|
|
|
function IsDocumentEmpty() {
|
|
try {
|
|
return GetCurrentEditor().documentIsEmpty;
|
|
} catch (e) {}
|
|
return false;
|
|
}
|
|
|
|
function IsDocumentModified() {
|
|
try {
|
|
return GetCurrentEditor().documentModified;
|
|
} catch (e) {}
|
|
return false;
|
|
}
|
|
|
|
function IsHTMLSourceChanged() {
|
|
// gSourceTextEditor will not be defined if we're just a text editor.
|
|
return gSourceTextEditor ? gSourceTextEditor.documentModified : false;
|
|
}
|
|
|
|
function newCommandParams() {
|
|
try {
|
|
return Cu.createCommandParams();
|
|
} catch (e) {
|
|
dump("error thrown in newCommandParams: " + e + "\n");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** *********** General editing command utilities ***************/
|
|
|
|
function GetDocumentTitle() {
|
|
try {
|
|
return GetCurrentEditorElement().contentDocument.title;
|
|
} catch (e) {}
|
|
|
|
return "";
|
|
}
|
|
|
|
function SetDocumentTitle(title) {
|
|
try {
|
|
GetCurrentEditorElement().contentDocument.title = title;
|
|
|
|
// Update window title (doesn't work if called from a dialog)
|
|
if ("UpdateWindowTitle" in window) {
|
|
window.UpdateWindowTitle();
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
function EditorGetTextProperty(
|
|
property,
|
|
attribute,
|
|
value,
|
|
firstHas,
|
|
anyHas,
|
|
allHas
|
|
) {
|
|
try {
|
|
return GetCurrentEditor().getInlinePropertyWithAttrValue(
|
|
property,
|
|
attribute,
|
|
value,
|
|
firstHas,
|
|
anyHas,
|
|
allHas
|
|
);
|
|
} catch (e) {}
|
|
}
|
|
|
|
function EditorSetTextProperty(property, attribute, value) {
|
|
try {
|
|
GetCurrentEditor().setInlineProperty(property, attribute, value);
|
|
if ("gContentWindow" in window) {
|
|
window.gContentWindow.focus();
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
function EditorRemoveTextProperty(property, attribute) {
|
|
try {
|
|
GetCurrentEditor().removeInlineProperty(property, attribute);
|
|
if ("gContentWindow" in window) {
|
|
window.gContentWindow.focus();
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
/** *********** Element enbabling/disabling ***************/
|
|
|
|
// this function takes an elementID and a flag
|
|
// if the element can be found by ID, then it is either enabled (by removing "disabled" attr)
|
|
// or disabled (setAttribute) as specified in the "doEnable" parameter
|
|
function SetElementEnabledById(elementID, doEnable) {
|
|
SetElementEnabled(document.getElementById(elementID), doEnable);
|
|
}
|
|
|
|
function SetElementEnabled(element, doEnable) {
|
|
if (element) {
|
|
if (doEnable) {
|
|
element.removeAttribute("disabled");
|
|
} else {
|
|
element.setAttribute("disabled", "true");
|
|
}
|
|
} else {
|
|
dump("Element not found in SetElementEnabled\n");
|
|
}
|
|
}
|
|
|
|
/** *********** Services / Prefs ***************/
|
|
|
|
function GetFileProtocolHandler() {
|
|
let handler = Services.io.getProtocolHandler("file");
|
|
return handler.QueryInterface(Ci.nsIFileProtocolHandler);
|
|
}
|
|
|
|
function SetStringPref(aPrefName, aPrefValue) {
|
|
try {
|
|
Services.prefs.setStringPref(aPrefName, aPrefValue);
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Set initial directory for a filepicker from URLs saved in prefs
|
|
function SetFilePickerDirectory(filePicker, fileType) {
|
|
if (filePicker) {
|
|
try {
|
|
// Save current directory so we can reset it in SaveFilePickerDirectory
|
|
gFilePickerDirectory = filePicker.displayDirectory;
|
|
|
|
let location = Services.prefs.getComplexValue(
|
|
"editor.lastFileLocation." + fileType,
|
|
Ci.nsIFile
|
|
);
|
|
if (location) {
|
|
filePicker.displayDirectory = location;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
// Save the directory of the selected file to prefs
|
|
function SaveFilePickerDirectory(filePicker, fileType) {
|
|
if (filePicker && filePicker.file) {
|
|
try {
|
|
var fileDir;
|
|
if (filePicker.file.parent) {
|
|
fileDir = filePicker.file.parent.QueryInterface(Ci.nsIFile);
|
|
}
|
|
|
|
Services.prefs.setComplexValue(
|
|
"editor.lastFileLocation." + fileType,
|
|
Ci.nsIFile,
|
|
fileDir
|
|
);
|
|
|
|
Services.prefs.savePrefFile(null);
|
|
} catch (e) {}
|
|
}
|
|
|
|
// Restore the directory used before SetFilePickerDirectory was called;
|
|
// This reduces interference with Browser and other module directory defaults
|
|
if (gFilePickerDirectory) {
|
|
filePicker.displayDirectory = gFilePickerDirectory;
|
|
}
|
|
|
|
gFilePickerDirectory = null;
|
|
}
|
|
|
|
function GetDefaultBrowserColors() {
|
|
var colors = {
|
|
TextColor: 0,
|
|
BackgroundColor: 0,
|
|
LinkColor: 0,
|
|
ActiveLinkColor: 0,
|
|
VisitedLinkColor: 0,
|
|
};
|
|
var useSysColors = Services.prefs.getBoolPref(
|
|
"browser.display.use_system_colors",
|
|
false
|
|
);
|
|
|
|
if (!useSysColors) {
|
|
colors.TextColor = Services.prefs.getCharPref(
|
|
"browser.display.foreground_color",
|
|
0
|
|
);
|
|
colors.BackgroundColor = Services.prefs.getCharPref(
|
|
"browser.display.background_color",
|
|
0
|
|
);
|
|
}
|
|
// Use OS colors for text and background if explicitly asked or pref is not set
|
|
if (!colors.TextColor) {
|
|
colors.TextColor = "windowtext";
|
|
}
|
|
|
|
if (!colors.BackgroundColor) {
|
|
colors.BackgroundColor = "window";
|
|
}
|
|
|
|
colors.LinkColor = Services.prefs.getCharPref("browser.anchor_color");
|
|
colors.ActiveLinkColor = Services.prefs.getCharPref("browser.active_color");
|
|
colors.VisitedLinkColor = Services.prefs.getCharPref("browser.visited_color");
|
|
|
|
return colors;
|
|
}
|
|
|
|
/** *********** URL handling ***************/
|
|
|
|
function TextIsURI(selectedText) {
|
|
return (
|
|
selectedText &&
|
|
/^http:\/\/|^https:\/\/|^file:\/\/|^ftp:\/\/|^about:|^mailto:|^news:|^snews:|^telnet:|^ldap:|^ldaps:|^gopher:|^finger:|^javascript:/i.test(
|
|
selectedText
|
|
)
|
|
);
|
|
}
|
|
|
|
function IsUrlAboutBlank(urlString) {
|
|
return urlString == "about:blank";
|
|
}
|
|
|
|
function MakeRelativeUrl(url) {
|
|
let inputUrl = url.trim();
|
|
if (!inputUrl) {
|
|
return inputUrl;
|
|
}
|
|
|
|
// Get the filespec relative to current document's location
|
|
// NOTE: Can't do this if file isn't saved yet!
|
|
var docUrl = GetDocumentBaseUrl();
|
|
var docScheme = GetScheme(docUrl);
|
|
|
|
// Can't relativize if no doc scheme (page hasn't been saved)
|
|
if (!docScheme) {
|
|
return inputUrl;
|
|
}
|
|
|
|
var urlScheme = GetScheme(inputUrl);
|
|
|
|
// Do nothing if not the same scheme or url is already relativized
|
|
if (docScheme != urlScheme) {
|
|
return inputUrl;
|
|
}
|
|
|
|
// Host must be the same
|
|
var docHost = GetHost(docUrl);
|
|
var urlHost = GetHost(inputUrl);
|
|
if (docHost != urlHost) {
|
|
return inputUrl;
|
|
}
|
|
|
|
// Get just the file path part of the urls
|
|
// XXX Should we use GetCurrentEditor().documentCharacterSet for 2nd param ?
|
|
let docPath = Services.io.newURI(
|
|
docUrl,
|
|
GetCurrentEditor().documentCharacterSet
|
|
).pathQueryRef;
|
|
let urlPath = Services.io.newURI(
|
|
inputUrl,
|
|
GetCurrentEditor().documentCharacterSet
|
|
).pathQueryRef;
|
|
|
|
// We only return "urlPath", so we can convert the entire docPath for
|
|
// case-insensitive comparisons.
|
|
var doCaseInsensitive = docScheme == "file" && AppConstants.platform == "win";
|
|
if (doCaseInsensitive) {
|
|
docPath = docPath.toLowerCase();
|
|
}
|
|
|
|
// Get document filename before we start chopping up the docPath
|
|
var docFilename = GetFilename(docPath);
|
|
|
|
// Both url and doc paths now begin with "/"
|
|
// Look for shared dirs starting after that
|
|
urlPath = urlPath.slice(1);
|
|
docPath = docPath.slice(1);
|
|
|
|
var firstDirTest = true;
|
|
var nextDocSlash = 0;
|
|
var done = false;
|
|
|
|
// Remove all matching subdirs common to both doc and input urls
|
|
do {
|
|
nextDocSlash = docPath.indexOf("/");
|
|
var nextUrlSlash = urlPath.indexOf("/");
|
|
|
|
if (nextUrlSlash == -1) {
|
|
// We're done matching and all dirs in url
|
|
// what's left is the filename
|
|
done = true;
|
|
|
|
// Remove filename for named anchors in the same file
|
|
if (nextDocSlash == -1 && docFilename) {
|
|
var anchorIndex = urlPath.indexOf("#");
|
|
if (anchorIndex > 0) {
|
|
var urlFilename = doCaseInsensitive ? urlPath.toLowerCase() : urlPath;
|
|
|
|
if (urlFilename.startsWith(docFilename)) {
|
|
urlPath = urlPath.slice(anchorIndex);
|
|
}
|
|
}
|
|
}
|
|
} else if (nextDocSlash >= 0) {
|
|
// Test for matching subdir
|
|
var docDir = docPath.slice(0, nextDocSlash);
|
|
var urlDir = urlPath.slice(0, nextUrlSlash);
|
|
if (doCaseInsensitive) {
|
|
urlDir = urlDir.toLowerCase();
|
|
}
|
|
|
|
if (urlDir == docDir) {
|
|
// Remove matching dir+"/" from each path
|
|
// and continue to next dir.
|
|
docPath = docPath.slice(nextDocSlash + 1);
|
|
urlPath = urlPath.slice(nextUrlSlash + 1);
|
|
} else {
|
|
// No match, we're done.
|
|
done = true;
|
|
|
|
// Be sure we are on the same local drive or volume
|
|
// (the first "dir" in the path) because we can't
|
|
// relativize to different drives/volumes.
|
|
// UNIX doesn't have volumes, so we must not do this else
|
|
// the first directory will be misinterpreted as a volume name.
|
|
if (
|
|
firstDirTest &&
|
|
docScheme == "file" &&
|
|
AppConstants.platform != "unix"
|
|
) {
|
|
return inputUrl;
|
|
}
|
|
}
|
|
} else {
|
|
// No more doc dirs left, we're done
|
|
done = true;
|
|
}
|
|
|
|
firstDirTest = false;
|
|
} while (!done);
|
|
|
|
// Add "../" for each dir left in docPath
|
|
while (nextDocSlash > 0) {
|
|
urlPath = "../" + urlPath;
|
|
nextDocSlash = docPath.indexOf("/", nextDocSlash + 1);
|
|
}
|
|
return urlPath;
|
|
}
|
|
|
|
function MakeAbsoluteUrl(url) {
|
|
let resultUrl = TrimString(url);
|
|
if (!resultUrl) {
|
|
return resultUrl;
|
|
}
|
|
|
|
// Check if URL is already absolute, i.e., it has a scheme
|
|
let urlScheme = GetScheme(resultUrl);
|
|
|
|
if (urlScheme) {
|
|
return resultUrl;
|
|
}
|
|
|
|
let docUrl = GetDocumentBaseUrl();
|
|
let docScheme = GetScheme(docUrl);
|
|
|
|
// Can't relativize if no doc scheme (page hasn't been saved)
|
|
if (!docScheme) {
|
|
return resultUrl;
|
|
}
|
|
|
|
// Make a URI object to use its "resolve" method
|
|
let absoluteUrl = resultUrl;
|
|
let docUri = Services.io.newURI(
|
|
docUrl,
|
|
GetCurrentEditor().documentCharacterSet
|
|
);
|
|
|
|
try {
|
|
absoluteUrl = docUri.resolve(resultUrl);
|
|
// This is deprecated and buggy!
|
|
// If used, we must make it a path for the parent directory (remove filename)
|
|
// absoluteUrl = IOService.resolveRelativePath(resultUrl, docUrl);
|
|
} catch (e) {}
|
|
|
|
return absoluteUrl;
|
|
}
|
|
|
|
// Get the HREF of the page's <base> tag or the document location
|
|
// returns empty string if no base href and document hasn't been saved yet
|
|
function GetDocumentBaseUrl() {
|
|
try {
|
|
var docUrl;
|
|
|
|
// if document supplies a <base> tag, use that URL instead
|
|
let base = GetCurrentEditor().document.querySelector("base");
|
|
if (base) {
|
|
docUrl = base.getAttribute("href");
|
|
}
|
|
if (!docUrl) {
|
|
docUrl = GetDocumentUrl();
|
|
}
|
|
|
|
if (!IsUrlAboutBlank(docUrl)) {
|
|
return docUrl;
|
|
}
|
|
} catch (e) {}
|
|
return "";
|
|
}
|
|
|
|
function GetDocumentUrl() {
|
|
try {
|
|
return GetCurrentEditor().document.URL;
|
|
} catch (e) {}
|
|
return "";
|
|
}
|
|
|
|
// Extract the scheme (e.g., 'file', 'http') from a URL string
|
|
function GetScheme(urlspec) {
|
|
var resultUrl = TrimString(urlspec);
|
|
// Unsaved document URL has no acceptable scheme yet
|
|
if (!resultUrl || IsUrlAboutBlank(resultUrl)) {
|
|
return "";
|
|
}
|
|
|
|
var scheme = "";
|
|
try {
|
|
// This fails if there's no scheme
|
|
scheme = Services.io.extractScheme(resultUrl);
|
|
} catch (e) {}
|
|
|
|
return scheme ? scheme.toLowerCase() : "";
|
|
}
|
|
|
|
function GetHost(urlspec) {
|
|
if (!urlspec) {
|
|
return "";
|
|
}
|
|
|
|
var host = "";
|
|
try {
|
|
host = Services.io.newURI(urlspec).host;
|
|
} catch (e) {}
|
|
|
|
return host;
|
|
}
|
|
|
|
function GetUsername(urlspec) {
|
|
if (!urlspec) {
|
|
return "";
|
|
}
|
|
|
|
var username = "";
|
|
try {
|
|
username = Services.io.newURI(urlspec).username;
|
|
} catch (e) {}
|
|
|
|
return username;
|
|
}
|
|
|
|
function GetFilename(urlspec) {
|
|
if (!urlspec || IsUrlAboutBlank(urlspec)) {
|
|
return "";
|
|
}
|
|
|
|
var filename;
|
|
|
|
try {
|
|
let uri = Services.io.newURI(urlspec);
|
|
if (uri) {
|
|
let url = uri.QueryInterface(Ci.nsIURL);
|
|
if (url) {
|
|
filename = url.fileName;
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
|
|
return filename ? filename : "";
|
|
}
|
|
|
|
// Return the url without username and password
|
|
// Optional output objects return extracted username and password strings
|
|
// This uses just string routines via nsIIOServices
|
|
function StripUsernamePassword(urlspec, usernameObj, passwordObj) {
|
|
urlspec = TrimString(urlspec);
|
|
if (!urlspec || IsUrlAboutBlank(urlspec)) {
|
|
return urlspec;
|
|
}
|
|
|
|
if (usernameObj) {
|
|
usernameObj.value = "";
|
|
}
|
|
if (passwordObj) {
|
|
passwordObj.value = "";
|
|
}
|
|
|
|
// "@" must exist else we will never detect username or password
|
|
var atIndex = urlspec.indexOf("@");
|
|
if (atIndex > 0) {
|
|
try {
|
|
let uri = Services.io.newURI(urlspec);
|
|
let username = uri.username;
|
|
let password = uri.password;
|
|
|
|
if (usernameObj && username) {
|
|
usernameObj.value = username;
|
|
}
|
|
if (passwordObj && password) {
|
|
passwordObj.value = password;
|
|
}
|
|
if (username) {
|
|
let usernameStart = urlspec.indexOf(username);
|
|
if (usernameStart != -1) {
|
|
return urlspec.slice(0, usernameStart) + urlspec.slice(atIndex + 1);
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
return urlspec;
|
|
}
|
|
|
|
function StripPassword(urlspec, passwordObj) {
|
|
urlspec = TrimString(urlspec);
|
|
if (!urlspec || IsUrlAboutBlank(urlspec)) {
|
|
return urlspec;
|
|
}
|
|
|
|
if (passwordObj) {
|
|
passwordObj.value = "";
|
|
}
|
|
|
|
// "@" must exist else we will never detect password
|
|
var atIndex = urlspec.indexOf("@");
|
|
if (atIndex > 0) {
|
|
try {
|
|
let password = Services.io.newURI(urlspec).password;
|
|
|
|
if (passwordObj && password) {
|
|
passwordObj.value = password;
|
|
}
|
|
if (password) {
|
|
// Find last ":" before "@"
|
|
let colon = urlspec.lastIndexOf(":", atIndex);
|
|
if (colon != -1) {
|
|
// Include the "@"
|
|
return urlspec.slice(0, colon) + urlspec.slice(atIndex);
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
return urlspec;
|
|
}
|
|
|
|
// Version to use when you have an nsIURI object
|
|
function StripUsernamePasswordFromURI(uri) {
|
|
var urlspec = "";
|
|
if (uri) {
|
|
try {
|
|
urlspec = uri.spec;
|
|
var userPass = uri.userPass;
|
|
if (userPass) {
|
|
let start = urlspec.indexOf(userPass);
|
|
urlspec =
|
|
urlspec.slice(0, start) + urlspec.slice(start + userPass.length + 1);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
return urlspec;
|
|
}
|
|
|
|
function InsertUsernameIntoUrl(urlspec, username) {
|
|
if (!urlspec || !username) {
|
|
return urlspec;
|
|
}
|
|
|
|
try {
|
|
let URI = Services.io.newURI(
|
|
urlspec,
|
|
GetCurrentEditor().documentCharacterSet
|
|
);
|
|
URI.username = username;
|
|
return URI.spec;
|
|
} catch (e) {}
|
|
|
|
return urlspec;
|
|
}
|
|
|
|
function ConvertRGBColorIntoHEXColor(color) {
|
|
if (/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.test(color)) {
|
|
var r = Number(RegExp.$1).toString(16);
|
|
if (r.length == 1) {
|
|
r = "0" + r;
|
|
}
|
|
var g = Number(RegExp.$2).toString(16);
|
|
if (g.length == 1) {
|
|
g = "0" + g;
|
|
}
|
|
var b = Number(RegExp.$3).toString(16);
|
|
if (b.length == 1) {
|
|
b = "0" + b;
|
|
}
|
|
return "#" + r + g + b;
|
|
}
|
|
|
|
return color;
|
|
}
|
|
|
|
/** *********** CSS ***************/
|
|
|
|
function GetHTMLOrCSSStyleValue(element, attrName, cssPropertyName) {
|
|
var value;
|
|
if (Services.prefs.getBoolPref("editor.use_css") && IsHTMLEditor()) {
|
|
value = element.style.getPropertyValue(cssPropertyName);
|
|
}
|
|
|
|
if (!value) {
|
|
value = element.getAttribute(attrName);
|
|
}
|
|
|
|
if (!value) {
|
|
return "";
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/** *********** Miscellaneous ***************/
|
|
// Clone simple JS objects
|
|
function Clone(obj) {
|
|
var clone = {};
|
|
for (var i in obj) {
|
|
if (typeof obj[i] == "object") {
|
|
clone[i] = Clone(obj[i]);
|
|
} else {
|
|
clone[i] = obj[i];
|
|
}
|
|
}
|
|
return clone;
|
|
}
|
|
|
|
/**
|
|
* Utility functions to handle shortended data: URLs in EdColorProps.js and EdImageOverlay.js.
|
|
*/
|
|
|
|
/**
|
|
* Is the passed in image URI a shortened data URI?
|
|
* @return {bool}
|
|
*/
|
|
function isImageDataShortened(aImageData) {
|
|
return /^data:/i.test(aImageData) && aImageData.includes("…");
|
|
}
|
|
|
|
/**
|
|
* Event handler for Copy or Cut
|
|
* @param aEvent the event
|
|
*/
|
|
function onCopyOrCutShortened(aEvent) {
|
|
// Put the original data URI onto the clipboard in case the value
|
|
// is a shortened data URI.
|
|
let field = aEvent.target;
|
|
let startPos = field.selectionStart;
|
|
if (startPos == undefined) {
|
|
return;
|
|
}
|
|
let endPos = field.selectionEnd;
|
|
let selection = field.value.substring(startPos, endPos).trim();
|
|
|
|
// Test that a) the user selected the whole value,
|
|
// b) the value is a data URI,
|
|
// c) it contains the ellipsis we added. Otherwise it could be
|
|
// a new value that the user pasted in.
|
|
if (selection == field.value.trim() && isImageDataShortened(selection)) {
|
|
aEvent.clipboardData.setData("text/plain", field.fullDataURI);
|
|
if (aEvent.type == "cut") {
|
|
// We have to cut the selection manually. Since we tested that
|
|
// everything was selected, we can just reset the field.
|
|
field.value = "";
|
|
}
|
|
aEvent.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up element showing an image URI with a shortened version.
|
|
* and add event handler for Copy or Cut.
|
|
*
|
|
* @param aImageData the data: URL of the image to be shortened.
|
|
* Note: Original stored in 'aDialogField.fullDataURI'.
|
|
* @param aDialogField The field of the dialog to contain the data.
|
|
* @return {bool} URL was shortened?
|
|
*/
|
|
function shortenImageData(aImageData, aDialogField) {
|
|
let shortened = false;
|
|
aDialogField.value = aImageData.replace(/^(data:.+;base64,)(.*)/i, function(
|
|
match,
|
|
nonDataPart,
|
|
dataPart
|
|
) {
|
|
if (dataPart.length <= 35) {
|
|
return match;
|
|
}
|
|
|
|
shortened = true;
|
|
aDialogField.addEventListener("copy", onCopyOrCutShortened);
|
|
aDialogField.addEventListener("cut", onCopyOrCutShortened);
|
|
aDialogField.fullDataURI = aImageData;
|
|
aDialogField.removeAttribute("tooltiptext");
|
|
aDialogField.setAttribute("tooltip", "shortenedDataURI");
|
|
return (
|
|
nonDataPart +
|
|
dataPart.substring(0, 5) +
|
|
"…" +
|
|
dataPart.substring(dataPart.length - 30)
|
|
);
|
|
});
|
|
return shortened;
|
|
}
|
|
|
|
/**
|
|
* Return full data URIs for a shortened element.
|
|
*
|
|
* @param aDialogField The field of the dialog containing the data.
|
|
*/
|
|
function restoredImageData(aDialogField) {
|
|
return aDialogField.fullDataURI;
|
|
}
|