1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2026-02-13 07:05:46 +01:00

Rename liberator/ to common/

This commit is contained in:
Daniel Bainton
2008-12-04 07:56:35 +02:00
parent cc6bdfc2fc
commit 729854c749
30 changed files with 0 additions and 0 deletions

110
common/Makefile.common Normal file
View File

@@ -0,0 +1,110 @@
#### configuration
TOP = $(shell pwd)
OS = $(shell uname -s)
BUILD_DATE = $(shell date "+%Y/%m/%d %H:%M:%S")
BASE = $(TOP)/../liberator
DOC_SRC_FILES = $(wildcard locale/*/*.txt)
DOC_FILES = ${DOC_SRC_FILES:%.txt=%.html}
MAKE_JAR = VERSION="$(VERSION)" DATE="$(BUILD_DATE)" sh $(BASE)/make_jar.sh
# TODO: specify source files manually?
JAR_BASES = $(TOP) $(BASE)
JAR_DIRS = content skin locale
JAR_TEXTS = js css dtd xml xul html xhtml
JAR_BINS = png
JAR = chrome/${NAME}.jar
XPI_BASES = $(JAR_BASES) $(TOP)/..
XPI_FILES = install.rdf TODO AUTHORS Donators NEWS License.txt
XPI_DIRS = modules components chrome
XPI_TEXTS = js jsm
XPI_BINS = jar
XPI_NAME = ${NAME}_${VERSION}
XPI_PATH = ../downloads/${XPI_NAME}
XPI = $(XPI_PATH).xpi
RDF = ../downloads/update.rdf
RDF_IN = ${RDF}.in
BUILD_DIR = build.${VERSION}.${OS}
ASCIIDOC = asciidoc
.SILENT:
#### rules
.PHONY: all help info doc jar xpi install clean distclean $(JAR)
all: help
help:
@echo "${NAME} ${VERSION} build"
@echo
@echo " make help - display this help"
@echo " make info - show some info about the system"
@echo " make doc - build doc files"
@echo " make jar - build a JAR (${JAR})"
@echo " make xpi - build an XPI (${XPI_NAME})"
@echo " make release - updates update.rdf (this is not for you)"
@echo " make clean - clean up"
@echo " make distclean - clean up more"
@echo
@echo "running some commands with V=1 will show more build details"
info:
@echo "version ${VERSION}"
@echo "release file ${XPI}"
@echo "doc files ${DOC_SRC_FILES}"
@echo -e "jar files $(shell echo ${JAR_FILES} | sed 's/ /\\n /g' )"
@echo "xpi files ${XPI_FILES}"
doc: ${DOC_FILES}
xpi: ${XPI}
jar: ${JAR}
release: ${XPI} ${RDF}
${RDF}: ${RDF_IN} Makefile
@echo "Preparing release..."
${Q}${SED} -e "s,###VERSION###,${VERSION},g" \
-e "s,###DATE###,${BUILD_DATE},g" \
< $< > $@
@echo "SUCCESS: $@"
clean:
@echo "Cleanup..."
rm -f ${JAR} ${XPI}
find . -name '*~' -exec rm -f {} \;
distclean: clean
@echo "More cleanup..."
rm -f ${DOC_FILES}
rm -rf ${BUILD_DIR}
#### xpi
$(XPI): $(JAR)
@echo "Building XPI..."
mkdir -p $(XPI_PATH)
awk -v 'name=$(NAME)' -f $(BASE)/process_manifest.awk $(TOP)/chrome.manifest >$(XPI_PATH)/chrome.manifest
$(MAKE_JAR) "$(XPI)" "$(XPI_BASES)" "$(XPI_DIRS)" "$(XPI_TEXTS)" "$(XPI_BINS)" "$(XPI_FILES)"
@echo "SUCCESS: $@"
#### jar
$(JAR): doc
@echo "Building JAR..."
$(MAKE_JAR) "$(JAR)" "$(JAR_BASES)" "$(JAR_DIRS)" "$(JAR_TEXTS)" "$(JAR_BINS)" "$(JAR_FILES)"
@echo "SUCCESS: $@"
#### doc
${DOC_FILES}: %.html: %.txt $(BASE)/Makefile.common locale/en-US/asciidoc.conf
@echo "DOC $@"
${ASCIIDOC} --unsafe -a linkcss -o $@ $<

149
common/content/README.E4X Normal file
View File

@@ -0,0 +1,149 @@
A terse introduction to E4X
Public Domain
The inline XML literals in this code are part of E4X, a standard
XML processing interface for ECMAScript. In addition to syntax
for XML literals, E4X provides a new kind of native object,
"xml", and a syntax, similar to XPath, for accessing and
modifying the tree. Here is a brief synopsis of the kind of
usage you'll see herein:
> let xml =
<foo bar="baz" baz="qux">
<bar>
<baz id="1"/>
</bar>
<baz id="2"/>
</foo>;
// Select all bar elements of the root foo element
> xml.bar
<bar><baz id="1"/></bar>
// Select all baz elements anywhere beneath the root
> xml..baz
<baz id="1"/>
<baz id="2"/>
// Select all of the immediate children of the root
> xml.*
<bar><baz id="1"/></bar>
<baz id="2"/>
// Select the bar attribute of the root node
> xml.@bar
baz
// Select all id attributes in the tree
> xml..@id
1
2
// Select all attributes of the root node
> xml.@*
baz
quz
// Add a quux elemend beneath the first baz
> xml..baz[0] += <quux/>
<baz id="1"/>
<quux/>
> xml
<foo bar="baz" baz="qux">
<bar>
<baz id="1"/>
<quux/>
</bar>
<baz id="2"/>
</foo>
// and beneath the second
> xml.baz[1] = <quux id="1"/>
> xml
<foo bar="baz" baz="qux">
<bar>
<baz id="1"/>
<quux/>
</bar>
<baz id="2"/>
<quux id="1"/>
</foo>
// Replace bar's subtree with a foo element
> xml.bar.* = <foo id="1"/>
> xml
<foo bar="baz" baz="qux">
<bar>
<foo id="1"/>
</bar>
<baz id="2"/>
<quux id="1"/>
</foo>
// Add a bar below bar
> xml.bar.* += <bar id="1"/>
<foo id="1"/>
<bar id="1"/>
> xml
<foo bar="baz" baz="qux">
<bar>
<foo id="1"/>
<bar id="1"/>
</bar>
<baz id="2"/>
<quux id="1"/>
</foo>
// Adding a quux attribute to the root
> xml.@quux = "foo"
foo
> xml
<foo bar="baz" baz="qux" quux="foo">
<bar>
<foo id="1"/>
<bar id="1"/>
</bar>
<baz id="2"/>
<quux id="1"/>
</foo>
> xml.bar.@id = "0"
> xml..foo[0] = "Foo"
Foo
> xml..bar[1] = "Bar"
Bar
> xml
js> xml
<foo bar="baz" baz="qux" quux="foo" id="0">
<bar id="0">
<foo id="1">Foo</foo>
<bar id="1">Bar</bar>
</bar>
<baz id="2"/>
<quux id="1"/>
</foo>
// Selecting all bar elements where id="1"
> xml..bar.(@id == 1)
Bar
// Literals:
// XMLList literal. No root node.
> <>Foo<br/>Baz</>
Foo
<br/>
Baz
// Interpolation.
> let x = "<foo/>"
> <foo bar={x}>{x + "<?>"}</foo>
<foo/><?>
> <foo bar={x}>{x + "<?>"}</foo>.toXMLString()
<foo bar="&lt;foo/>">&lt;foo/&gt;&lt;?&gt;</foo>
> let x = <foo/>
> <foo bar={x}>{x}</foo>.toXMLString()
<foo bar="">
<foo/>
</foo>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl"
xmlns:liberator="http://vimperator.org/namespaces/liberator"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml">
<binding id="frame">
<content>
<html:div liberator:highlight="FrameIndicator"/>
<children/>
</content>
</binding>
<binding id="compitem-td">
<!-- No white space. The table is white-space: pre; :( -->
<content><html:span class="td-strut"/><html:span class="td-span"><children/></html:span></content>
</binding>
<binding id="tab" display="xul:hbox"
extends="chrome://global/content/bindings/tabbox.xml#tab">
<content chromedir="ltr" closetabtext="Close Tab">
<xul:stack class="liberator-tab-stack">
<xul:image xbl:inherits="validate,src=image" class="tab-icon-image" liberator:highlight="TabIcon"/>
<xul:vbox>
<xul:spring flex="1"/>
<xul:label xbl:inherits="value=ordinal" liberator:highlight="TabIconNumber"/>
<xul:spring flex="1"/>
</xul:vbox>
</xul:stack>
<xul:label xbl:inherits="value=ordinal" liberator:highlight="TabNumber"/>
<xul:label flex="1" xbl:inherits="value=label,crop,accesskey" class="tab-text" liberator:highlight="TabText"/>
<xul:toolbarbutton anonid="close-button" tabindex="-1" class="tab-close-button" liberator:highlight="TabClose"/>
</content>
</binding>
</bindings>
<!-- vim:se ft=xbl sw=4 sts=4 tw=0 et: -->

1708
common/content/buffer.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title/>
</head>
<body/>
</html>

892
common/content/commands.js Normal file
View File

@@ -0,0 +1,892 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
// Do NOT create instances of this class yourself, use the helper method
// commands.add() instead
function Command(specs, description, action, extraInfo) //{{{
{
if (!specs || !action)
return null;
if (!extraInfo)
extraInfo = {};
// convert command name abbreviation specs of the form
// 'shortname[optional-tail]' to short and long versions Eg. 'abc[def]' ->
// 'abc', 'abcdef'
function parseSpecs(specs)
{
// Whoever wrote the following should be ashamed. :(
// Good grief! I have no words... -- djk ;-)
// let shortNames = longNames = names = [];
let names = [];
let longNames = [];
let shortNames = [];
for (let [,spec] in Iterator(specs))
{
let matches = spec.match(/(\w+)\[(\w+)\]/);
if (matches)
{
shortNames.push(matches[1]);
longNames.push(matches[1] + matches[2]);
// order as long1, short1, long2, short2
names.push(matches[1] + matches[2]);
names.push(matches[1]);
}
else
{
longNames.push(spec);
names.push(spec);
}
}
return { names: names, longNames: longNames, shortNames: shortNames };
};
let expandedSpecs = parseSpecs(specs);
this.specs = specs;
this.shortNames = expandedSpecs.shortNames;
this.longNames = expandedSpecs.longNames;
// return the primary command name (the long name of the first spec listed)
this.name = this.longNames[0];
this.names = expandedSpecs.names; // return all command name aliases
this.description = description || "";
this.action = action;
this.argCount = extraInfo.argCount || 0;
this.completer = extraInfo.completer || null;
this.hereDoc = extraInfo.hereDoc || false;
this.options = extraInfo.options || [];
this.bang = extraInfo.bang || false;
this.count = extraInfo.count || false;
this.literal = extraInfo.literal == null ? null : extraInfo.literal;
this.serial = extraInfo.serial;
this.isUserCommand = extraInfo.isUserCommand || false;
this.replacementText = extraInfo.replacementText || null;
};
Command.prototype = {
execute: function (args, bang, count, modifiers)
{
// XXX
bang = !!bang;
count = (count === undefined) ? -1 : count;
modifiers = modifiers || {};
let self = this;
function exec(args)
{
// FIXME: Move to parseCommand?
args = self.parseArgs(args);
if (!args)
return;
args.count = count;
args.bang = bang;
self.action.call(self, args, bang, count, modifiers);
}
if (this.hereDoc)
{
let matches = args.match(/(.*)<<\s*(\S+)$/);
if (matches && matches[2])
{
commandline.inputMultiline(new RegExp("^" + matches[2] + "$", "m"),
function (args) { exec(matches[1] + "\n" + args) });
return;
}
}
exec(args);
},
hasName: function (name)
{
for (let [,spec] in Iterator(this.specs))
{
let fullName = spec.replace(/\[(\w+)]$/, "$1");
let index = spec.indexOf("[");
let min = index == -1 ? fullName.length : index;
if (fullName.indexOf(name) == 0 && name.length >= min)
return true;
}
return false;
},
parseArgs: function (args, complete) commands.parseArgs(args, this.options, this.argCount, false, this.literal, complete)
}; //}}}
function Commands() //{{{
{
////////////////////////////////////////////////////////////////////////////////
////////////////////// PRIVATE SECTION /////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
var exCommands = [];
function parseBool(arg)
{
if (arg == "true" || arg == "1" || arg == "on")
return true;
if (arg == "false" || arg == "0" || arg == "off")
return false;
return NaN;
}
const quoteMap = {
"\n": "n",
"\t": "t"
}
function quote(q, list)
{
let re = RegExp("[" + list + "]", "g");
return function (str) q + String.replace(str, re, function ($0) $0 in quoteMap ? quoteMap[$0] : ("\\" + $0)) + q;
}
const complQuote = { // FIXME
'"': ['"', quote("", '\n\t"\\\\'), '"'],
"'": ["'", quote("", "\\\\'"), "'"],
"": ["", quote("", "\\\\ "), ""]
};
const quoteArg = {
'"': quote('"', '\n\t"\\\\'),
"'": quote("'", "\\\\'"),
"": quote("", "\\\\ ")
}
const ArgType = new Struct("description", "parse");
const argTypes = [
null,
["no arg", function (arg) !arg],
["boolean", parseBool],
["string", function (val) val],
["int", parseInt],
["float", parseFloat],
["list", function (arg) arg && arg.split(/\s*,\s*/)]
].map(function (x) x && ArgType.apply(null, x));
function addCommand(command, isUserCommand, replace)
{
if (!command) // XXX
return false;
if (exCommands.some(function (c) c.hasName(command.name)))
{
if (isUserCommand && replace)
{
commands.removeUserCommand(command.name);
}
else
{
liberator.log("Warning: :" + command.name + " already exists, NOT replacing existing command.", 1);
return false;
}
}
exCommands.push(command);
return true;
}
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// PUBLIC SECTION //////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
liberator.registerObserver("load_completion", function ()
{
completion.setFunctionCompleter(commands.get, [function () ([c.name, c.description] for (c in commands))]);
});
var commandManager = {
// FIXME: remove later, when our option handler is better
OPTION_ANY: 0, // can be given no argument or an argument of any type,
// caller is responsible for parsing the return value
OPTION_NOARG: 1,
OPTION_BOOL: 2,
OPTION_STRING: 3,
OPTION_INT: 4,
OPTION_FLOAT: 5,
OPTION_LIST: 6,
COUNT_NONE: -1,
COUNT_ALL: -2, // :%...
__iterator__: function ()
{
let sorted = exCommands.sort(function (a, b) a.name > b.name);
return util.Array.iterator(sorted);
},
add: function (names, description, action, extra)
{
return addCommand(new Command(names, description, action, extra), false, false);
},
addUserCommand: function (names, description, action, extra, replace)
{
extra = extra || {};
extra.isUserCommand = true;
description = description || "User defined command";
return addCommand(new Command(names, description, action, extra), true, replace);
},
commandToString: function (args)
{
let res = [args.command + (args.bang ? "!" : "")];
function quote(str) quoteArg[/\s/.test(str) ? '"' : ""](str);
for (let [opt, val] in Iterator(args.options || {}))
{
res.push(opt);
if (val != null)
res.push(quote(val));
}
for (let [,arg] in Iterator(args.arguments || []))
res.push(quote(arg));
let str = args.literalArg;
if (str)
res.push(/\n/.test(str) ? "<<EOF\n" + str + "EOF" : str);
return res.join(" ");
},
get: function (name)
{
return exCommands.filter(function (cmd) cmd.hasName(name))[0] || null;
},
getUserCommand: function (name)
{
return exCommands.filter(function (cmd) cmd.isUserCommand && cmd.hasName(name))[0] || null;
},
getUserCommands: function ()
{
return exCommands.filter(function (cmd) cmd.isUserCommand);
},
// in '-quoted strings, only ' and \ itself are escaped
// in "-quoted strings, also ", \n and \t are translated
// in non-quoted strings everything is taken literally apart from "\ " and "\\"
//
// @param str: something like "-x=foo -opt=bar arg1 arg2"
// "options" is an array [name, type, validator, completions] and could look like:
// options = [[["-force"], OPTION_NOARG],
// [["-fullscreen", "-f"], OPTION_BOOL],
// [["-language"], OPTION_STRING, validateFunc, ["perl", "ruby"]],
// [["-speed"], OPTION_INT],
// [["-acceleration"], OPTION_FLOAT],
// [["-accessories"], OPTION_LIST, null, ["foo", "bar"]],
// [["-other"], OPTION_ANY]];
// @param argCount can be:
// "0": no arguments
// "1": exactly one argument
// "+": one or more arguments
// "*": zero or more arguments (default if unspecified)
// "?": zero or one arguments
// @param allowUnknownOptions: -foo won't result in an error, if -foo isn't
// specified in "options"
// TODO: should it handle comments?
parseArgs: function (str, options, argCount, allowUnknownOptions, literal, complete)
{
// returns [count, parsed_argument]
function getNextArg(str)
{
var stringDelimiter = null;
var escapeNext = false;
var arg = "";
outer:
for (let i = 0; i < str.length; i++)
{
inner:
switch (str[i])
{
case '"':
case "'":
if (escapeNext)
{
escapeNext = false;
break;
}
switch (stringDelimiter)
{
case str[i]:
stringDelimiter = null;
continue outer;
case null:
stringDelimiter = str[i];
continue outer;
}
break;
// \ is an escape key for non quoted or "-quoted strings
// for '-quoted strings it is taken literally, apart from \' and \\
case "\\":
if (escapeNext)
{
escapeNext = false;
break;
}
else
{
// in non-quoted strings, only escape "\\" and "\ ", otherwise drop "\\"
if (!stringDelimiter && str[i + 1] != "\\" && str[i + 1] != " ")
continue outer;
// in single quoted strings, only escape "\\" and "\'", otherwise keep "\\"
if (stringDelimiter == "'" && str[i + 1] != "\\" && str[i + 1] != "'")
break;
escapeNext = true;
continue outer;
}
break;
default:
if (stringDelimiter == "'")
{
escapeNext = false;
break;
}
if (escapeNext)
{
escapeNext = false;
switch (str[i])
{
case "n": arg += "\n"; break;
case "t": arg += "\t"; break;
default:
break inner; // this makes "a\fb" -> afb; wanted or should we return ab? --mst
}
continue outer;
}
else if (stringDelimiter != '"' && /\s/.test(str[i]))
{
return [i, arg];
}
break;
}
arg += str[i];
}
// TODO: add parsing of a " comment here:
if (stringDelimiter)
return [str.length, arg, stringDelimiter];
if (escapeNext)
return [str.length, arg, "\\"];
else
return [str.length, arg];
}
if (!options)
options = [];
if (!argCount)
argCount = "*";
var args = []; // parsed options
args.__iterator__ = util.Array.iterator2;
args.string = str; // for access to the unparsed string
args.literalArg = "";
var invalid = false;
// FIXME: best way to specify these requirements?
var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0 || false; // after a -- has been found
var arg = null;
var count = 0; // the length of the argument
var i = 0;
var completeOpts;
// XXX
function matchOpts(arg)
{
// Push possible option matches into completions
if (complete && !onlyArgumentsRemaining)
completeOpts = [[opt[0], opt[0][0]] for ([i, opt] in Iterator(options)) if (!(opt[0][0] in args))];
}
function resetCompletions()
{
completeOpts = null;
args.completeArg = null;
args.completeOpt = null;
args.completeFilter = null;
args.completeStart = i;
args.quote = complQuote[""];
}
if (complete)
{
resetCompletions();
matchOpts("");
args.completeArg = 0;
}
function echoerr(error)
{
if (complete)
complete.message = error;
else
liberator.echoerr(error);
}
outer:
while (i < str.length || complete)
{
// skip whitespace
while (/\s/.test(str[i]) && i < str.length)
i++;
if (i == str.length && !complete)
break;
if (complete)
resetCompletions();
var sub = str.substr(i);
if ((!onlyArgumentsRemaining) && /^--(\s|$)/.test(sub))
{
onlyArgumentsRemaining = true;
i += 2;
continue;
}
var optname = "";
if (!onlyArgumentsRemaining)
{
for (let [,opt] in Iterator(options))
{
for (let [,optname] in Iterator(opt[0]))
{
if (sub.indexOf(optname) == 0)
{
invalid = false;
arg = null;
quote = null;
count = 0;
let sep = sub[optname.length];
if (sep == "=" || /\s/.test(sep) && opt[1] != this.OPTION_NOARG)
{
[count, arg, quote] = getNextArg(sub.substr(optname.length + 1));
// if we add the argument to an option after a space, it MUST not be empty
if (sep != "=" && !quote && arg.length == 0)
arg = null;
count++; // to compensate the "=" character
}
else if (!/\s/.test(sep)) // this isn't really an option as it has trailing characters, parse it as an argument
{
invalid = true;
}
let context = null;
if (!complete && quote)
{
liberator.echoerr("Invalid argument for option " + optname);
return null;
}
if (!invalid)
{
if (complete && count > 0)
{
args.completeStart += optname.length + 1;
args.completeOpt = opt;
args.completeFilter = arg;
args.quote = complQuote[quote] || complQuote[""];
}
let type = argTypes[opt[1]];
if (type && (!complete || arg != null))
{
let orig = arg;
arg = type.parse(arg);
if (arg == null || (typeof arg == "number" && isNaN(arg)))
{
if (!complete || orig != "" || args.completeStart != str.length)
echoerr("Invalid argument for " + type.description + " option: " + optname);
if (complete)
complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
else
return null;
}
}
// we have a validator function
if (typeof opt[2] == "function")
{
if (opt[2].call(this, arg) == false)
{
echoerr("Invalid argument for option: " + optname);
if (complete)
complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
else
return null;
}
}
args[opt[0][0]] = arg; // always use the first name of the option
i += optname.length + count;
if (i == str.length)
break outer;
continue outer;
}
// if it is invalid, just fall through and try the next argument
}
}
}
}
matchOpts(sub);
if (complete)
{
if (argCount == "0" || args.length > 0 && (/[1?]/.test(argCount)))
complete.highlight(i, sub.length, "SPELLCHECK")
}
if (args.length == literal)
{
if (complete)
args.completeArg = args.length;
args.literalArg = sub;
args.push(sub);
args.quote = null;
break;
}
// if not an option, treat this token as an argument
var [count, arg, quote] = getNextArg(sub);
if (complete)
{
args.quote = complQuote[quote] || complQuote[""];
args.completeFilter = arg || "";
}
else if (count == -1)
{
liberator.echoerr("Error parsing arguments: " + arg);
return null;
}
else if (!onlyArgumentsRemaining && /^-/.test(arg))
{
liberator.echoerr("Invalid option: " + arg);
return null;
}
if (arg != null)
args.push(arg);
if (complete)
args.completeArg = args.length - 1;
i += count;
if (count <= 0 || i == str.length)
break;
}
if (complete)
{
if (args.completeOpt)
{
let opt = args.completeOpt;
let context = complete.fork(opt[0][0], args.completeStart);
context.filter = args.completeFilter;
if (typeof opt[3] == "function")
var compl = opt[3](context, args);
else
compl = opt[3] || [];
context.title = [opt[0][0]];
context.quote = args.quote;
context.completions = compl;
}
complete.advance(args.completeStart);
complete.title = ["Options"];
if (completeOpts)
complete.completions = completeOpts;
}
// check for correct number of arguments
if (args.length == 0 && /^[1+]$/.test(argCount) ||
literal != null && /[1+]/.test(argCount) && !/\S/.test(args.literalArg || ""))
{
if (!complete)
{
liberator.echoerr("E471: Argument required");
return null;
}
}
else if (args.length == 1 && (argCount == "0") ||
args.length > 1 && /^[01?]$/.test(argCount))
{
echoerr("E488: Trailing characters");
return null;
}
return args;
},
// return [null, null, null, null, heredoc_tag || false];
// [count, cmd, special, args] = match;
parseCommand: function (str, tag)
{
// remove comments
str.replace(/\s*".*$/, "");
if (tag) // we already have a multiline heredoc construct
{
if (str == tag)
return [null, null, null, null, false];
else
return [null, null, null, str, tag];
}
// 0 - count, 1 - cmd, 2 - special, 3 - args, 4 - heredoc tag
let matches = str.match(/^:*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?))?$/);
//var matches = str.match(/^:*(\d+|%)?([a-zA-Z]+|!)(!)?(?:\s*(.*?)\s*)?$/);
if (!matches)
return [null, null, null, null, null];
let [, count, cmd, special, args, heredoc] = matches;
// parse count
if (count)
count = count == "%" ? this.COUNT_ALL: parseInt(count, 10);
else
count = this.COUNT_NONE;
if (args)
{
tag = args.match(/<<\s*(\w+)\s*$/);
if (tag && tag[1])
heredoc = tag[1];
}
return [count, cmd, !!special, args || "", heredoc];
},
get quoteArg() quoteArg,
removeUserCommand: function (name)
{
exCommands = exCommands.filter(function (cmd) !(cmd.isUserCommand && cmd.hasName(name)));
},
// FIXME: still belong here? Also used for autocommand parameters
replaceTokens: function replaceTokens(str, tokens)
{
return str.replace(/<((?:q-)?)([a-zA-Z]+)?>/g, function (match, quote, token)
{
if (token == "lt") // Don't quote, as in vim (but, why so in vim? You'd think people wouldn't say <q-lt> if they didn't want it)
return "<";
let res = tokens[token];
if (res == undefined) // Ignore anything undefined
res = "<" + token + ">";
if (quote && typeof res != "number")
return quoteArg['"'](res);
return res;
});
}
};
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// COMMANDS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
function userCommand(args, modifiers)
{
let tokens = {
args: this.argCount && args.string,
bang: this.bang && args.bang ? "!" : "",
count: this.count && args.count
};
liberator.execute(commands.replaceTokens(this.replacementText, tokens));
}
// TODO: offer completion.ex?
var completeOptionMap = {
altstyle: "alternateStylesheet", bookmark: "bookmark",
buffer: "buffer", color: "colorScheme", command: "command",
dialog: "dialog", dir: "directory", environment: "environment",
event: "autocmdEvent", file: "file", help: "help",
highlight: "highlightGroup", javascript: "javascript", macro: "macro",
mapping: "userMapping", menu: "menuItem", option: "option",
preference: "preference", search: "search",
shellcmd: "shellCommand", sidebar: "sidebar", url: "url",
usercommand: "userCommand"
};
// TODO: Vim allows commands to be defined without {rep} if there are {attr}s
// specified - useful?
commandManager.add(["com[mand]"],
"List and define commands",
function (args)
{
let cmd = args[0];
if (cmd != null && /\W/.test(cmd))
{
liberator.echoerr("E182: Invalid command name");
return;
}
if (args.literalArg)
{
let nargsOpt = args["-nargs"] || "0";
let bangOpt = "-bang" in args;
let countOpt = "-count" in args;
let completeOpt = args["-complete"];
let completeFunc = null; // default to no completion for user commands
if (completeOpt)
{
let func;
if (/^custom,/.test(completeOpt))
func = completeOpt.replace("custom,", "");
else
func = "completion." + completeOptionMap[completeOpt];
completeFunc = eval(func);
}
if (!commands.addUserCommand(
[cmd],
"User defined command",
userCommand,
{
argCount: nargsOpt,
bang: bangOpt,
count: countOpt,
completer: completeFunc,
replacementText: args.literalArg
},
args.bang)
)
{
liberator.echoerr("E174: Command already exists: add ! to replace it");
}
}
else
{
function completerToString(completer)
{
if (completer)
return [k for ([k, v] in Iterator(completeOptionMap))
if (v == completer.name)][0] || "custom";
else
return "";
}
// TODO: using an array comprehension here generates flakey results across repeated calls
// : perhaps we shouldn't allow options in a list call but just ignore them for now
let cmds = exCommands.filter(function (c) c.isUserCommand && (!cmd || c.name.match("^" + cmd)));
if (cmds.length > 0)
{
let str = template.tabular(["", "Name", "Args", "Range", "Complete", "Definition"], ["padding-right: 2em;"],
([cmd.bang ? "!" : " ",
cmd.name,
cmd.argCount,
cmd.count ? "0c" : "",
completerToString(cmd.completer),
cmd.replacementText || "function () { ... }"]
for each (cmd in cmds)));
commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
}
else
{
liberator.echo("No user-defined commands found");
}
}
},
{
bang: true,
completer: function (context) completion.userCommand(context),
options: [
[["-nargs"], commandManager.OPTION_STRING,
function (arg) /^[01*?+]$/.test(arg), ["0", "1", "*", "?", "+"]],
[["-bang"], commandManager.OPTION_NOARG],
[["-count"], commandManager.OPTION_NOARG],
[["-complete"], commandManager.OPTION_STRING,
function (arg) arg in completeOptionMap || /custom,\w+/.test(arg)]
],
literal: 1,
serial: function () [
{
command: this.name,
bang: true,
// Yeah, this is a bit scary. Perhaps I'll fix it when I'm
// awake.
options: util.Array.assocToObj(
util.map({ argCount: "-nargs", bang: "-bang", count: "-count" },
function ([k, v]) k in cmd && cmd[k] != "0" && [v, typeof cmd[k] == "boolean" ? null : cmd[k]])
.filter(util.identity)),
arguments: [cmd.name],
literalArg: cmd.replacementText
}
for ([k, cmd] in Iterator(exCommands))
if (cmd.isUserCommand && cmd.replacementText)
]
});
commandManager.add(["comc[lear]"],
"Delete all user-defined commands",
function ()
{
commands.getUserCommands().forEach(function (cmd) { commands.removeUserCommand(cmd.name); });
},
{ argCount: "0" });
commandManager.add(["delc[ommand]"],
"Delete the specified user-defined command",
function (args)
{
let name = args[0];
if (commands.get(name))
commands.removeUserCommand(name);
else
liberator.echoerr("E184: No such user-defined command: " + name);
},
{
argCount: "1",
completer: function (context) completion.userCommand(context)
});
//}}}
return commandManager;
}; //}}}
// vim: set fdm=marker sw=4 ts=4 et:

1704
common/content/completion.js Normal file

File diff suppressed because it is too large Load Diff

1128
common/content/editor.js Normal file

File diff suppressed because it is too large Load Diff

10
common/content/eval.js Normal file
View File

@@ -0,0 +1,10 @@
try { __liberator_eval_result = eval(__liberator_eval_string)
}
catch (e)
{
__liberator_eval_error = e;
}
// Important: The eval statement *must* remain on the first line
// in order for line numbering in any errors to remain correct.
//
// vim: set fdm=marker sw=4 ts=4 et:

1694
common/content/events.js Normal file

File diff suppressed because it is too large Load Diff

473
common/content/find.js Normal file
View File

@@ -0,0 +1,473 @@
/***** B/GIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
// TODO: proper backwards search - implement our own component?
// : implement our own highlighter?
// : frameset pages
// : <ESC> should cancel search highlighting in 'incsearch' mode and jump
// back to the presearch page location - can probably use the same
// solution as marks
// : 'linksearch' searches should highlight link matches only
// : changing any search settings should also update the search state including highlighting
// : incremental searches shouldn't permanently update search modifiers
// make sure you only create this object when the "liberator" object is ready
function Search() //{{{
{
////////////////////////////////////////////////////////////////////////////////
////////////////////// PRIVATE SECTION /////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
// FIXME:
//var self = this; // needed for callbacks since "this" is the "liberator" object in a callback
var found = false; // true if the last search was successful
var backwards = false; // currently searching backwards
var searchString = ""; // current search string (without modifiers)
var searchPattern = ""; // current search string (includes modifiers)
var lastSearchPattern = ""; // the last searched pattern (includes modifiers)
var lastSearchString = ""; // the last searched string (without modifiers)
var lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with <esc> this is not set
var caseSensitive = false; // search string is case sensitive
var linksOnly = false; // search is limited to link text only
// Event handlers for search - closure is needed
liberator.registerCallback("change", modes.SEARCH_FORWARD, function (command) { search.searchKeyPressed(command); });
liberator.registerCallback("submit", modes.SEARCH_FORWARD, function (command) { search.searchSubmitted(command); });
liberator.registerCallback("cancel", modes.SEARCH_FORWARD, function () { search.searchCanceled(); });
// TODO: allow advanced myModes in register/triggerCallback
liberator.registerCallback("change", modes.SEARCH_BACKWARD, function (command) { search.searchKeyPressed(command); });
liberator.registerCallback("submit", modes.SEARCH_BACKWARD, function (command) { search.searchSubmitted(command); });
liberator.registerCallback("cancel", modes.SEARCH_BACKWARD, function () { search.searchCanceled(); });
// set searchString, searchPattern, caseSensitive, linksOnly
function processUserPattern(pattern)
{
//// strip off pattern terminator and offset
//if (backwards)
// pattern = pattern.replace(/\?.*/, "");
//else
// pattern = pattern.replace(/\/.*/, "");
searchPattern = pattern;
// links only search - \l wins if both modifiers specified
if (/\\l/.test(pattern))
linksOnly = true;
else if (/\L/.test(pattern))
linksOnly = false;
else if (options["linksearch"])
linksOnly = true;
else
linksOnly = false;
// strip links-only modifiers
pattern = pattern.replace(/(\\)?\\[lL]/g, function ($0, $1) { return $1 ? $0 : ""; });
// case sensitivity - \c wins if both modifiers specified
if (/\c/.test(pattern))
caseSensitive = false;
else if (/\C/.test(pattern))
caseSensitive = true;
else if (options["ignorecase"] && options["smartcase"] && /[A-Z]/.test(pattern))
caseSensitive = true;
else if (options["ignorecase"])
caseSensitive = false;
else
caseSensitive = true;
// strip case-sensitive modifiers
pattern = pattern.replace(/(\\)?\\[cC]/g, function ($0, $1) { return $1 ? $0 : ""; });
// remove any modifer escape \
pattern = pattern.replace(/\\(\\[cClL])/g, "$1");
searchString = pattern;
}
/* Stolen from toolkit.jar in Firefox, for the time being. The private
* methods were unstable, and changed. The new version is not remotely
* compatible with what we do.
* The following only applies to this object, and may not be
* necessary, or accurate, but, just in case:
* The Original Code is mozilla.org viewsource frontend.
*
* The Initial Developer of the Original Code is
* Netscape Communications Corporation.
* Portions created by the Initial Developer are Copyright (C) 2003
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Blake Ross <blake@cs.stanford.edu> (Original Author)
* Masayuki Nakano <masayuki@d-toybox.com>
* Ben Basson <contact@cusser.net>
* Jason Barnabe <jason_barnabe@fastmail.fm>
* Asaf Romano <mano@mozilla.com>
* Ehsan Akhgari <ehsan.akhgari@gmail.com>
* Graeme McCutcheon <graememcc_firefox@graeme-online.co.uk>
*/
var highlightObj = {
search: function (aWord, matchCase)
{
var finder = Components.classes["@mozilla.org/embedcomp/rangefind;1"]
.createInstance()
.QueryInterface(Components.interfaces.nsIFind);
if (matchCase !== undefined)
finder.caseSensitive = matchCase;
var range;
while ((range = finder.Find(aWord,
this.searchRange,
this.startPt,
this.endPt)))
yield range;
},
highlightDoc: function highlightDoc(win, aWord)
{
Array.forEach(win.frames, function (frame) highlightObj.highlightDoc(frame, aWord));
var doc = win.document;
if (!doc || !(doc instanceof HTMLDocument))
return;
if (!aWord)
{
let elems = highlightObj.getSpans(doc);
for (let i = elems.snapshotLength; --i >= 0;)
{
let elem = elems.snapshotItem(i);
let docfrag = doc.createDocumentFragment();
let next = elem.nextSibling;
let parent = elem.parentNode;
let child;
while (child = elem.firstChild)
docfrag.appendChild(child);
parent.removeChild(elem);
parent.insertBefore(docfrag, next);
parent.normalize();
}
return;
}
var baseNode = <span highlight="Search"/>
baseNode = util.xmlToDom(baseNode, window.content.document);
var body = doc.body;
var count = body.childNodes.length;
this.searchRange = doc.createRange();
this.startPt = doc.createRange();
this.endPt = doc.createRange();
this.searchRange.setStart(body, 0);
this.searchRange.setEnd(body, count);
this.startPt.setStart(body, 0);
this.startPt.setEnd(body, 0);
this.endPt.setStart(body, count);
this.endPt.setEnd(body, count);
liberator.interrupted = false;
let n = 0;
for (let retRange in this.search(aWord, caseSensitive))
{
// Highlight
var nodeSurround = baseNode.cloneNode(true);
var node = this.highlight(retRange, nodeSurround);
this.startPt = node.ownerDocument.createRange();
this.startPt.setStart(node, node.childNodes.length);
this.startPt.setEnd(node, node.childNodes.length);
if (n++ % 20 == 0)
liberator.threadYield(true);
if (liberator.interrupted)
break;
}
},
highlight: function highlight(aRange, aNode)
{
var startContainer = aRange.startContainer;
var startOffset = aRange.startOffset;
var endOffset = aRange.endOffset;
var docfrag = aRange.extractContents();
var before = startContainer.splitText(startOffset);
var parent = before.parentNode;
aNode.appendChild(docfrag);
parent.insertBefore(aNode, before);
return aNode;
},
getSpans: function (doc) buffer.evaluateXPath("//*[@liberator:highlight='Search']", doc)
};
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// OPTIONS /////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
options.add(["hlsearch", "hls"],
"Highlight previous search pattern matches",
"boolean", "false",
{
setter: function (value)
{
if (value)
search.highlight();
else
search.clear();
return value;
}
});
options.add(["ignorecase", "ic"],
"Ignore case in search patterns",
"boolean", true);
options.add(["incsearch", "is"],
"Show where the search pattern matches as it is typed",
"boolean", true);
options.add(["linksearch", "lks"],
"Limit the search to hyperlink text",
"boolean", false);
options.add(["smartcase", "scs"],
"Override the 'ignorecase' option if the pattern contains uppercase characters",
"boolean", true);
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// MAPPINGS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
var myModes = config.browserModes;
myModes = myModes.concat([modes.CARET]);
mappings.add(myModes,
["/"], "Search forward for a pattern",
function () { search.openSearchDialog(modes.SEARCH_FORWARD); });
mappings.add(myModes,
["?"], "Search backwards for a pattern",
function () { search.openSearchDialog(modes.SEARCH_BACKWARD); });
mappings.add(myModes,
["n"], "Find next",
function () { search.findAgain(false); });
mappings.add(myModes,
["N"], "Find previous",
function () { search.findAgain(true); });
mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["*"],
"Find word under cursor",
function ()
{
search.searchSubmitted(buffer.getCurrentWord(), false);
search.findAgain();
});
mappings.add(myModes.concat([modes.CARET, modes.TEXTAREA]), ["#"],
"Find word under cursor backwards",
function ()
{
search.searchSubmitted(buffer.getCurrentWord(), true);
search.findAgain();
});
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// COMMANDS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
commands.add(["noh[lsearch]"],
"Remove the search highlighting",
function () { search.clear(); },
{ argCount: "0" });
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// PUBLIC SECTION //////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
return {
// Called when the search dialog is asked for
// If you omit "mode", it will default to forward searching
openSearchDialog: function (mode)
{
if (mode == modes.SEARCH_BACKWARD)
{
commandline.open("?", "", modes.SEARCH_BACKWARD);
backwards = true;
}
else
{
commandline.open("/", "", modes.SEARCH_FORWARD);
backwards = false;
}
// TODO: focus the top of the currently visible screen
},
// Finds text in a page
// TODO: backwards seems impossible i fear :(
find: function (str, backwards)
{
var fastFind = getBrowser().fastFind;
processUserPattern(str);
fastFind.caseSensitive = caseSensitive;
found = fastFind.find(searchString, linksOnly) != Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND;
if (!found)
setTimeout(function () { liberator.echoerr("E486: Pattern not found: " + searchPattern); }, 0);
return found;
},
// Called when the current search needs to be repeated
findAgain: function (reverse)
{
// this hack is needed to make n/N work with the correct string, if
// we typed /foo<esc> after the original search. Since searchString is
// readonly we have to call find() again to update it.
if (getBrowser().fastFind.searchString != lastSearchString)
this.find(lastSearchString, false);
var up = reverse ? !lastSearchBackwards : lastSearchBackwards;
var result = getBrowser().fastFind.findAgain(up, linksOnly);
if (result == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND)
{
liberator.echoerr("E486: Pattern not found: " + lastSearchPattern);
}
else if (result == Components.interfaces.nsITypeAheadFind.FIND_WRAPPED)
{
// hack needed, because wrapping causes a "scroll" event which clears
// our command line
setTimeout(function () {
if (up)
commandline.echo("search hit TOP, continuing at BOTTOM",
commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
else
commandline.echo("search hit BOTTOM, continuing at TOP",
commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
}, 0);
}
else
{
liberator.echo((up ? "?" : "/") + lastSearchPattern, null, commandline.FORCE_SINGLELINE);
if (options["hlsearch"])
this.highlight(lastSearchString);
}
},
// Called when the user types a key in the search dialog. Triggers a find attempt if 'incsearch' is set
searchKeyPressed: function (command)
{
if (options["incsearch"])
this.find(command, backwards);
},
// Called when the enter key is pressed to trigger a search
// use forcedBackward if you call this function directly
searchSubmitted: function (command, forcedBackward)
{
if (typeof forcedBackward === "boolean")
backwards = forcedBackward;
// use the last pattern if none specified
if (!command)
command = lastSearchPattern;
if (!options["incsearch"] || !found)
{
this.clear();
this.find(command, backwards);
}
lastSearchBackwards = backwards;
//lastSearchPattern = command.replace(backwards ? /\?.*/ : /\/.*/, ""); // XXX
lastSearchPattern = command;
lastSearchString = searchString;
// TODO: move to find() when reverse incremental searching is kludged in
// need to find again for reverse searching
if (backwards)
setTimeout(function () { search.findAgain(false); }, 0);
if (options["hlsearch"])
this.highlight(searchString);
modes.reset();
},
// Called when the search is canceled - for example if someone presses
// escape while typing a search
searchCanceled: function ()
{
this.clear();
// TODO: code to reposition the document to the place before search started
},
// FIXME: thunderbird incompatible
// this is not dependent on the value of 'hlsearch'
highlight: function (text)
{
if (config.name == "Muttator")
return;
// already highlighted?
if (highlightObj.getSpans(content.document).snapshotLength > 0)
return;
if (!text)
text = lastSearchString;
highlightObj.highlightDoc(window.content, text);
// recreate selection since _highlightDoc collapses the selection backwards
getBrowser().fastFind.findAgain(false, linksOnly);
// TODO: remove highlighting from non-link matches (HTML - A/AREA with href attribute; XML - Xlink [type="simple"])
},
clear: function ()
{
highlightObj.highlightDoc(window.content);
// need to manually collapse the selection if the document is not
// highlighted
getBrowser().fastFind.collapseSelection();
}
};
//}}}
}; //}}}
// vim: set fdm=marker sw=4 ts=4 et:

149
common/content/help.css Normal file
View File

@@ -0,0 +1,149 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
div.main {
font-family: -moz-fixed;
white-space: -moz-pre-wrap;
width: 800px;
margin-left: auto;
margin-right: auto;
}
h1 {
text-align: center;
}
p.tagline {
text-align: center;
font-weight: bold;
}
table.vimperator {
border-width: 1px;
border-style: dotted;
border-color: gray;
/*margin-bottom: 2em; /* FIXME: just a quick hack until we have proper pages */
}
table.vimperator td {
border: none;
padding: 3px;
}
tr.separator {
height: 10px;
}
hr {
height: 1px;
background-color: white;
border-style: none;
margin-top: 0;
margin-bottom: 0;
}
td.taglist {
text-align: right;
vertical-align: top;
border-spacing: 13px 10px;
}
td.taglist td {
width: 100px;
padding: 3px 0px;
}
tr.taglist code, td.usage code {
margin: 0px 2px;
}
td.usage code {
white-space: nowrap;
}
td.taglist code {
margin-left: 2em;
}
code.tag {
font-weight: bold;
color: rgb(255, 0, 255); /* magenta */
padding-left: 5px;
}
tr.description {
margin-bottom: 4px;
}
table.commands {
background-color: rgb(250, 240, 230);
color: black;
}
table.mappings {
background-color: rgb(230, 240, 250);
color: black;
}
table.options {
background-color: rgb(240, 250, 230);
color: black;
}
fieldset.paypal {
border: none;
}
.argument {
color: #6A97D4;
}
.command {
font-weight: bold;
color: #632610;
}
.mapping {
font-weight: bold;
color: #102663;
}
.option {
font-weight: bold;
color: #106326;
}
.code {
color: #108826;
}
.shorthelp {
font-weight: bold;
}
.version {
position: absolute;
top: 10px;
right: 2%;
color: #C0C0C0;
text-align: right;
}
.warning {
font-weight: bold;
color: red;
}
/* vim: set fdm=marker sw=4 ts=4 et: */

811
common/content/hints.js Normal file
View File

@@ -0,0 +1,811 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
function Hints() //{{{
{
////////////////////////////////////////////////////////////////////////////////
////////////////////// PRIVATE SECTION /////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
const ELEM = 0, TEXT = 1, SPAN = 2, IMGSPAN = 3;
var myModes = config.browserModes;
var hintMode;
var submode = ""; // used for extended mode, can be "o", "t", "y", etc.
var hintString = ""; // the typed string part of the hint is in this string
var hintNumber = 0; // only the numerical part of the hint
var usedTabKey = false; // when we used <Tab> to select an element
var prevInput = ""; // record previous user input type, "text" || "number"
// hints[] = [elem, text, span, imgspan, elem.style.backgroundColor, elem.style.color]
var pageHints = [];
var validHints = []; // store the indices of the "hints" array with valid elements
var escapeNumbers = false; // escape mode for numbers. true -> treated as hint-text
var activeTimeout = null; // needed for hinttimeout > 0
var canUpdate = false;
// keep track of the documents which we generated the hints for
// docs = { doc: document, start: start_index in hints[], end: end_index in hints[] }
var docs = [];
const Mode = new Struct("prompt", "action", "tags");
Mode.defaultValue("tags", function () function () options.hinttags);
function extended() options.extendedhinttags;
const hintModes = {
";": Mode("Focus hint", function (elem) buffer.focusElement(elem), extended),
a: Mode("Save hint with prompt", function (elem) buffer.saveLink(elem, false)),
s: Mode("Save hint", function (elem) buffer.saveLink(elem, true)),
o: Mode("Follow hint", function (elem) buffer.followLink(elem, liberator.CURRENT_TAB)),
t: Mode("Follow hint in a new tab", function (elem) buffer.followLink(elem, liberator.NEW_TAB)),
b: Mode("Follow hint in a background tab", function (elem) buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB)),
v: Mode("View hint source", function (elem, loc) buffer.viewSource(loc, false), extended),
V: Mode("View hint source", function (elem, loc) buffer.viewSource(loc, true), extended),
w: Mode("Follow hint in a new window", function (elem) buffer.followLink(elem, liberator.NEW_WINDOW), extended),
"?": Mode("Show information for hint", function (elem) buffer.showElementInfo(elem), extended),
O: Mode("Open location based on hint", function (elem, loc) commandline.open(":", "open " + loc, modes.EX)),
T: Mode("Open new tab based on hint", function (elem, loc) commandline.open(":", "tabopen " + loc, modes.EX)),
W: Mode("Open new window based on hint", function (elem, loc) commandline.open(":", "winopen " + loc, modes.EX)),
y: Mode("Yank hint location", function (elem, loc) util.copyToClipboard(loc, true)),
Y: Mode("Yank hint description", function (elem) util.copyToClipboard(elem.textContent || "", true), extended),
};
// reset all important variables
function reset()
{
statusline.updateInputBuffer("");
hintString = "";
hintNumber = 0;
usedTabKey = false;
prevInput = "";
pageHints = [];
validHints = [];
canUpdate = false;
docs = [];
escapeNumbers = false;
if (activeTimeout)
clearTimeout(activeTimeout);
activeTimeout = null;
}
function updateStatusline()
{
statusline.updateInputBuffer((escapeNumbers ? mappings.getMapLeader() : "") + (hintNumber || ""));
}
function generate(win)
{
if (!win)
win = window.content;
var doc = win.document;
var height = win.innerHeight;
var width = win.innerWidth;
var scrollX = doc.defaultView.scrollX;
var scrollY = doc.defaultView.scrollY;
var baseNodeAbsolute = util.xmlToDom(<span highlight="Hint"/>, doc);
var elem, tagname, text, span, rect;
var res = buffer.evaluateXPath(hintMode.tags(), doc, null, true);
var fragment = util.xmlToDom(<div highlight="hints"/>, doc);
var start = pageHints.length;
for (let elem in res)
{
// TODO: for iframes, this calculation is wrong
rect = elem.getBoundingClientRect();
if (!rect || rect.top > height || rect.bottom < 0 || rect.left > width || rect.right < 0)
continue;
rect = elem.getClientRects()[0];
if (!rect)
continue;
var computedStyle = doc.defaultView.getComputedStyle(elem, null);
if (computedStyle.getPropertyValue("visibility") == "hidden" || computedStyle.getPropertyValue("display") == "none")
continue;
// TODO: mozilla docs recommend localName instead of tagName
tagname = elem.tagName.toLowerCase();
if (tagname == "input" || tagname == "textarea")
text = elem.value;
else if (tagname == "select")
{
if (elem.selectedIndex >= 0)
text = elem.item(elem.selectedIndex).text;
else
text = "";
}
else
text = elem.textContent.toLowerCase();
span = baseNodeAbsolute.cloneNode(true);
span.style.left = (rect.left + scrollX) + "px";
span.style.top = (rect.top + scrollY) + "px";
fragment.appendChild(span);
pageHints.push([elem, text, span, null, elem.style.backgroundColor, elem.style.color]);
}
if (doc.body)
{
doc.body.appendChild(fragment);
docs.push({ doc: doc, start: start, end: pageHints.length - 1 });
}
// also generate hints for frames
Array.forEach(win.frames, function (frame) { generate(frame); });
return true;
}
// TODO: make it aware of imgspans
function showActiveHint(newID, oldID)
{
var oldElem = validHints[oldID - 1];
if (oldElem)
setClass(oldElem, false);
var newElem = validHints[newID - 1];
if (newElem)
setClass(newElem, true);
}
function setClass(elem, active)
{
let prefix = (elem.getAttributeNS(NS.uri, "class") || "") + " ";
if (active)
elem.setAttributeNS(NS.uri, "highlight", prefix + "HintActive");
else
elem.setAttributeNS(NS.uri, "highlight", prefix + "HintElem");
}
function showHints()
{
let elem, tagname, text, rect, span, imgspan;
let hintnum = 1;
let validHint = hintMatcher(hintString.toLowerCase());
let activeHint = hintNumber || 1;
validHints = [];
for (let [,{ doc: doc, start: start, end: end }] in Iterator(docs))
{
let scrollX = doc.defaultView.scrollX;
let scrollY = doc.defaultView.scrollY;
inner:
for (let i in (util.interruptableRange(start, end + 1, 500)))
{
let hint = pageHints[i];
[elem, text, span, imgspan] = hint;
if (!validHint(text))
{
span.style.display = "none";
if (imgspan)
imgspan.style.display = "none";
elem.removeAttributeNS(NS.uri, "highlight");
continue inner;
}
if (text == "" && elem.firstChild && elem.firstChild.tagName == "IMG")
{
if (!imgspan)
{
rect = elem.firstChild.getBoundingClientRect();
if (!rect)
continue;
imgspan = util.xmlToDom(<span highlight="Hint"/>, doc);
imgspan.setAttributeNS(NS.uri, "class", "HintImage");
imgspan.style.left = (rect.left + scrollX) + "px";
imgspan.style.top = (rect.top + scrollY) + "px";
imgspan.style.width = (rect.right - rect.left) + "px";
imgspan.style.height = (rect.bottom - rect.top) + "px";
hint[IMGSPAN] = imgspan;
span.parentNode.appendChild(imgspan);
}
setClass(imgspan, activeHint == hintnum)
}
span.setAttribute("number", hintnum++);
if (imgspan)
imgspan.setAttribute("number", hintnum);
else
setClass(elem, activeHint == hintnum);
validHints.push(elem);
}
}
if (options.usermode)
{
let css = [];
// FIXME: Broken for imgspans.
for (let [,{ doc: doc }] in Iterator(docs))
{
for (let elem in buffer.evaluateXPath("//*[@liberator:highlight and @number]", doc))
{
let group = elem.getAttributeNS(NS.uri, "highlight");
css.push(highlight.selector(group) + "[number='" + elem.getAttribute("number") + "'] { " + elem.style.cssText + " }");
}
}
styles.addSheet("hint-positions", "*", css.join("\n"), true, true);
}
return true;
}
function removeHints(timeout)
{
var firstElem = validHints[0] || null;
for (let [,{ doc: doc, start: start, end: end }] in Iterator(docs))
{
for (let elem in buffer.evaluateXPath("//*[@liberator:highlight='hints']", doc))
elem.parentNode.removeChild(elem);
for (let i in util.range(start, end + 1))
{
let hint = pageHints[i];
if (!timeout || hint[ELEM] != firstElem)
hint[ELEM].removeAttributeNS(NS.uri, "highlight");
}
// animate the disappearance of the first hint
if (timeout && firstElem)
{
// USE THIS FOR MAKING THE SELECTED ELEM RED
// firstElem.style.backgroundColor = "red";
// firstElem.style.color = "white";
// setTimeout(function () {
// firstElem.style.backgroundColor = firstElemBgColor;
// firstElem.style.color = firstElemColor;
// }, 200);
// OR USE THIS FOR BLINKING:
// var counter = 0;
// var id = setInterval(function () {
// firstElem.style.backgroundColor = "red";
// if (counter % 2 == 0)
// firstElem.style.backgroundColor = "yellow";
// else
// firstElem.style.backgroundColor = "#88FF00";
//
// if (counter++ >= 2)
// {
// firstElem.style.backgroundColor = firstElemBgColor;
// firstElem.style.color = firstElemColor;
// clearTimeout(id);
// }
// }, 100);
setTimeout(function () { firstElem.removeAttributeNS(NS.uri, "highlight") }, timeout);
}
}
styles.removeSheet("hint-positions", null, null, null, true);
reset();
}
function processHints(followFirst)
{
if (validHints.length == 0)
{
liberator.beep();
return false;
}
if (options["followhints"] > 0)
{
if (!followFirst)
return false; // no return hit; don't examine uniqueness
// OK. return hit. But there's more than one hint, and
// there's no tab-selected current link. Do not follow in mode 2
if (options["followhints"] == 2 && validHints.length > 1 && !hintNumber)
return liberator.beep();
}
if (!followFirst)
{
let firstHref = validHints[0].getAttribute("href") || null;
if (firstHref)
{
if (validHints.some(function (e) e.getAttribute("href") != firstHref))
return false;
}
else if (validHints.length > 1)
{
return false;
}
}
var timeout = followFirst || events.feedingKeys ? 0 : 500;
var activeIndex = (hintNumber ? hintNumber - 1 : 0);
var elem = validHints[activeIndex];
removeHints(timeout);
if (timeout == 0)
// force a possible mode change, based on wheter an input field has focus
events.onFocusChange();
setTimeout(function () {
if (modes.extended & modes.HINTS)
modes.reset();
hintMode.action(elem, elem.href || "");
}, timeout);
return true;
}
function onInput (event)
{
prevInput = "text";
// clear any timeout which might be active after pressing a number
if (activeTimeout)
{
clearTimeout(activeTimeout);
activeTimeout = null;
}
hintNumber = 0;
hintString = commandline.getCommand();
updateStatusline();
showHints();
if (validHints.length == 1)
processHints(false);
}
function hintMatcher(hintString) //{{{
{
function containsMatcher(hintString) //{{{
{
var tokens = hintString.split(/ +/);
return function (linkText) tokens.every(function (token) linkText.indexOf(token) >= 0);
} //}}}
function wordStartsWithMatcher(hintString, allowWordOverleaping) //{{{
{
var hintStrings = hintString.split(/ +/);
var wordSplitRegex = new RegExp(options["wordseparators"]);
// What the **** does this do? --Kris
function charsAtBeginningOfWords(chars, words, allowWordOverleaping)
{
var charIdx = 0;
var numMatchedWords = 0;
for (let [,word] in Iterator(words))
{
if (word.length == 0)
continue;
let wcIdx = 0;
// Check if the current word matches same characters as the previous word.
// Each already matched word has matched at least one character.
if (charIdx > numMatchedWords)
{
let matchingStarted = false;
for (let i in util.range(numMatchedWords, charIdx))
{
if (chars[i] == word[wcIdx])
{
matchingStarted = true;
wcIdx++;
}
else if (matchingStarted)
{
wcIdx = 0;
break;
}
}
}
// the current word matches same characters as the previous word
var prevCharIdx;
if (wcIdx > 0)
{
prevCharIdx = charIdx;
// now check if it matches additional characters
for (; wcIdx < word.length && charIdx < chars.length; wcIdx++, charIdx++)
{
if (word[wcIdx] != chars[charIdx])
break;
}
// the word doesn't match additional characters, now check if the
// already matched characters are equal to the next characters for matching,
// if yes, then consume them
if (prevCharIdx == charIdx)
{
for (let i = 0; i < wcIdx && charIdx < chars.length; i++, charIdx++)
{
if (word[i] != chars[charIdx])
break;
}
}
numMatchedWords++;
}
// the current word doesn't match same characters as the previous word, just
// try to match the next characters
else
{
prevCharIdx = charIdx;
for (let i = 0; i < word.length && charIdx < chars.length; i++, charIdx++)
{
if (word[i] != chars[charIdx])
break;
}
if (prevCharIdx == charIdx)
{
if (!allowWordOverleaping)
return false;
}
else
numMatchedWords++;
}
if (charIdx == chars.length)
return true;
}
return (charIdx == chars.length);
}
function stringsAtBeginningOfWords(strings, words, allowWordOverleaping)
{
var strIdx = 0;
for (let [,word] in Iterator(words))
{
if (word.length == 0)
continue;
let str = strings[strIdx];
if (str.length == 0 || word.indexOf(str) == 0)
strIdx++;
else if (!allowWordOverleaping)
return false;
if (strIdx == strings.length)
return true;
}
for (; strIdx < strings.length; strIdx++)
{
if (strings[strIdx].length != 0)
return false;
}
return true;
}
function wordStartsWith(linkText)
{
if (hintStrings.length == 1 && hintStrings[0].length == 0)
return true;
let words = linkText.split(wordSplitRegex).map(String.toLowerCase);
if (hintStrings.length == 1)
return charsAtBeginningOfWords(hintStrings[0], words, allowWordOverleaping);
else
return stringsAtBeginningOfWords(hintStrings, words, allowWordOverleaping);
}
return wordStartsWith;
} //}}}
var hintMatching = options["hintmatching"];
switch (hintMatching)
{
case "contains" : return containsMatcher(hintString);
case "wordstartswith": return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ true);
case "firstletters" : return wordStartsWithMatcher(hintString, /*allowWordOverleaping=*/ false);
case "custom" : return liberator.plugins.customHintMatcher(hintString);
default : liberator.echoerr("Invalid hintmatching type: " + hintMatching);
}
return null;
} //}}}
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// OPTIONS /////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
const DEFAULT_HINTTAGS = "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
"//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
"//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
"//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | //xhtml:button | //xhtml:select";
options.add(["extendedhinttags", "eht"],
"XPath string of hintable elements activated by ';'",
"string", DEFAULT_HINTTAGS);
options.add(["hinttags", "ht"],
"XPath string of hintable elements activated by 'f' and 'F'",
"string", DEFAULT_HINTTAGS);
options.add(["hinttimeout", "hto"],
"Timeout before automatically following a non-unique numerical hint",
"number", 0,
{ validator: function (value) value >= 0 });
options.add(["followhints", "fh"],
// FIXME: this description isn't very clear but I can't think of a
// better one right now.
"Change the behaviour of <Return> in hint mode",
"number", 0,
{
completer: function () [
["0", "Follow the first hint as soon as typed text uniquely identifies it. Follow the selected hint on [m]<Return>[m]."],
["1", "Follow the selected hint on [m]<Return>[m]."],
["2", "Follow the selected hint on [m]<Return>[m] only it's been [m]<Tab>[m]-selected."]
],
validator: function (value) Option.validateCompleter
});
options.add(["hintmatching", "hm"],
"How links are matched",
"string", "contains",
{
completer: function (filter)
{
return [[m, ""] for each (m in ["contains", "wordstartswith", "firstletters", "custom"])];
},
validator: Option.validateCompleter
});
options.add(["wordseparators", "wsp"],
"How words are split for hintmatching",
"string", '[.,!?:;/"^$%&?()[\\]{}<>#*+|=~ _-]');
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// MAPPINGS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
mappings.add(myModes, ["f"],
"Start QuickHint mode",
function () { hints.show("o"); });
mappings.add(myModes, ["F"],
"Start QuickHint mode, but open link in a new tab",
function () { hints.show("t"); });
mappings.add(myModes, [";"],
"Start an extended hint mode",
function (arg) { hints.show(arg); },
{ flags: Mappings.flags.ARGUMENT });
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// PUBLIC SECTION //////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
return {
addMode: function (mode)
{
hintModes[mode] = Mode.apply(Mode, Array.slice(arguments, 1));
},
show: function (minor, filter, win)
{
hintMode = hintModes[minor];
if (!hintMode)
{
liberator.beep();
return;
}
commandline.input(hintMode.prompt + ":", null, { onChange: onInput });
modes.extended = modes.HINTS;
submode = minor;
hintString = filter || "";
hintNumber = 0;
usedTab = false;
prevInput = "";
canUpdate = false;
generate(win);
// get all keys from the input queue
liberator.threadYield(true);
canUpdate = true;
showHints();
if (validHints.length == 0)
{
liberator.beep();
modes.reset();
return false;
}
else if (validHints.length == 1)
{
processHints(true);
return false;
}
else // still hints visible
return true;
},
hide: function ()
{
removeHints(0);
},
onEvent: function (event)
{
var key = events.toString(event);
var followFirst = false;
// clear any timeout which might be active after pressing a number
if (activeTimeout)
{
clearTimeout(activeTimeout);
activeTimeout = null;
}
switch (key)
{
case "<Return>":
followFirst = true;
break;
case "<Tab>":
case "<S-Tab>":
usedTabKey = true;
if (hintNumber == 0)
hintNumber = 1;
var oldID = hintNumber;
if (key == "<Tab>")
{
if (++hintNumber > validHints.length)
hintNumber = 1;
}
else
{
if (--hintNumber < 1)
hintNumber = validHints.length;
}
showActiveHint(hintNumber, oldID);
updateStatusline();
return;
case "<BS>":
if (hintNumber > 0 && !usedTabKey)
{
hintNumber = Math.floor(hintNumber / 10);
if (hintNumber == 0)
prevInput = "text";
}
else
{
usedTabKey = false;
hintNumber = 0;
liberator.beep();
return;
}
break;
case mappings.getMapLeader():
escapeNumbers = !escapeNumbers;
if (escapeNumbers && usedTabKey) // hintNumber not used normally, but someone may wants to toggle
hintNumber = 0; // <tab>s ? reset. Prevent to show numbers not entered.
updateStatusline();
return;
default:
if (/^\d$/.test(key))
{
// FIXME: Kludge.
if (escapeNumbers)
{
let cmdline = document.getElementById("liberator-commandline-command");
let start = cmdline.selectionStart;
let end = cmdline.selectionEnd;
cmdline.value = cmdline.value.substr(0, start) + key + cmdline.value.substr(start);
cmdline.selectionStart = start + 1;
cmdline.selectionEnd = end + 1;
return;
}
prevInput = "number";
var oldHintNumber = hintNumber;
if (hintNumber == 0 || usedTabKey)
{
usedTabKey = false;
hintNumber = parseInt(key, 10);
}
else
hintNumber = (hintNumber * 10) + parseInt(key, 10);
updateStatusline();
if (!canUpdate)
return;
if (docs.length == 0)
{
generate();
showHints();
}
showActiveHint(hintNumber, oldHintNumber || 1);
if (hintNumber == 0 || hintNumber > validHints.length)
{
liberator.beep();
return;
}
// if we write a numeric part like 3, but we have 45 hints, only follow
// the hint after a timeout, as the user might have wanted to follow link 34
if (hintNumber > 0 && hintNumber * 10 <= validHints.length)
{
var timeout = options["hinttimeout"];
if (timeout > 0)
activeTimeout = setTimeout(function () { processHints(true); }, timeout);
return false;
}
// we have a unique hint
processHints(true);
return;
}
}
updateStatusline();
if (canUpdate)
{
if (docs.length == 0 && hintString.length > 0)
generate();
showHints();
processHints(followFirst);
}
}
};
// FIXME: add resize support
// window.addEventListener("resize", onResize, null);
// function onResize(event)
// {
// if (event)
// doc = event.originalTarget;
// else
// doc = window.content.document;
// }
//}}}
}; //}}}
// vim: set fdm=marker sw=4 ts=4 et:

966
common/content/io.js Normal file
View File

@@ -0,0 +1,966 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Code based on venkman
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
plugins.contexts = {};
function Script(file)
{
let self = plugins.contexts[file.path]
if (self)
{
if (self.onUnload)
self.onUnload();
return self;
}
plugins.contexts[file.path] = this;
this.NAME = file.leafName.replace(/\..*/, "").replace(/-([a-z])/, function (_0, _1) _1.toUpperCase());
this.PATH = file.path;
this.__context__ = this;
// This belongs elsewhere
for (let [,dir] in Iterator(io.getRuntimeDirectories("plugin")))
{
if (dir.contains(file, false))
plugins[name] = this.NAME;
}
}
Script.prototype = plugins;
// TODO: why are we passing around strings rather than file objects?
function IO() //{{{
{
////////////////////////////////////////////////////////////////////////////////
////////////////////// PRIVATE SECTION /////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
const WINDOWS = liberator.has("Win32");
const EXTENSION_NAME = config.name.toLowerCase(); // "vimperator" or "muttator"
const ioService = Components.classes['@mozilla.org/network/io-service;1']
.getService(Components.interfaces.nsIIOService);
const environmentService = Components.classes["@mozilla.org/process/environment;1"]
.getService(Components.interfaces.nsIEnvironment);
const directoryService = Components.classes["@mozilla.org/file/directory_service;1"]
.getService(Components.interfaces.nsIProperties);
const downloadManager = Components.classes["@mozilla.org/download-manager;1"]
.createInstance(Components.interfaces.nsIDownloadManager);
var processDir = directoryService.get("CurWorkD", Components.interfaces.nsIFile);
var cwd = processDir;
var oldcwd = null;
var lastRunCommand = ""; // updated whenever the users runs a command with :!
var scriptNames = [];
// default option values
var cdpath = "," + (environmentService.get("CDPATH").replace(/[:;]/g, ",") || ",");
var runtimepath = "~/" + (WINDOWS ? "" : ".") + EXTENSION_NAME;
var shell, shellcmdflag;
if (WINDOWS)
{
shell = "cmd.exe";
// TODO: setting 'shell' to "something containing sh" updates
// 'shellcmdflag' appropriately at startup on Windows in Vim
shellcmdflag = "/c";
}
else
{
shell = environmentService.get("SHELL") || "sh";
shellcmdflag = "-c";
}
function expandPathList(list) list.split(",").map(io.expandPath).join(",")
function getPathsFromPathList(list)
{
if (!list)
return [];
else
// empty list item means the current directory
return list.replace(/,$/, "")
.split(",")
.map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir);
}
function replacePathSep(path)
{
if (WINDOWS)
return path.replace("/", "\\");
return path;
}
function joinPaths(head, tail)
{
let path = ioManager.getFile(head);
path.appendRelativePath(ioManager.expandPath(tail)); // FIXME: should only expand env vars and normalise path separators
return path;
}
function isAbsolutePath(path)
{
try
{
Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile)
.initWithPath(path);
return true;
}
catch (e)
{
return false;
}
}
var downloadListener = {
onDownloadStateChange: function (state, download)
{
if (download.state == downloadManager.DOWNLOAD_FINISHED)
{
let url = download.source.spec;
let title = download.displayName;
let file = download.targetFile.path;
let size = download.size;
liberator.echomsg("Download of " + title + " to " + file + " finished", 1);
autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
}
},
onStateChange: function () {},
onProgressChange: function () {},
onSecurityChange: function () {}
};
downloadManager.addListener(downloadListener);
liberator.registerObserver("shutdown", function () {
downloadManager.removeListener(downloadListener);
});
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// OPTIONS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
options.add(["cdpath", "cd"],
"List of directories searched when executing :cd",
"stringlist", cdpath,
{ setter: function (value) expandPathList(value) });
options.add(["runtimepath", "rtp"],
"List of directories searched for runtime files",
"stringlist", runtimepath,
{ setter: function (value) expandPathList(value) });
options.add(["shell", "sh"],
"Shell to use for executing :! and :run commands",
"string", shell,
{ setter: function (value) io.expandPath(value) });
options.add(["shellcmdflag", "shcf"],
"Flag passed to shell when executing :! and :run commands",
"string", shellcmdflag);
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// COMMANDS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
commands.add(["cd", "chd[ir]"],
"Change the current directory",
function (args)
{
args = args.literalArg;
if (!args)
{
args = "~";
}
else if (args == "-")
{
if (oldcwd)
{
args = oldcwd.path;
}
else
{
liberator.echoerr("E186: No previous directory");
return;
}
}
args = io.expandPath(args);
// go directly to an absolute path or look for a relative path
// match in 'cdpath'
// TODO: handle ../ and ./ paths
if (isAbsolutePath(args))
{
if (io.setCurrentDirectory(args))
liberator.echo(io.getCurrentDirectory().path);
}
else
{
let dirs = getPathsFromPathList(options["cdpath"]);
let found = false;
for (let [,dir] in Iterator(dirs))
{
dir = joinPaths(dir, args);
if (dir.exists() && dir.isDirectory() && dir.isReadable())
{
io.setCurrentDirectory(dir.path);
liberator.echo(io.getCurrentDirectory().path);
found = true;
break;
}
}
if (!found)
{
liberator.echoerr("E344: Can't find directory \"" + args + "\" in cdpath"
+ "\n"
+ "E472: Command failed");
}
}
},
{
argCount: "?",
completer: function (context) completion.directory(context, true),
literal: 0
});
// NOTE: this command is only used in :source
commands.add(["fini[sh]"],
"Stop sourcing a script file",
function () { liberator.echoerr("E168: :finish used outside of a sourced file"); },
{ argCount: "0" });
commands.add(["pw[d]"],
"Print the current directory name",
function () { liberator.echo(io.getCurrentDirectory().path); },
{ argCount: "0" });
// "mkv[imperatorrc]" or "mkm[uttatorrc]"
commands.add([EXTENSION_NAME.replace(/(.)(.*)/, "mk$1[$2rc]")],
"Write current key mappings and changed options to the config file",
function (args)
{
// TODO: "E172: Only one file name allowed"
let filename = args[0] || "~/" + (WINDOWS ? "_" : ".") + EXTENSION_NAME + "rc";
let file = io.getFile(filename);
if (file.exists() && !args.bang)
{
liberator.echoerr("E189: \"" + filename + "\" exists (add ! to override)");
return;
}
// FIXME: Use a set/specifiable list here:
let lines = [cmd.serial().map(commands.commandToString) for (cmd in commands) if (cmd.serial)];
lines = util.Array.flatten(lines);
// :mkvimrc doesn't save autocommands, so we don't either - remove this code at some point
// line += "\n\" Auto-Commands\n";
// for (let item in autocommands)
// line += "autocmd " + item.event + " " + item.pattern.source + " " + item.command + "\n";
// if (mappings.getMapLeader() != "\\")
// line += "\nlet mapleader = \"" + mappings.getMapLeader() + "\"\n";
// source a user .vimperatorrc file
lines.unshift('"' + liberator.version);
lines.push("\nsource! " + filename + ".local");
lines.push("\n\" vim: set ft=vimperator:");
try
{
io.writeFile(file, lines.join("\n"));
}
catch (e)
{
liberator.echoerr("E190: Cannot open \"" + filename + "\" for writing");
liberator.log("Could not write to " + file.path + ": " + e.message); // XXX
}
},
{
argCount: "?",
bang: true,
completer: function (context) completion.file(context, true)
});
commands.add(["runt[ime]"],
"Source the specified file from each directory in 'runtimepath'",
function (args) { io.sourceFromRuntimePath(args, args.bang); },
{
argCount: "+",
bang: true
}
);
commands.add(["scrip[tnames]"],
"List all sourced script names",
function ()
{
let list = template.tabular(["<SNR>", "Filename"], ["text-align: right; padding-right: 1em;"],
([i + 1, file] for ([i, file] in Iterator(scriptNames)))); // TODO: add colon?
commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
},
{ argCount: "0" });
commands.add(["so[urce]"],
"Read Ex commands from a file",
function (args)
{
// FIXME: "E172: Only one file name allowed"
io.source(args[0], args.bang);
},
{
argCount: "1",
bang: true,
completer: function (context) completion.file(context, true)
});
commands.add(["!", "run"],
"Run a command",
function (args)
{
let special = args.bang;
args = args.string;
// :!! needs to be treated specially as the command parser sets the
// special flag but removes the ! from args
if (special)
args = "!" + args;
// replaceable bang and no previous command?
if (/((^|[^\\])(\\\\)*)!/.test(args) && !lastRunCommand)
{
liberator.echoerr("E34: No previous command");
return;
}
// NOTE: Vim doesn't replace ! preceded by 2 or more backslashes and documents it - desirable?
// pass through a raw bang when escaped or substitute the last command
args = args.replace(/(\\)*!/g,
function (m) /^\\(\\\\)*!$/.test(m) ? m.replace("\\!", "!") : m.replace("!", lastRunCommand)
);
lastRunCommand = args;
let output = io.system(args);
let command = ":" + util.escapeHTML(commandline.getCommand()) + "<br/>";
liberator.echo(template.generic(<span style="white-space: pre">{output}</span>))
liberator.echo(command + util.escapeHTML(output));
autocommands.trigger("ShellCmdPost", {});
},
{
argCount: "?", // TODO: "1" - probably not worth supporting weird Vim edge cases. The dream is dead. --djk
bang: true,
completer: function (context) completion.shellCommand(context),
literal: 0
});
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// PUBLIC SECTION //////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
liberator.registerObserver("load_completion", function ()
{
completion.setFunctionCompleter([ioManager.getFile, ioManager.expandPath],
[function (context, obj, args) {
context.quote[2] = "";
completion.file(context, true);
}]);
});
var ioManager = {
MODE_RDONLY: 0x01,
MODE_WRONLY: 0x02,
MODE_RDWR: 0x04,
MODE_CREATE: 0x08,
MODE_APPEND: 0x10,
MODE_TRUNCATE: 0x20,
MODE_SYNC: 0x40,
MODE_EXCL: 0x80,
sourcing: null,
expandPath: function (path)
{
// TODO: proper pathname separator translation like Vim - this should be done elsewhere
if (WINDOWS)
path = path.replace("/", "\\", "g");
// expand "~" to VIMPERATOR_HOME or HOME (USERPROFILE or HOMEDRIVE\HOMEPATH on Windows if HOME is not set)
if (/^~/.test(path))
{
let home = environmentService.get("VIMPERATOR_HOME");
if (!home)
home = environmentService.get("HOME");
if (WINDOWS && !home)
home = environmentService.get("USERPROFILE") ||
environmentService.get("HOMEDRIVE") + environmentService.get("HOMEPATH");
path = home + path.substr(1);
}
// expand any $ENV vars - this is naive but so is Vim and we like to be compatible
// TODO: Vim does not expand variables set to an empty string (and documents it).
// Kris reckons we shouldn't replicate this 'bug'. --djk
// TODO: should we be doing this for all paths?
path = path.replace(
RegExp("\\$(\\w+)\\b|\\${(\\w+)}" + (WINDOWS ? "|%(\\w+)%" : ""), "g"),
function (m, n1, n2, n3) environmentService.get(n1 || n2 || n3) || m
);
return path.replace("\\ ", " ", "g");
},
// TODO: there seems to be no way, short of a new component, to change
// Firefox's CWD - see // https://bugzilla.mozilla.org/show_bug.cgi?id=280953
getCurrentDirectory: function ()
{
let dir = ioManager.getFile(cwd.path);
// NOTE: the directory could have been deleted underneath us so
// fallback to Firefox's CWD
if (dir.exists() && dir.isDirectory())
return dir;
else
return processDir;
},
setCurrentDirectory: function (newdir)
{
newdir = newdir || "~";
if (newdir == "-")
{
[cwd, oldcwd] = [oldcwd, this.getCurrentDirectory()];
}
else
{
let dir = ioManager.getFile(newdir);
if (!dir.exists() || !dir.isDirectory())
{
liberator.echoerr("E344: Can't find directory \"" + dir.path + "\" in path");
return null;
}
[cwd, oldcwd] = [dir, this.getCurrentDirectory()];
}
return ioManager.getCurrentDirectory();
},
getRuntimeDirectories: function (specialDirectory)
{
let dirs = getPathsFromPathList(options["runtimepath"]);
dirs = dirs.map(function (dir) joinPaths(dir, specialDirectory))
.filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable());
return dirs;
},
getRCFile: function (dir)
{
dir = dir || "~";
let rcFile1 = joinPaths(dir, "." + EXTENSION_NAME + "rc");
let rcFile2 = joinPaths(dir, "_" + EXTENSION_NAME + "rc");
if (WINDOWS)
[rcFile1, rcFile2] = [rcFile2, rcFile1];
if (rcFile1.exists() && rcFile1.isFile())
return rcFile1;
else if (rcFile2.exists() && rcFile2.isFile())
return rcFile2;
else
return null;
},
// return a nsILocalFile for path where you can call isDirectory(), etc. on
// caller must check with .exists() if the returned file really exists
// also expands relative paths
getFile: function (path, noCheckPWD)
{
let file = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
let protocolHandler = Components.classes["@mozilla.org/network/protocol;1?name=file"]
.createInstance(Components.interfaces.nsIFileProtocolHandler);
if (/file:\/\//.test(path))
{
file = protocolHandler.getFileFromURLSpec(path);
}
else
{
let expandedPath = ioManager.expandPath(path);
if (!isAbsolutePath(expandedPath) && !noCheckPWD)
file = joinPaths(ioManager.getCurrentDirectory().path, expandedPath);
else
file.initWithPath(expandedPath);
}
return file;
},
// TODO: make secure
// returns a nsILocalFile or null if it could not be created
createTempFile: function ()
{
let tmpName = EXTENSION_NAME + ".tmp";
switch (EXTENSION_NAME)
{
case "muttator":
tmpName = "mutt-ator-mail"; // to allow vim to :set ft=mail automatically
break;
case "vimperator":
try
{
if (window.content.document.location.hostname)
tmpName = EXTENSION_NAME + "-" + window.content.document.location.hostname + ".tmp";
}
catch (e) {}
break;
}
let file = directoryService.get("TmpD", Components.interfaces.nsIFile);
file.append(tmpName);
file.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
if (file.exists())
return file;
else
return null; // XXX
},
// file is either a full pathname or an instance of file instanceof nsILocalFile
readDirectory: function (file, sort)
{
if (typeof file == "string")
file = ioManager.getFile(file);
else if (!(file instanceof Components.interfaces.nsILocalFile))
throw Components.results.NS_ERROR_INVALID_ARG; // FIXME: does not work as expected, just shows undefined: undefined
if (file.isDirectory())
{
let entries = file.directoryEntries;
let array = [];
while (entries.hasMoreElements())
{
let entry = entries.getNext();
entry.QueryInterface(Components.interfaces.nsIFile);
array.push(entry);
}
if (sort)
return array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
return array;
}
else
return []; // XXX: or should it throw an error, probably yes?
},
// file is either a full pathname or an instance of file instanceof nsILocalFile
// reads a file in "text" mode and returns the string
readFile: function (file)
{
let ifstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
.createInstance(Components.interfaces.nsIFileInputStream);
let icstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
.createInstance(Components.interfaces.nsIConverterInputStream);
let charset = "UTF-8";
if (typeof file == "string")
file = ioManager.getFile(file);
else if (!(file instanceof Components.interfaces.nsILocalFile))
throw Components.results.NS_ERROR_INVALID_ARG; // FIXME: does not work as expected, just shows undefined: undefined
ifstream.init(file, -1, 0, 0);
const replacementChar = Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER;
icstream.init(ifstream, charset, 4096, replacementChar); // 4096 bytes buffering
let buffer = "";
let str = {};
while (icstream.readString(4096, str) != 0)
buffer += str.value;
icstream.close();
ifstream.close();
return buffer;
},
// file is either a full pathname or an instance of file instanceof nsILocalFile
// default permission = 0644, only used when creating a new file, does not change permissions if the file exists
// mode can be ">" or ">>" in addition to the normal MODE_* flags
writeFile: function (file, buf, mode, perms)
{
let ofstream = Components.classes["@mozilla.org/network/file-output-stream;1"]
.createInstance(Components.interfaces.nsIFileOutputStream);
let ocstream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Components.interfaces.nsIConverterOutputStream);
let charset = "UTF-8"; // Can be any character encoding name that Mozilla supports
if (typeof file == "string")
file = ioManager.getFile(file);
else if (!(file instanceof Components.interfaces.nsILocalFile))
throw Components.results.NS_ERROR_INVALID_ARG; // FIXME: does not work as expected, just shows undefined: undefined
if (mode == ">>")
mode = ioManager.MODE_WRONLY | ioManager.MODE_CREATE | ioManager.MODE_APPEND;
else if (!mode || mode == ">")
mode = ioManager.MODE_WRONLY | ioManager.MODE_CREATE | ioManager.MODE_TRUNCATE;
if (!perms)
perms = 0644;
ofstream.init(file, mode, perms, 0);
ocstream.init(ofstream, charset, 0, 0x0000);
ocstream.writeString(buf);
ocstream.close();
ofstream.close();
},
run: function (program, args, blocking)
{
args = args || [];
blocking = !!blocking;
let file;
if (isAbsolutePath(program))
{
file = ioManager.getFile(program, true);
}
else
{
let dirs = environmentService.get("PATH").split(WINDOWS ? ";" : ":");
// Windows tries the cwd first TODO: desirable?
if (WINDOWS)
dirs = [io.getCurrentDirectory().path].concat(dirs);
lookup:
for (let [,dir] in Iterator(dirs))
{
file = joinPaths(dir, program);
try
{
if (file.exists())
break;
// TODO: couldn't we just palm this off to the start command?
// automatically try to add the executable path extensions on windows
if (WINDOWS)
{
let extensions = environmentService.get("PATHEXT").split(";");
for (let [,extension] in Iterator(extensions))
{
file = joinPaths(dir, program + extension);
if (file.exists())
break lookup;
}
}
}
catch (e) {}
}
}
if (!file || !file.exists())
{
liberator.echoerr("Command not found: " + program);
return -1;
}
let process = Components.classes["@mozilla.org/process/util;1"]
.createInstance(Components.interfaces.nsIProcess);
process.init(file);
process.run(blocking, args, args.length);
return process.exitValue;
},
// when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is fixed
// is fixed, should use that instead of a tmpfile
system: function (command, input)
{
liberator.echomsg("Calling shell to execute: " + command, 4);
let stdoutFile = ioManager.createTempFile();
let stderrFile = ioManager.createTempFile();
function escapeQuotes(str) str.replace('"', '\\"', "g");
if (!stdoutFile || !stderrFile) // FIXME: error reporting
return "";
if (WINDOWS)
command = "cd /D " + cwd.path + " && " + command + " > " + stdoutFile.path + " 2> " + stderrFile.path;
else
// TODO: should we only attempt the actual command conditionally on a successful cd?
command = "cd " + escapeQuotes(cwd.path) + "; " + command + " > \"" + escapeQuotes(stdoutFile.path) + "\""
+ " 2> \"" + escapeQuotes(stderrFile.path) + "\"";
let stdinFile = null;
if (input)
{
stdinFile = ioManager.createTempFile(); // FIXME: no returned file?
ioManager.writeFile(stdinFile, input);
command += " < \"" + escapeQuotes(stdinFile.path) + "\"";
}
let res = ioManager.run(options["shell"], [options["shellcmdflag"], command], true);
if (res > 0)
var output = ioManager.readFile(stderrFile) + "\nshell returned " + res;
else
var output = ioManager.readFile(stdoutFile);
stdoutFile.remove(false);
stderrFile.remove(false);
if (stdinFile)
stdinFile.remove(false);
// if there is only one \n at the end, chop it off
if (output && output.indexOf("\n") == output.length - 1)
output = output.substr(0, output.length - 1);
return output;
},
// FIXME: multiple paths?
sourceFromRuntimePath: function (paths, all)
{
let dirs = getPathsFromPathList(options["runtimepath"]);
let found = false;
// FIXME: should use original arg string
liberator.echomsg("Searching for \"" + paths.join(" ") + "\" in \"" + options["runtimepath"] + "\"", 2);
outer:
for (let [,dir] in Iterator(dirs))
{
for (let [,path] in Iterator(paths))
{
let file = joinPaths(dir, path);
liberator.echomsg("Searching for \"" + file.path, 3);
if (file.exists() && file.isFile() && file.isReadable())
{
io.source(file.path, false);
found = true;
if (!all)
break outer;
}
}
}
if (!found)
liberator.echomsg("not found in 'runtimepath': \"" + paths.join(" ") + "\"", 1); // FIXME: should use original arg string
return found;
},
// files which end in .js are sourced as pure javascript files,
// no need (actually forbidden) to add: js <<EOF ... EOF around those files
source: function (filename, silent)
{
let wasSourcing = ioManager.sourcing;
try
{
var file = ioManager.getFile(filename);
ioManager.sourcing = {
file: file.path,
line: 0
};
if (!file.exists() || !file.isReadable() || file.isDirectory())
{
if (!silent)
{
if (file.exists() && file.isDirectory())
liberator.echomsg("Cannot source a directory: \"" + filename + "\"", 0);
else
liberator.echomsg("could not source: \"" + filename + "\"", 1);
liberator.echoerr("E484: Can't open file " + filename);
}
return;
}
liberator.echomsg("sourcing \"" + filename + "\"", 2);
let str = ioManager.readFile(file);
let uri = ioService.newFileURI(file);
// handle pure javascript files specially
if (/\.js$/.test(filename))
{
try
{
liberator.loadScript(uri.spec, new Script(file));
}
catch (e)
{
let err = new Error();
for (let [k, v] in Iterator(e))
err[k] = v;
err.echoerr = file.path + ":" + e.lineNumber + ": " + e;
throw err;
}
}
else if (/\.css$/.test(filename))
{
storage.styles.registerSheet(uri.spec, !silent, true);
}
else
{
let heredoc = "";
let heredocEnd = null; // the string which ends the heredoc
let lines = str.split("\n");
for (let [i, line] in Iterator(lines))
{
if (heredocEnd) // we already are in a heredoc
{
if (heredocEnd.test(line))
{
command.execute(heredoc, special, count);
heredoc = "";
heredocEnd = null;
}
else
{
heredoc += line + "\n";
}
}
else
{
ioManager.sourcing.line = i + 1;
// skip line comments and blank lines
if (/^\s*(".*)?$/.test(line))
continue;
var [count, cmd, special, args] = commands.parseCommand(line);
var command = commands.get(cmd);
if (!command)
{
let lineNumber = i + 1;
// FIXME: messages need to be able to specify
// whether they can be cleared/overwritten or
// should be appended to and the MOW opened
liberator.echoerr("Error detected while processing " + file.path,
commandline.FORCE_MULTILINE);
commandline.echo("line " + lineNumber + ":", commandline.HL_LINENR,
commandline.APPEND_TO_MESSAGES);
liberator.echoerr("E492: Not an editor command: " + line);
}
else
{
if (command.name == "finish")
{
break;
}
else if (command.hereDoc)
{
// check for a heredoc
let matches = args.match(/(.*)<<\s*(\S+)$/);
if (matches)
{
args = matches[1];
heredocEnd = new RegExp("^" + matches[2] + "$", "m");
if (matches[1])
heredoc = matches[1] + "\n";
}
else
{
command.execute(args, special, count);
}
}
else
{
// execute a normal liberator command
liberator.execute(line);
}
}
}
}
// if no heredoc-end delimiter is found before EOF then
// process the heredoc anyway - Vim compatible ;-)
if (heredocEnd)
command.execute(heredoc, special, count);
}
if (scriptNames.indexOf(file.path) == -1)
scriptNames.push(file.path);
liberator.echomsg("finished sourcing \"" + filename + "\"", 2);
liberator.log("Sourced: " + file.path, 3);
}
catch (e)
{
let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e);
liberator.reportError(e);
if (!silent)
liberator.echoerr(message);
}
finally
{
ioManager.sourcing = wasSourcing;
}
}
}; //}}}
return ioManager;
}; //}}}
// vim: set fdm=marker sw=4 ts=4 et:

View File

@@ -0,0 +1,58 @@
(function () {
const modules = {};
const BASE = "chrome://liberator/content/";
modules.modules = modules;
const loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader);
function load(script)
{
for (let [i, base] in Iterator(prefix))
{
try
{
loader.loadSubScript(base + script, modules)
return;
}
catch (e)
{
if (i + 1 < prefix.length)
continue;
if (Components.utils.reportError)
Components.utils.reportError(e);
dump("liberator: Loading script " + script + ": " + e + "\n");
}
}
}
Components.utils.import("resource://liberator/storage.jsm", modules);
let prefix = [BASE];
["liberator.js",
"config.js",
"util.js",
"style.js",
"buffer.js",
"commands.js",
"completion.js",
"editor.js",
"events.js",
"find.js",
"hints.js",
"io.js",
"mappings.js",
"modes.js",
"options.js",
"template.js",
"ui.js"].forEach(load);
prefix.unshift("chrome://" + modules.config.name.toLowerCase() + "/content/");
if (modules.config.scripts)
modules.config.scripts.forEach(load);
})()
// vim: set fdm=marker sw=4 ts=4 et:

1360
common/content/liberator.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK ***** -->
<?xml-stylesheet href="chrome://liberator/skin/liberator.css" type="text/css"?>
<!DOCTYPE overlay SYSTEM "liberator.dtd" [
<!ENTITY liberator.content "chrome://liberator/content/">
]>
<overlay id="liberator"
xmlns:liberator="http://vimperator.org/namespaces/liberator"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:nc="http://home.netscape.com/NC-rdf#"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/x-javascript;version=1.8" src="&liberator.content;liberator-overlay.js"/>
<window id="&liberator.mainWindow;">
<keyset id="mainKeyset">
<key id="key_open_vimbar" key=":" oncommand="liberator.modules.commandline.open(':', '', liberator.modules.modes.EX);" modifiers=""/>
<key id="key_stop" keycode="VK_ESCAPE" oncommand="liberator.modules.events.onEscape();"/>
<!-- other keys are handled inside the event loop in events.js -->
</keyset>
<popupset>
<panel id="liberator-visualbell" liberator:highlight="Bell"/>
</popupset>
<!--this notifies us also of focus events in the XUL
from: http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands !-->
<commandset id="onVimperatorFocus"
commandupdater="true"
events="focus"
oncommandupdate="if (typeof liberator.modules.events != 'undefined') liberator.modules.events.onFocusChange(event);"/>
<commandset id="onVimperatorSelect"
commandupdater="true"
events="select"
oncommandupdate="if (typeof liberator.modules.events != 'undefined') liberator.modules.events.onSelectionChange(event);"/>
<!-- As of Firefox 3.1pre, <iframe>.height changes do not seem to have immediate effect,
therefore we need to put them into a <vbox> for which that works just fine -->
<vbox class="liberator-container" hidden="false" collapsed="true">
<iframe id="liberator-multiline-output" src="chrome://liberator/content/buffer.xhtml"
flex="1" hidden="false" collapsed="false"
onclick="liberator.modules.commandline.onMultilineOutputEvent(event)"/>
</vbox>
<vbox class="liberator-container" hidden="false" collapsed="true">
<iframe id="liberator-completions" src="chrome://liberator/content/buffer.xhtml"
flex="1" hidden="false" collapsed="false"
onclick="liberator.modules.commandline.onMultilineOutputEvent(event)"/>
</vbox>
<hbox id="liberator-commandline" hidden="false" liberator:highlight="Normal">
<label class="plain" id="liberator-commandline-prompt" flex="0" crop="end" value="" collapsed="true"/>
<textbox class="plain" id="liberator-commandline-command" flex="1" type="timed" timeout="100"
oninput="liberator.modules.commandline.onEvent(event);"
onfocus="liberator.modules.commandline.onEvent(event);"
onblur="liberator.modules.commandline.onEvent(event);"/>
</hbox>
<vbox class="liberator-container" hidden="false" collapsed="false">
<textbox id="liberator-multiline-input" class="plain" flex="1" rows="1" hidden="false" collapsed="true" multiline="true"
onkeypress="liberator.modules.commandline.onMultilineInputEvent(event);"
oninput="liberator.modules.commandline.onMultilineInputEvent(event);"
onblur="liberator.modules.commandline.onMultilineInputEvent(event);"/>
</vbox>
</window>
<statusbar id="status-bar" liberator:highlight="StatusLine">
<hbox insertbefore="&liberator.statusBefore;" insertafter="&liberator.statusAfter;"
id="liberator-statusline" flex="1" hidden="false" align="center">
<textbox class="plain" id="liberator-statusline-field-url" readonly="false" flex="1" crop="end"/>
<label class="plain" id="liberator-statusline-field-inputbuffer" flex="0"/>
<label class="plain" id="liberator-statusline-field-progress" flex="0"/>
<label class="plain" id="liberator-statusline-field-tabcount" flex="0"/>
<label class="plain" id="liberator-statusline-field-bufferposition" flex="0"/>
</hbox>
<!-- just hide them since other elements expect them -->
<statusbarpanel id="statusbar-display" hidden="true"/>
<statusbarpanel id="statusbar-progresspanel" hidden="true"/>
</statusbar>
</overlay>
<!-- vim: set fdm=marker sw=4 ts=4 et: -->

414
common/content/mappings.js Normal file
View File

@@ -0,0 +1,414 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
// Do NOT create instances of this class yourself, use the helper method
// mappings.add() instead
function Map(modes, cmds, description, action, extraInfo) //{{{
{
if (!modes || (!cmds || !cmds.length) || !action)
return null;
if (!extraInfo)
extraInfo = {};
this.modes = modes;
// only store keysyms with uppercase modifier strings
this.names = cmds.map(function (cmd) cmd.replace(/[casm]-/g, String.toUpperCase));
this.action = action;
this.flags = extraInfo.flags || 0;
this.description = description || "";
this.rhs = extraInfo.rhs || null;
this.noremap = extraInfo.noremap || false;
this.silent = extraInfo.silent || false;
};
Map.prototype = {
hasName: function (name)
{
return this.names.indexOf(name) >= 0;
},
execute: function (motion, count, argument)
{
var args = [];
if (this.flags & Mappings.flags.MOTION)
args.push(motion);
if (this.flags & Mappings.flags.COUNT)
args.push(count);
if (this.flags & Mappings.flags.ARGUMENT)
args.push(argument);
let self = this;
// FIXME: Kludge.
if (this.names[0] != ".")
mappings.repeat = function () self.action.apply(self, args);
return this.action.apply(this, args);
}
}; //}}}
function Mappings() //{{{
{
////////////////////////////////////////////////////////////////////////////////
////////////////////// PRIVATE SECTION /////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
var main = []; // default mappings
var user = []; // user created mappings
for (let mode in modes)
{
main[mode] = [];
user[mode] = [];
}
function addMap(map, userMap)
{
var where = userMap ? user : main;
map.modes.forEach(function (mode) {
if (!(mode in where))
where[mode] = [];
where[mode].push(map);
});
}
function getMap(mode, cmd, stack)
{
var maps = stack[mode] || [];
for (let [,map] in Iterator(maps))
{
if (map.hasName(cmd))
return map;
}
return null;
}
function removeMap(mode, cmd)
{
var maps = user[mode] || [];
var names;
for (let [i, map] in Iterator(maps))
{
for (let [j, name] in Iterator(map.names))
{
if (name == cmd)
{
map.names.splice(j, 1);
if (map.names.length == 0)
maps.splice(i, 1);
return;
}
}
}
}
function expandLeader(keyString) keyString.replace(/<Leader>/i, mappings.getMapLeader())
// Return all mappings present in all @modes
function mappingsIterator(modes, stack)
{
modes = modes.slice();
return (map for ([i, map] in Iterator(stack[modes.shift()]))
if (modes.every(function (mode) stack[mode].some(
function (m) m.rhs == map.rhs && m.names[0] == map.names[0]))))
}
function addMapCommands(ch, modes, modeDescription)
{
// 0 args -> list all maps
// 1 arg -> list the maps starting with args
// 2 args -> map arg1 to arg*
function map(args, mode, noremap)
{
if (!args.length)
{
mappings.list(mode);
return;
}
// ?:\s+ <- don't remember; (...)? optional = rhs
let [lhs, rhs] = args;
if (!rhs) // list the mapping
{
mappings.list(mode, expandLeader(lhs));
}
else
{
for (let [,m] in Iterator(mode))
{
mappings.addUserMap([m], [lhs],
"User defined mapping",
function (count) { events.feedkeys((count > 1 ? count : "") + this.rhs, this.noremap, this.silent); },
{
flags: Mappings.flags.COUNT,
rhs: rhs,
noremap: !!noremap,
silent: "<silent>" in args
});
}
}
}
modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
const opts = {
completer: function (context, args) completion.userMapping(context, args, modes),
options: [
[["<silent>", "<Silent>"], commands.OPTION_NOARG]
],
literal: 1,
serial: function () {
let noremap = this.name.indexOf("noremap") > -1;
return [
{
command: this.name,
options: map.silent ? { "<silent>": null } : {},
arguments: [map.names[0]],
literalArg: map.rhs
}
for (map in mappingsIterator(modes, user))
if (map.rhs && map.noremap == noremap)
]
}
};
commands.add([ch ? ch + "m[ap]" : "map"],
"Map a key sequence" + modeDescription,
function (args) { map(args, modes, false); },
opts);
commands.add([ch + "no[remap]"],
"Map a key sequence without remapping keys" + modeDescription,
function (args) { map(args, modes, true); },
opts);
commands.add([ch + "mapc[lear]"],
"Remove all mappings" + modeDescription,
function () { modes.forEach(function (mode) { mappings.removeAll(mode); }); },
{ argCount: "0" });
commands.add([ch + "unm[ap]"],
"Remove a mapping" + modeDescription,
function (args)
{
args = args[0];
let found = false;
for (let [,mode] in Iterator(modes))
{
if (mappings.hasMap(mode, args))
{
mappings.remove(mode, args);
found = true;
}
}
if (!found)
liberator.echoerr("E31: No such mapping");
},
{
argCount: "1",
completer: function (context, args) completion.userMapping(context, args, modes)
});
}
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// COMMANDS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
addMapCommands("", [modes.NORMAL], "");
addMapCommands("c", [modes.COMMAND_LINE], "command line");
addMapCommands("i", [modes.INSERT, modes.TEXTAREA], "insert");
if (liberator.has("mail"))
addMapCommands("m", [modes.MESSAGE], "message");
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// PUBLIC SECTION //////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
liberator.registerObserver("load_completion", function ()
{
completion.setFunctionCompleter(mappings.get,
[
null,
function (context, obj, args)
{
let mode = args[0]
return util.Array.flatten(
[
[[name, map.description] for ([i, name] in Iterator(map.names))]
for ([i, map] in Iterator(user[mode].concat(main[mode])))
])
}
]);
});
// FIXME:
Mappings.flags = {
ALLOW_EVENT_ROUTING: 1 << 0, // if set, return true inside the map command to pass the event further to firefox
MOTION: 1 << 1,
COUNT: 1 << 2,
ARGUMENT: 1 << 3
};
return {
// NOTE: just normal mode for now
__iterator__: function () mappingsIterator([modes.NORMAL], main),
// used by :mkvimperatorrc to save mappings
getUserIterator: function (mode) mappingsIterator(mode, user),
add: function (modes, keys, description, action, extra)
{
addMap(new Map(modes, keys, description, action, extra), false);
},
addUserMap: function (modes, keys, description, action, extra)
{
keys = keys.map(expandLeader);
var map = new Map(modes, keys, description || "User defined mapping", action, extra);
// remove all old mappings to this key sequence
for (let [,name] in Iterator(map.names))
{
for (let [,mode] in Iterator(map.modes))
removeMap(mode, name);
}
addMap(map, true);
},
get: function (mode, cmd)
{
mode = mode || modes.NORMAL;
return getMap(mode, cmd, user) || getMap(mode, cmd, main);
},
getDefault: function (mode, cmd)
{
mode = mode || modes.NORMAL;
return getMap(mode, cmd, main);
},
// returns an array of mappings with names which START with "cmd" (but are NOT "cmd")
getCandidates: function (mode, cmd)
{
let mappings = user[mode].concat(main[mode]);
let matches = [];
for (let [,map] in Iterator(mappings))
{
for (let [,name] in Iterator(map.names))
{
if (name.indexOf(cmd) == 0 && name.length > cmd.length)
{
// for < only return a candidate if it doesn't look like a <c-x> mapping
if (cmd != "<" || !/^<.+>/.test(name))
matches.push(map);
}
}
}
return matches;
},
getMapLeader: function ()
{
var leaderRef = liberator.variableReference("mapleader");
return leaderRef[0] ? leaderRef[0][leaderRef[1]] : "\\";
},
// returns whether the user added a custom user map
hasMap: function (mode, cmd)
{
return user[mode].some(function (map) map.hasName(cmd));
},
remove: function (mode, cmd)
{
removeMap(mode, cmd);
},
removeAll: function (mode)
{
user[mode] = [];
},
list: function (modes, filter)
{
let modeSign = "";
modes.forEach(function (mode)
{
if (mode == modes.NORMAL)
modeSign += "n";
if ((mode == modes.INSERT || mode == modes.TEXTAREA) && modeSign.indexOf("i") == -1)
modeSign += "i";
if (mode == modes.COMMAND_LINE)
modeSign += "c";
if (mode == modes.MESSAGRE)
modeSign += "m";
});
let maps = mappingsIterator(modes, user);
if (filter)
maps = [map for (map in maps) if (map.names[0] == filter)];
let list = <table>
{
template.map(maps, function (map)
template.map(map.names, function (name)
<tr>
<td>{modeSign} {name}</td>
<td>{map.noremap ? "*" : " "}</td>
<td>{map.rhs || "function () { ... }"}</td>
</tr>))
}
</table>;
// TODO: Move this to an ItemList to show this automatically
if (list.*.length() == list.text().length())
{
liberator.echo(<div highlight="Title">No mappings found</div>, commandline.FORCE_MULTILINE);
return;
}
commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
}
};
//}}}
}; //}}}
// vim: set fdm=marker sw=4 ts=4 et:

294
common/content/modes.js Normal file
View File

@@ -0,0 +1,294 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
const modes = (function () //{{{
{
////////////////////////////////////////////////////////////////////////////////
////////////////////// PRIVATE SECTION /////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
var main = 1; // NORMAL
var extended = 0; // NONE
var passNextKey = false;
var passAllKeys = false;
var isRecording = false;
var isReplaying = false; // playing a macro
var modeStack = [];
function getModeMessage()
{
if (passNextKey && !passAllKeys)
return "-- PASS THROUGH (next) --";
else if (passAllKeys && !passNextKey)
return "-- PASS THROUGH --";
// when recording a macro
var macromode = "";
if (modes.isRecording)
macromode = "recording";
else if (modes.isReplaying)
macromode = "replaying";
var ext = "";
if (extended & modes.MENU) // TODO: desirable?
ext += " (menu)";
ext += " --" + macromode;
if (main in modeMap && typeof modeMap[main].display == "function")
return "-- " + modeMap[main].display() + ext;
return macromode;
}
// NOTE: Pay attention that you don't run into endless loops
// Usually you should only indicate to leave a special mode like HINTS
// by calling modes.reset() and adding the stuff which is needed
// for its cleanup here
function handleModeChange(oldMode, newMode)
{
// TODO: fix v.log() to work with verbosity level
//liberator.log("switching from mode " + oldMode + " to mode " + newMode, 7);
//liberator.dump("switching from mode " + oldMode + " to mode " + newMode + "\n");
switch (oldMode)
{
case modes.TEXTAREA:
case modes.INSERT:
editor.unselectText();
break;
case modes.VISUAL:
if (newMode == modes.CARET)
{
// clear any selection made
var selection = window.content.getSelection();
try
{ // a simple if (selection) does not work
selection.collapseToStart();
}
catch (e) {}
}
else
editor.unselectText();
break;
case modes.CUSTOM:
plugins.stop();
break;
case modes.COMMAND_LINE:
// clean up for HINT mode
if (modes.extended & modes.HINTS)
hints.hide();
commandline.close();
break;
}
if (newMode == modes.NORMAL)
{
// disable caret mode when we want to switch to normal mode
var value = options.getPref("accessibility.browsewithcaret", false);
if (value)
options.setPref("accessibility.browsewithcaret", false);
statusline.updateUrl();
// Kludge to prevent the input field losing focus on MS/Mac
setTimeout(function () {
let focus = document.commandDispatcher.focusedElement;
let urlbar = document.getElementById("urlbar");
if (!urlbar || focus != urlbar.inputField)
liberator.focusContent(false);
}, 100);
}
}
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// PUBLIC SECTION //////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
var self = {
NONE: 0,
__iterator__: function () util.Array.iterator(this.all),
get all() mainModes.slice(),
addMode: function (name, extended, display) {
let disp = name.replace("_", " ", "g");
this[name] = 1 << lastMode++;
modeMap[name] = modeMap[this[name]] = {
extended: extended,
mask: this[name],
name: name,
display: display || function () disp
};
if (!extended)
mainModes.push(this[name]);
},
// show the current mode string in the command line
show: function ()
{
if (!options["showmode"])
return;
// never show mode messages if we are in command line mode
if (main == modes.COMMAND_LINE)
return;
commandline.echo(getModeMessage(), commandline.HL_MODEMSG,
commandline.DISALLOW_MULTILINE);
},
// add/remove always work on the extended mode only
add: function (mode)
{
extended |= mode;
this.show();
},
// helper function to set both modes in one go
// if silent == true, you also need to take care of the mode handling changes yourself
set: function (mainMode, extendedMode, silent)
{
// if a main mode is set, the extended is always cleared
if (typeof mainMode === "number")
{
main = mainMode;
if (!extendedMode)
extended = modes.NONE;
if (!silent && mainMode != main)
handleModeChange(main, mainMode);
}
if (typeof extendedMode === "number")
extended = extendedMode;
if (!silent)
this.show();
},
push: function (mainMode, extendedMode, silent)
{
modeStack.push([main, extended]);
this.set(mainMode, extendedMode, silent);
},
pop: function (silent)
{
var a = modeStack.pop();
if (a)
this.set(a[0], a[1], silent);
else
this.reset(silent);
},
setCustomMode: function (modestr, oneventfunc, stopfunc)
{
// TODO this.plugin[id]... ('id' maybe submode or what..)
plugins.mode = modestr;
plugins.onEvent = oneventfunc;
plugins.stop = stopfunc;
},
// keeps recording state
reset: function (silent)
{
modeStack = [];
if (config.isComposeWindow)
this.set(modes.COMPOSE, modes.NONE, silent);
else
this.set(modes.NORMAL, modes.NONE, silent);
},
remove: function (mode)
{
extended &= ~mode;
this.show();
},
get passNextKey() passNextKey,
set passNextKey(value) { passNextKey = value; this.show(); },
get passAllKeys() passAllKeys,
set passAllKeys(value) { passAllKeys = value; this.show(); },
get isRecording() isRecording,
set isRecording(value) { isRecording = value; this.show(); },
get isReplaying() isReplaying,
set isReplaying(value) { isReplaying = value; this.show(); },
get main() main,
set main(value) {
if (value != main)
handleModeChange(main, value);
main = value;
// setting the main mode always resets any extended mode
extended = modes.NONE;
this.show();
},
get extended() extended,
set extended(value) { extended = value; this.show(); }
};
var mainModes = [self.NONE];
var lastMode = 0;
var modeMap = {};
// main modes, only one should ever be active
self.addMode("NORMAL", false, -1);
self.addMode("INSERT");
self.addMode("VISUAL", false, function () "VISUAL" + (extended & modes.LINE ? " LINE" : ""));
self.addMode("COMMAND_LINE");
self.addMode("CARET"); // text cursor is visible
self.addMode("TEXTAREA"); // text cursor is in a HTMLTextAreaElement
self.addMode("MESSAGE"); // for now only used in Muttator when the message has focus
self.addMode("COMPOSE");
self.addMode("CUSTOM", false, function () plugins.mode);
// extended modes, can include multiple modes, and even main modes
self.addMode("EX", true);
self.addMode("HINTS", true);
self.addMode("INPUT_MULTILINE", true);
self.addMode("OUTPUT_MULTILINE", true);
self.addMode("SEARCH_FORWARD", true);
self.addMode("SEARCH_BACKWARD", true);
self.addMode("MENU", true); // a popupmenu is active
self.addMode("LINE", true); // linewise visual mode
self.addMode("RECORDING", true);
self.addMode("PROMPT", true);
return self;
//}}}
})(); //}}}
// vim: set fdm=marker sw=4 ts=4 et:

984
common/content/options.js Normal file
View File

@@ -0,0 +1,984 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
// do NOT create instances of this class yourself, use the helper method
// options.add() instead
function Option(names, description, type, defaultValue, extraInfo) //{{{
{
if (!names || !type)
return null;
if (!extraInfo)
extraInfo = {};
this.name = names[0];
this.names = names;
this.type = type;
this.scope = (extraInfo.scope & options.OPTION_SCOPE_BOTH) ||
options.OPTION_SCOPE_GLOBAL;
// XXX set to BOTH by default someday? - kstep
this.description = description || "";
// "", 0 are valid default values
this.defaultValue = (defaultValue === undefined) ? null : defaultValue;
this.setter = extraInfo.setter || null;
this.getter = extraInfo.getter || null;
this.completer = extraInfo.completer || null;
this.validator = extraInfo.validator || null;
this.checkHas = extraInfo.checkHas || null;
// this property is set to true whenever the option is first set
// useful to see whether it was changed by some rc file
this.hasChanged = false;
// add no{option} variant of boolean {option} to this.names
if (this.type == "boolean")
{
this.names = []; // reset since order is important
for (let [,name] in Iterator(names))
{
this.names.push(name);
this.names.push("no" + name);
}
}
if (this.globalvalue == undefined)
this.globalvalue = this.defaultValue;
}
Option.prototype = {
get globalvalue() options.store.get(this.name),
set globalvalue(val) { options.store.set(this.name, val) },
parseValues: function (value)
{
if (this.type == "stringlist")
return value.split(",");
if (this.type == "charlist")
return Array.slice(value);
return value;
},
joinValues: function (values)
{
if (this.type == "stringlist")
return values.join(",");
if (this.type == "charlist")
return values.join("");
return values;
},
get values() this.parseValues(this.value),
set values(values) this.setValues(this.scope, values),
getValues: function (scope) this.parseValues(this.get(scope)),
setValues: function (values, scope)
{
this.set(this.joinValues(values), scope || this.scope);
},
get: function (scope)
{
if (scope)
{
if ((scope & this.scope) == 0) // option doesn't exist in this scope
return null;
}
else
{
scope = this.scope;
}
var aValue;
if (liberator.has("tabs") && (scope & options.OPTION_SCOPE_LOCAL))
aValue = tabs.options[this.name];
if ((scope & options.OPTION_SCOPE_GLOBAL) && (aValue == undefined))
aValue = this.globalvalue;
if (this.getter)
this.getter.call(this, aValue);
return aValue;
},
set: function (newValue, scope)
{
scope = scope || this.scope;
if ((scope & this.scope) == 0) // option doesn't exist in this scope
return null;
if (this.setter)
{
let tmpValue = newValue;
newValue = this.setter.call(this, newValue);
if (newValue === undefined)
{
newValue = tmpValue;
liberator.log("DEPRECATED: '" + this.name + "' setter should return a value");
}
}
if (liberator.has("tabs") && (scope & options.OPTION_SCOPE_LOCAL))
tabs.options[this.name] = newValue;
if ((scope & options.OPTION_SCOPE_GLOBAL) && newValue != this.globalValue)
this.globalvalue = newValue;
this.hasChanged = true;
},
get value() this.get(),
set value(val) this.set(val),
has: function ()
{
let self = this;
let test = function (val) values.indexOf(val) >= 0;
if (this.checkHas)
test = function (val) values.some(function (value) self.checkHas(value, val));
let values = this.values;
/* Return whether some argument matches */
return Array.some(arguments, function (val) test(val))
},
hasName: function (name) this.names.indexOf(name) >= 0,
isValidValue: function (values)
{
if (this.validator)
return this.validator(values);
else
return true;
},
reset: function ()
{
this.value = this.defaultValue;
},
op: function (operator, values, scope, invert)
{
let newValue = null;
let self = this;
switch (this.type)
{
case "boolean":
if (operator != "=")
break;
if (invert)
newValue = !this.value;
else
newValue = values;
break;
case "number":
let value = parseInt(values); // deduce radix
if (isNaN(value))
return "E521: Number required";
switch (operator)
{
case "+":
newValue = this.value + value;
break;
case "-":
newValue = this.value - value;
break;
case "^":
newValue = this.value * value;
break;
case "=":
newValue = value;
break;
}
break;
case "charlist":
case "stringlist":
values = Array.concat(values);
switch (operator)
{
case "+":
newValue = util.Array.uniq(Array.concat(this.values, values), true);
break;
case "^":
// NOTE: Vim doesn't prepend if there's a match in the current value
newValue = util.Array.uniq(Array.concat(values, this.values), true);
break;
case "-":
newValue = this.values.filter(function (item) values.indexOf(item) == -1);
break;
case "=":
newValue = values;
if (invert)
{
let keepValues = this.values.filter(function (item) values.indexOf(item) == -1);
let addValues = values.filter(function (item) self.values.indexOf(item) == -1);
newValue = addValues.concat(keepValues);
}
break;
}
break;
case "string":
switch (operator)
{
case "+":
newValue = this.value + values;
break;
case "-":
newValue = this.value.replace(values, "");
break;
case "^":
newValue = values + this.value;
break;
case "=":
newValue = values;
break;
}
break;
default:
return "E685: Internal error: option type `" + option.type + "' not supported";
}
if (newValue == null)
return "Operator " + operator + " not supported for option type " + this.type;
if (!this.isValidValue(newValue))
return "E474: Invalid argument: " + values;
this.setValues(newValue, scope);
}
};
// TODO: Run this by default?
Option.validateCompleter = function (values)
{
let context = CompletionContext("");
let res = context.fork("", 0, this, this.completer);
if (!res)
res = context.allItems.items.map(function (item) [item.text]);
return Array.concat(values).every(
function (value) res.some(function (item) item[0] == value));
}; //}}}
function Options() //{{{
{
////////////////////////////////////////////////////////////////////////////////
////////////////////// PRIVATE SECTION /////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
var prefService = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
var optionHash = {};
function optionObserver(key, event, option)
{
// Trigger any setters.
let opt = options.get(option);
if (event == "change" && opt)
opt.set(opt.value, options.OPTION_SCOPE_GLOBAL);
}
storage.newMap("options", false);
storage.addObserver("options", optionObserver);
liberator.registerObserver("shutdown", function () {
storage.removeObserver("options", optionObserver);
});
function storePreference(name, value)
{
var type = prefService.getPrefType(name);
switch (typeof value)
{
case "string":
if (type == prefService.PREF_INVALID || type == prefService.PREF_STRING)
prefService.setCharPref(name, value);
else if (type == prefService.PREF_INT)
liberator.echoerr("E521: Number required after =: " + name + "=" + value);
else
liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
break;
case "number":
if (type == prefService.PREF_INVALID || type == prefService.PREF_INT)
prefService.setIntPref(name, value);
else
liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
break;
case "boolean":
if (type == prefService.PREF_INVALID || type == prefService.PREF_BOOL)
prefService.setBoolPref(name, value);
else if (type == prefService.PREF_INT)
liberator.echoerr("E521: Number required after =: " + name + "=" + value);
else
liberator.echoerr("E474: Invalid argument: " + name + "=" + value);
break;
default:
liberator.echoerr("Unknown preference type: " + typeof value + " (" + name + "=" + value + ")");
}
}
function loadPreference(name, forcedDefault, defaultBranch)
{
var defaultValue = null; // XXX
if (forcedDefault != null) // this argument sets defaults for non-user settable options (like extensions.history.comp_history)
defaultValue = forcedDefault;
var branch = defaultBranch ? prefService.getDefaultBranch("") : prefService;
var type = prefService.getPrefType(name);
try
{
switch (type)
{
case prefService.PREF_STRING:
var value = branch.getComplexValue(name, Components.interfaces.nsISupportsString).data;
// try in case it's a localized string (will throw an exception if not)
if (!prefService.prefIsLocked(name) && !prefService.prefHasUserValue(name) &&
/^chrome:\/\/.+\/locale\/.+\.properties/.test(value))
value = branch.getComplexValue(name, Components.interfaces.nsIPrefLocalizedString).data;
return value;
case prefService.PREF_INT:
return branch.getIntPref(name);
case prefService.PREF_BOOL:
return branch.getBoolPref(name);
default:
return defaultValue;
}
}
catch (e)
{
return defaultValue;
}
}
//
// firefox preferences which need to be changed to work well with vimperator
//
// work around firefox popup blocker
var popupAllowedEvents = loadPreference("dom.popup_allowed_events", "change click dblclick mouseup reset submit");
if (!/keypress/.test(popupAllowedEvents))
{
storePreference("dom.popup_allowed_events", popupAllowedEvents + " keypress");
liberator.registerObserver("shutdown", function ()
{
if (loadPreference("dom.popup_allowed_events", "")
== popupAllowedEvents + " keypress")
storePreference("dom.popup_allowed_events", popupAllowedEvents);
});
}
// TODO: maybe reset in .destroy()?
// TODO: move to vim.js or buffer.js
// we have our own typeahead find implementation
storePreference("accessibility.typeaheadfind.autostart", false);
storePreference("accessibility.typeaheadfind", false); // actually the above setting should do it, but has no effect in firefox
// start with saved session
storePreference("browser.startup.page", 3);
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// COMMANDS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
commands.add(["let"],
"Set or list a variable",
function (args)
{
args = args.string;
if (!args)
{
var str =
<table>
{
template.map(liberator.globalVariables, function ([i, value]) {
let prefix = typeof value == "number" ? "#" :
typeof value == "function" ? "*" :
" ";
return <tr>
<td style="width: 200px;">{i}</td>
<td>{prefix}{value}</td>
</tr>
})
}
</table>;
if (str.*.length())
liberator.echo(str, commandline.FORCE_MULTILINE);
else
liberator.echo("No variables found");
return;
}
var matches;
// 1 - type, 2 - name, 3 - +-., 4 - expr
if (matches = args.match(/([$@&])?([\w:]+)\s*([-+.])?=\s*(.+)/))
{
if (!matches[1])
{
var reference = liberator.variableReference(matches[2]);
if (!reference[0] && matches[3])
{
liberator.echoerr("E121: Undefined variable: " + matches[2]);
return;
}
var expr = liberator.evalExpression(matches[4]);
if (expr === undefined)
{
liberator.echoerr("E15: Invalid expression: " + matches[4]);
return;
}
else
{
if (!reference[0])
{
if (reference[2] == "g")
reference[0] = liberator.globalVariables;
else
return; // for now
}
if (matches[3])
{
if (matches[3] == "+")
reference[0][reference[1]] += expr;
else if (matches[3] == "-")
reference[0][reference[1]] -= expr;
else if (matches[3] == ".")
reference[0][reference[1]] += expr.toString();
}
else
reference[0][reference[1]] = expr;
}
}
}
// 1 - name
else if (matches = args.match(/^\s*([\w:]+)\s*$/))
{
var reference = liberator.variableReference(matches[1]);
if (!reference[0])
{
liberator.echoerr("E121: Undefined variable: " + matches[1]);
return;
}
var value = reference[0][reference[1]];
let prefix = typeof value == "number" ? "#" :
typeof value == "function" ? "*" :
" ";
liberator.echo(reference[1] + "\t\t" + prefix + value);
}
});
commands.add(["pref[erences]", "prefs"],
"Show " + config.hostApplication + " preferences",
function (args)
{
if (args.bang) // open Firefox settings GUI dialog
{
liberator.open("about:config",
(options["newtab"] && options.get("newtab").has("all", "prefs"))
? liberator.NEW_TAB : liberator.CURRENT_TAB);
}
else
{
window.openPreferences();
}
},
{
argCount: "0",
bang: true
});
commands.add(["setl[ocal]"],
"Set local option",
function (args)
{
commands.get("set").execute(args.string, args.bang, args.count, { scope: options.OPTION_SCOPE_LOCAL });
},
{
bang: true,
count: true,
completer: function (context, args)
{
return commands.get("set").completer(context.filter, args.bang, args.count, { scope: options.OPTION_SCOPE_LOCAL });
},
literal: 0
}
);
commands.add(["setg[lobal]"],
"Set global option",
function (args)
{
commands.get("set").execute(args.string, args.bang, args.count, { scope: options.OPTION_SCOPE_GLOBAL });
},
{
bang: true,
count: true,
completer: function (context, args)
{
return commands.get("set").completer(context.filter, args.bang, args.count, { scope: options.OPTION_SCOPE_GLOBAL });
},
literal: 0
}
);
// TODO: support setting multiple options at once
commands.add(["se[t]"],
"Set an option",
function (args, modifiers)
{
let bang = args.bang;
args = args.string;
if (bang)
{
var onlyNonDefault = false;
if (!args)
{
args = "all";
onlyNonDefault = true;
}
let [matches, name, postfix, valueGiven, operator, value] =
args.match(/^\s*?([a-zA-Z0-9\.\-_{}]+)([?&!])?\s*(([-+^]?)=(.*))?\s*$/);
let reset = (postfix == "&");
let invertBoolean = (postfix == "!");
if (name == "all" && reset)
liberator.echoerr("You can't reset all options, it could make " + config.hostApplication + " unusable.");
else if (name == "all")
options.listPrefs(onlyNonDefault, "");
else if (reset)
options.resetPref(name);
else if (invertBoolean)
options.invertPref(name);
else if (valueGiven)
{
switch (value)
{
case undefined:
value = "";
break;
case "true":
value = true;
break;
case "false":
value = false;
break;
default:
if (/^\d+$/.test(value))
value = parseInt(value, 10);
}
options.setPref(name, value);
}
else
{
options.listPrefs(onlyNonDefault, name);
}
return;
}
let opt = options.parseOpt(args, modifiers);
if (!opt)
{
liberator.echoerr("Error parsing :set command: " + args);
return;
}
let option = opt.option;
if (option == null && !opt.all)
{
liberator.echoerr("No such option: " + opt.name);
return;
}
// reset a variable to its default value
if (opt.reset)
{
if (opt.all)
{
for (let option in options)
option.reset();
}
else
{
option.reset();
}
}
// read access
else if (opt.get)
{
if (opt.all)
{
options.list(opt.onlyNonDefault, opt.scope);
}
else
{
if (option.type == "boolean")
liberator.echo((opt.optionValue ? " " : "no") + option.name);
else
liberator.echo(" " + option.name + "=" + opt.optionValue);
}
}
// write access
// NOTE: the behavior is generally Vim compatible but could be
// improved. i.e. Vim's behavior is pretty sloppy to no real benefit
else
{
if (opt.option.type == "boolean")
{
if (opt.valueGiven)
{
liberator.echoerr("E474: Invalid argument: " + args);
return;
}
opt.values = !opt.unsetBoolean;
}
let res = opt.option.op(opt.operator || "=", opt.values, opt.scope, opt.invert);
if (res)
liberator.echoerr(res);
}
},
{
bang: true,
completer: function (context, args, modifiers)
{
let filter = context.filter;
var optionCompletions = [];
if (args.bang) // list completions for about:config entries
{
if (filter[filter.length - 1] == "=")
{
context.advance(filter.length);
context.completions = [options.getPref(filter.substr(0, filter.length - 1)), "Current Value"];
return;
}
return completion.preference(context);
}
let opt = options.parseOpt(filter, modifiers);
let prefix = opt.prefix;
if (context.filter.indexOf("=") == -1)
{
if (prefix)
context.filters.push(function ({ item: opt }) opt.type == "boolean" || prefix == "inv" && opt.values instanceof Array);
return completion.option(context, opt.scope);
}
else if (prefix == "no")
return;
if (prefix)
context.advance(prefix.length);
let option = opt.option;
context.advance(context.filter.indexOf("=") + 1);
if (!option)
{
context.message = "No such option: " + opt.name;
context.highlight(0, name.length, "SPELLCHECK");
}
if (opt.get || opt.reset || !option || prefix)
return;
if (!opt.value)
{
context.fork("default", 0, this, function (context) {
context.title = ["Extra Completions"];
context.completions = [[option.value, "Current value"], [option.defaultValue, "Default value"]].filter(function (f) f[0])
});
}
completion.optionValue(context, opt.name, opt.operator);
},
literal: 0,
serial: function () [
{
command: this.name,
literalArg: opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name
: opt.name + "=" + opt.value
}
for (opt in options)
if (!opt.getter && opt.value != opt.defaultValue && (opt.scope & options.OPTION_SCOPE_GLOBAL))
]
});
commands.add(["unl[et]"],
"Delete a variable",
function (args)
{
//var names = args.split(/ /);
//if (typeof names == "string") names = [names];
//var length = names.length;
//for (let i = 0, name = names[i]; i < length; name = names[++i])
for (let [,name] in args)
{
var name = args[i];
var reference = liberator.variableReference(name);
if (!reference[0])
{
if (!args.bang)
liberator.echoerr("E108: No such variable: " + name);
return;
}
delete reference[0][reference[1]];
}
},
{
argCount: "+",
bang: true
});
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// PUBLIC SECTION //////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
// TODO: Does this belong elsewhere?
liberator.registerObserver("load_completion", function ()
{
completion.setFunctionCompleter(options.get, [function () ([o.name, o.description] for (o in options))]);
completion.setFunctionCompleter([options.getPref, options.setPref, options.resetPref, options.invertPref],
[function () Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch)
.getChildList("", { value: 0 })
.map(function (pref) [pref, ""])]);
});
return {
OPTION_SCOPE_GLOBAL: 1,
OPTION_SCOPE_LOCAL: 2,
OPTION_SCOPE_BOTH: 3,
__iterator__: function ()
{
let sorted = [o for ([i, o] in Iterator(optionHash))].sort(function (a, b) String.localeCompare(a.name, b.name));
return (v for ([k, v] in Iterator(sorted)));
},
add: function (names, description, type, defaultValue, extraInfo)
{
if (!extraInfo)
extraInfo = {};
let option = new Option(names, description, type, defaultValue, extraInfo);
if (!option)
return false;
if (option.name in optionHash)
{
// never replace for now
liberator.log("Warning: '" + names[0] + "' already exists, NOT replacing existing option.", 1);
return false;
}
// quickly access options with options["wildmode"]:
this.__defineGetter__(option.name, function () option.value);
this.__defineSetter__(option.name, function (value) { option.value = value; });
optionHash[option.name] = option;
return true;
},
get: function (name, scope)
{
if (!scope)
scope = options.OPTION_SCOPE_BOTH;
if (name in optionHash)
return (optionHash[name].scope & scope) && optionHash[name];
for (let opt in Iterator(options))
{
if (opt.hasName(name))
return (opt.scope & scope) && opt;
}
return null;
},
list: function (onlyNonDefault, scope)
{
if (!scope)
scope = options.OPTION_SCOPE_BOTH;
let opts = function (opt) {
for (let opt in Iterator(options))
{
let option = {
isDefault: opt.value == opt.defaultValue,
name: opt.name,
default: opt.defaultValue,
pre: "\u00a0\u00a0", /* Unicode nonbreaking space. */
value: <></>
};
if (onlyNonDefault && option.isDefault)
continue;
if (!(opt.scope & scope))
continue;
if (opt.type == "boolean")
{
if (!opt.value)
option.pre = "no";
option.default = (option.default ? "" : "no") + opt.name;
}
else
{
option.value = <>={template.highlight(opt.value)}</>;
}
yield option;
}
};
let list = template.options("Options", opts());
commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
},
listPrefs: function (onlyNonDefault, filter)
{
if (!filter)
filter = "";
var prefArray = prefService.getChildList("", { value: 0 });
prefArray.sort();
let prefs = function () {
for each (let pref in prefArray)
{
var userValue = prefService.prefHasUserValue(pref);
if (onlyNonDefault && !userValue || pref.indexOf(filter) == -1)
continue;
value = options.getPref(pref);
let option = {
isDefault: !userValue,
default: loadPreference(pref, null, true),
value: <>={template.highlight(value, true, 100)}</>,
name: pref,
pre: "\u00a0\u00a0" /* Unicode nonbreaking space. */
};
yield option;
}
};
let list = template.options(config.hostApplication + " Options", prefs());
commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
},
parseOpt: function parseOpt(args, modifiers)
{
let ret = {};
let matches, prefix, postfix, valueGiven;
[matches, prefix, ret.name, postfix, valueGiven, ret.operator, ret.value] =
args.match(/^\s*(no|inv)?([a-z_]*)([?&!])?\s*(([-+^]?)=(.*))?\s*$/) || [];
ret.args = args;
ret.onlyNonDefault = false; // used for :set to print non-default options
if (!args)
{
ret.name = "all";
ret.onlyNonDefault = true;
}
if (matches)
ret.option = options.get(ret.name, ret.scope);
ret.prefix = prefix;
ret.postfix = postfix;
ret.all = (ret.name == "all");
ret.get = (ret.all || postfix == "?" || (ret.option && ret.option.type != "boolean" && !valueGiven));
ret.invert = (prefix == "inv" || postfix == "!");
ret.reset = (postfix == "&");
ret.unsetBoolean = (prefix == "no");
ret.scope = modifiers && modifiers.scope;
if (!ret.option)
return ret;
if (ret.value === undefined)
ret.value = "";
ret.optionValue = ret.option.get(ret.scope);
ret.optionValues = ret.option.getValues(ret.scope);
ret.values = ret.option.parseValues(ret.value);
return ret;
},
get store() storage.options,
getPref: function (name, forcedDefault)
{
return loadPreference(name, forcedDefault);
},
setPref: function (name, value)
{
return storePreference(name, value);
},
resetPref: function (name)
{
return prefService.clearUserPref(name);
},
// this works only for booleans
invertPref: function (name)
{
if (prefService.getPrefType(name) == prefService.PREF_BOOL)
this.setPref(name, !this.getPref(name));
else
liberator.echoerr("E488: Trailing characters: " + name + "!");
}
};
//}}}
}; //}}}
// vim: set fdm=marker sw=4 ts=4 et:

582
common/content/style.js Normal file
View File

@@ -0,0 +1,582 @@
/***** BEGIN LICENSE BLOCK ***** {{{
©2008 Kris Maglione <maglione.k at Gmail>
Distributable under the terms of the MIT license, which allows
for sublicensing under any compatible license, including the MPL,
GPL, and MPL. Anyone who changes this file is welcome to relicense
it under any or all of those licenseses.
}}} ***** END LICENSE BLOCK *****/
Highlights.prototype.CSS = <![CDATA[
Boolean color: red;
Function color: navy;
Null color: blue;
Number color: blue;
Object color: maroon;
String color: green;
Normal color: black; background: white;
ErrorMsg color: white; background: red; font-weight: bold;
InfoMsg color: black; background: white;
ModeMsg color: black; background: white;
MoreMsg color: green; background: white;
WarningMsg color: red; background: white;
Message white-space: normal; min-width: 100%; padding-left: 2em; text-indent: -2em; display: block;
NonText color: blue; min-height: 16px; padding-left: 2px;
Preview color: gray;
CompGroup
CompGroup:not(:first-of-type) margin-top: .5em;
CompTitle color: magenta; background: white; font-weight: bold;
CompTitle>* /* border-bottom: 1px dashed magenta; */
CompMsg font-style: italic; margin-left: 16px;
CompItem
CompItem[selected] background: yellow;
CompItem>* padding: 0 .5ex;
CompIcon width: 16px; min-width: 16px; display: inline-block; margin-right: .5ex;
CompIcon>img max-width: 16px; max-height: 16px; vertical-align: middle;
CompResult width: 45%; overflow: hidden;
CompDesc color: gray; width: 50%;
CompLess text-align: center; height: 0; line-height: .5ex; padding-top: 1ex;
CompLess::after content: "\2303" /* Unicode up arrowhead */
CompMore text-align: center; height: .5ex; line-height: .5ex; margin-bottom: -.5ex;
CompMore::after content: "\2304" /* Unicode down arrowhead */
Gradient height: 1px; margin-bottom: -1px; margin-top: -1px;
GradientLeft background-color: magenta;
GradientRight background-color: white;
Indicator color: blue;
Filter font-weight: bold;
Keyword color: red;
Tag color: blue;
LineNr color: orange; background: white;
Question color: green; background: white; font-weight: bold;
StatusLine color: white; background: black;
StatusLineBroken color: black; background: #FF6060; /* light-red */
StatusLineSecure color: black; background: #B0FF00; /* light-green */
TabClose
TabIcon
TabText
TabNumber font-weight: bold; margin: 0px; padding-right: .3ex;
TabIconNumber {
font-weight: bold;
color: white;
text-align: center;
text-shadow: black -1px 0 1px, black 0 1px 1px, black 1px 0 1px, black 0 -1px 1px;
}
Title color: magenta; background: white; font-weight: bold;
URL text-decoration: none; color: green; background: inherit;
URL:hover text-decoration: underline; cursor: pointer;
FrameIndicator,,* {
background-color: red;
opacity: 0.5;
z-index: 999;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
Bell border: none; background-color: black;
Hint,,* {
font-family: monospace;
font-size: 10px;
font-weight: bold;
color: white;
background-color: red;
border-color: ButtonShadow;
border-width: 0px;
border-style: solid;
padding: 0px 1px 0px 1px;
}
Hint::after,,* content: attr(number);
HintElem,,* background-color: yellow; color: black;
HintActive,,* background-color: #88FF00; color: black;
HintImage,,* opacity: .5;
Search,,* {
font-size: inherit;
padding: 0;
color: black;
background-color: yellow;
padding: 0;
}
]]>.toString();
function Highlights(name, store, serial)
{
var self = this;
var highlight = {};
var styles = storage.styles;
const Highlight = Struct("class", "selector", "filter", "default", "value");
Highlight.defaultValue("filter", function () "chrome://liberator/content/buffer.xhtml" + "," + config.styleableChrome);
Highlight.defaultValue("selector", function () self.selector(this.class));
Highlight.defaultValue("value", function () this.default);
Highlight.prototype.toString = function () "Highlight(" + this.class + ")\n\t" + [k + ": " + util.escapeString(v || "undefined") for ([k, v] in this)].join("\n\t");
function keys() [k for ([k, v] in Iterator(highlight))].sort();
this.__iterator__ = function () (highlight[v] for ([k, v] in Iterator(keys())));
this.get = function (k) highlight[k];
this.set = function (key, newStyle, force, append)
{
let [, class, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/);
if (!(class in highlight))
return "Unknown highlight keyword: " + class;
let style = highlight[key] || new Highlight(key);
styles.removeSheet(style.selector, null, null, null, true);
if (append)
newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle);
if (/^\s*$/.test(newStyle))
newStyle = null;
if (newStyle == null)
{
if (style.default == null)
{
delete highlight[style.class];
styles.removeSheet(style.selector, null, null, null, true);
return null;
}
newStyle = style.default;
force = true;
}
let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;")
.replace(";!important;", ";", "g"); // Seeming Spidermonkey bug
css = style.selector + " { " + css + " }";
let error = styles.addSheet(style.selector, style.filter, css, true, force);
if (error)
return error;
style.value = newStyle;
highlight[style.class] = style;
}
this.selector = function (class)
{
let [, hl, rest] = class.match(/^(\w*)(.*)/);
return "[liberator|highlight~=" + hl + "]" + rest
};
this.reload = function ()
{
this.CSS.replace(/\{((?:.|\n)*?)\}/g, function (_, _1) _1.replace(/\n\s*/g, " "))
.split("\n").filter(function (s) /\S/.test(s))
.forEach(function (style)
{
style = Highlight.apply(Highlight, Array.slice(style.match(/^\s*([^,\s]+)(?:,([^,\s]+)?)?(?:,([^,\s]+))?\s*(.*)$/), 1));
let old = highlight[style.class];
highlight[style.class] = style;
if (old && old.value != old.default)
style.value = old.value;
});
for (let [class, hl] in Iterator(highlight))
{
if (hl.value == hl.default)
this.set(class);
}
}
}
function Styles(name, store, serial)
{
/* Can't reference liberator or Components inside Styles --
* they're members of the window object, which disappear
* with this window.
*/
const util = modules.util;
const sleep = liberator.sleep;
const storage = modules.storage;
const consoleService = Components.classes["@mozilla.org/consoleservice;1"]
.getService(Components.interfaces.nsIConsoleService);
const ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
const sss = Components.classes["@mozilla.org/content/style-sheet-service;1"]
.getService(Components.interfaces.nsIStyleSheetService);
const namespace = '@namespace html "' + XHTML + '";\n' +
'@namespace xul "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";\n' +
'@namespace liberator "' + NS.uri + '";\n';
const Sheet = new Struct("name", "sites", "css", "ref");
let cssUri = function (css) "chrome-data:text/css," + encodeURI(css);
let userSheets = [];
let systemSheets = [];
let userNames = {};
let systemNames = {};
this.__iterator__ = function () Iterator(userSheets.concat(systemSheets));
this.__defineGetter__("systemSheets", function () Iterator(systemSheets));
this.__defineGetter__("userSheets", function () Iterator(userSheets));
this.__defineGetter__("systemNames", function () Iterator(systemNames));
this.__defineGetter__("userNames", function () Iterator(userNames));
this.addSheet = function (name, filter, css, system, force)
{
let sheets = system ? systemSheets : userSheets;
let names = system ? systemNames : userNames;
if (name && name in names)
this.removeSheet(name, null, null, null, system);
let sheet = sheets.filter(function (s) s.sites.join(",") == filter && s.css == css)[0];
if (!sheet)
sheet = new Sheet(name, filter.split(",").filter(util.identity), css, null);
if (sheet.ref == null) // Not registered yet
{
sheet.ref = [];
try
{
this.registerSheet(cssUri(wrapCSS(sheet)), !force);
}
catch (e)
{
return e.echoerr || e;
}
sheets.push(sheet);
}
if (name)
{
sheet.ref.push(name);
names[name] = sheet;
}
return null;
}
this.findSheets = function (name, filter, css, index, system)
{
let sheets = system ? systemSheets : userSheets;
let names = system ? systemNames : userNames;
// Grossly inefficient.
let matches = [k for ([k, v] in Iterator(sheets))];
if (index)
matches = String(index).split(",").filter(function (i) i in sheets);
if (name)
matches = matches.filter(function (i) sheets[i] == names[name]);
if (css)
matches = matches.filter(function (i) sheets[i].css == css);
if (filter)
matches = matches.filter(function (i) sheets[i].sites.indexOf(filter) >= 0);
return matches.map(function (i) sheets[i]);
};
this.removeSheet = function (name, filter, css, index, system)
{
let self = this;
let sheets = system ? systemSheets : userSheets;
let names = system ? systemNames : userNames;
if (filter && filter.indexOf(",") > -1)
return filter.split(",").reduce(
function (n, f) n + self.removeSheet(name, f, index, system), 0);
if (filter == undefined)
filter = "";
let matches = this.findSheets(name, filter, css, index, system);
if (matches.length == 0)
return;
for (let [,sheet] in Iterator(matches.reverse()))
{
if (name)
{
if (sheet.ref.indexOf(name) > -1)
sheet.ref.splice(sheet.ref.indexOf(name), 1);
delete names[name];
}
if (!sheet.ref.length)
{
this.unregisterSheet(cssUri(wrapCSS(sheet)));
if (sheets.indexOf(sheet) > -1)
sheets.splice(sheets.indexOf(sheet), 1);
}
if (filter)
{
let sites = sheet.sites.filter(function (f) f != filter);
if (sites.length)
this.addSheet(name, sites.join(","), css, system, true);
}
}
return matches.length;
}
this.registerSheet = function (uri, doCheckSyntax, reload)
{
//dump (uri + "\n\n");
if (doCheckSyntax)
checkSyntax(uri);
if (reload)
this.unregisterSheet(uri);
uri = ios.newURI(uri, null, null);
if (reload || !sss.sheetRegistered(uri, sss.USER_SHEET))
sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
}
this.unregisterSheet = function (uri)
{
uri = ios.newURI(uri, null, null);
if (sss.sheetRegistered(uri, sss.USER_SHEET))
sss.unregisterSheet(uri, sss.USER_SHEET);
}
function wrapCSS(sheet)
{
let filter = sheet.sites;
let css = sheet.css;
if (filter[0] == "*")
return namespace + css;
let selectors = filter.map(function (part) (/[*]$/.test(part) ? "url-prefix" :
/[\/:]/.test(part) ? "url"
: "domain")
+ '("' + part.replace(/"/g, "%22").replace(/[*]$/, "") + '")')
.join(", ");
return namespace + "@-moz-document " + selectors + "{\n" + css + "\n}\n";
}
let queryinterface = XPCOMUtils.generateQI([Components.interfaces.nsIConsoleListener]);
/* What happens if more than one thread tries to use this? */
let testDoc = document.implementation.createDocument(XHTML, "doc", null);
function checkSyntax(uri)
{
let errors = [];
let listener = {
QueryInterface: queryinterface,
observe: function (message)
{
try
{
message = message.QueryInterface(Components.interfaces.nsIScriptError);
if (message.sourceName == uri)
errors.push(message);
}
catch (e) {}
}
};
try
{
consoleService.registerListener(listener);
if (testDoc.documentElement.firstChild)
testDoc.documentElement.removeChild(testDoc.documentElement.firstChild);
testDoc.documentElement.appendChild(util.xmlToDom(
<html><head><link type="text/css" rel="stylesheet" href={uri}/></head></html>, testDoc));
while (true)
{
try
{
// Throws NS_ERROR_DOM_INVALID_ACCESS_ERR if not finished loading
testDoc.styleSheets[0].cssRules.length;
break;
}
catch (e)
{
if (e.name != "NS_ERROR_DOM_INVALID_ACCESS_ERR")
return [e.toString()];
sleep(10);
}
}
}
finally
{
consoleService.unregisterListener(listener);
}
if (errors.length)
{
let err = new Error("", errors[0].sourceName.replace(/^(chrome-data:text\/css,).*/, "$1..."), errors[0].lineNumber);
err.name = "CSSError"
err.message = errors.reduce(function (msg, e) msg + "; " + e.lineNumber + ": " + e.errorMessage,
errors.shift().errorMessage);
err.echoerr = err.fileName + ":" + err.lineNumber + ": " + err.message;
throw err;
}
}
}
let (array = util.Array)
{
Styles.prototype = {
get sites() array.uniq(array.flatten([v.sites for ([k, v] in this.userSheets)]))
};
}
const styles = storage.newObject("styles", Styles, false);
const highlight = storage.newObject("highlight", Highlights, false);
highlight.CSS = Highlights.prototype.CSS;
highlight.reload();
liberator.triggerObserver("load_styles", "styles");
liberator.triggerObserver("load_highlight", "highlight");
liberator.registerObserver("load_commands", function ()
{
// TODO: :colo default needs :hi clear
commands.add(["colo[rscheme]"],
"Load a color scheme",
function (args)
{
let scheme = args[0];
if (io.sourceFromRuntimePath(["colors/" + scheme + ".vimp"]))
autocommands.trigger("ColorScheme", { name: scheme });
else
liberator.echoerr("E185: Cannot find color scheme " + scheme);
},
{
argCount: "1",
completer: function (context) completion.colorScheme(context)
});
commands.add(["sty[le]"],
"Add or list user styles",
function (args)
{
let [filter, css] = args;
let name = args["-name"];
if (!css)
{
let list = Array.concat([i for (i in styles.userNames)],
[i for (i in styles.userSheets) if (!i[1].ref.length)]);
let str = template.tabular(["", "Filter", "CSS"],
["padding: 0 1em 0 1ex; vertical-align: top", "padding: 0 1em 0 0; vertical-align: top"],
([k, v[1].join(","), v[2]]
for ([i, [k, v]] in Iterator(list))
if ((!filter || v[1].indexOf(filter) >= 0) && (!name || v[0] == name))));
commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
}
else
{
if ("-append" in args)
{
let sheet = styles.findSheets(name, null, null, null, false)[0];
if (sheet)
{
filter = sheet.sites.concat(filter).join(",");
css = sheet.css.replace(/;?\s*$/, "; " + css);
}
}
let err = styles.addSheet(name, filter, css, false, args.bang);
if (err)
liberator.echoerr(err);
}
},
{
bang: true,
completer: function (context, args) {
let compl = [];
if (args.completeArg == 0)
{
try
{
compl.push([content.location.host, "Current Host"]);
compl.push([content.location.href, "Current URL"]);
}
catch (e) {}
context.completions = compl.concat([[s, ""] for each (s in styles.sites)])
}
else if (args.completeArg == 1)
{
let sheet = styles.findSheets(args["-name"], null, null, null, false)[0];
if (sheet)
context.completions = [[sheet.css, "Current Value"]];
}
},
hereDoc: true,
literal: 1,
options: [[["-name", "-n"], commands.OPTION_STRING, null, function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))]],
[["-append", "-a"], commands.OPTION_NOARG]],
serial: function () [
{
command: this.name,
bang: true,
options: sty.name ? { "-name": sty.name } : {},
arguments: [sty.sites.join(",")],
literalArg: sty.css
} for ([k, sty] in styles.userSheets)
]
});
commands.add(["dels[tyle]"],
"Remove a user stylesheet",
function (args) {
styles.removeSheet(args["-name"], args[0], args.literalArg, args["-index"], false);
},
{
completer: function (context) { context.completions = styles.sites.map(function (site) [site, ""]); },
literal: 1,
options: [[["-index", "-i"], commands.OPTION_INT, null, function () [[i, <>{s.sites.join(",")}: {s.css.replace("\n", "\\n")}</>] for ([i, s] in styles.userSheets)]],
[["-name", "-n"], commands.OPTION_STRING, null, function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))]]]
});
commands.add(["hi[ghlight]"],
"Set the style of certain display elements",
function (args)
{
let style = <![CDATA[
;
display: inline-block !important;
position: static !important;
margin: 0px !important; padding: 0px !important;
width: 3em !important; min-width: 3em !important; max-width: 3em !important;
height: 1em !important; min-height: 1em !important; max-height: 1em !important;
overflow: hidden !important;
]]>;
let [key, css] = args;
if (!css && !(key && args.bang))
{
let str = template.tabular(["Key", "Sample", "CSS"],
["padding: 0 1em 0 0; vertical-align: top", "text-align: center"],
([h.class,
<span style={"text-align: center; line-height: 1em;" + h.value + style}>XXX</span>,
template.highlightRegexp(h.value, /\b[-\w]+(?=:)/g)]
for (h in highlight)
if (!key || h.class.indexOf(key) > -1)));
commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
return;
}
let error = highlight.set(key, css, args.bang, "-append" in args);
if (error)
liberator.echoerr(error);
},
{
bang: true,
// TODO: add this as a standard highlight completion function?
completer: function (context, args)
{
if (args.completeArg == 0)
context.completions = [[v.class, ""] for (v in highlight)];
else if (args.completeArg == 1)
{
let hl = highlight.get(args[0]);
if (hl)
context.completions = [[hl.value, "Current Value"], [hl.default || "", "Default Value"]];
}
},
hereDoc: true,
literal: 1,
options: [[["-append", "-a"], commands.OPTION_NOARG]],
serial: function () [
{
command: this.name,
arguments: [k],
literalArg: v
}
for ([k, v] in Iterator(highlight))
if (v.value != v.default)
]
});
});
// vim: set fdm=marker sw=4 ts=4 et:

1000
common/content/tabs.js Normal file

File diff suppressed because it is too large Load Diff

305
common/content/template.js Normal file
View File

@@ -0,0 +1,305 @@
const template = {
add: function add(a, b) a + b,
join: function join(c) function (a, b) a + c + b,
map: function map(iter, fn, sep, interruptable)
{
if (iter.length) /* Kludge? */
iter = util.Array.iterator(iter);
let ret = <></>;
let n = 0;
for each (let i in Iterator(iter))
{
let val = fn(i);
if (val == undefined)
continue;
if (sep && n++)
ret += sep;
if (interruptable && n % interruptable == 0)
liberator.threadYield(true, true);
ret += val;
}
return ret;
},
maybeXML: function maybeXML(xml)
{
if (typeof xml == "xml")
return xml;
try
{
return new XMLList(xml);
}
catch (e) {}
return <>{xml}</>;
},
completionRow: function completionRow(item, class)
{
if (typeof icon == "function")
icon = icon();
if (class)
{
var text = item[0] || "";
var desc = item[1] || "";
}
else
{
var text = this.process[0].call(this, item, item.text || this.getKey(item, "text"));
var desc = this.process[1].call(this, item, this.getKey(item, "description"));
}
return <div highlight={class || "CompItem"} style="white-space: nowrap">
<!-- The non-breaking spaces prevent empty elements
- from pushing the baseline down and enlarging
- the row.
-->
<li highlight="CompResult">{text}&#160;</li>
<li highlight="CompDesc">{desc}&#160;</li>
</div>;
},
bookmarkDescription: function (item, text)
{
let extra = this.getKey(item, "extra");
return <>
<a href="#" highlight="URL">{text}</a>&#160;
{
!(extra && extra.length) ? "" :
<span class="extra-info">
({
template.map(extra, function (e)
<>{e[0]}: <span highlight={e[2]}>{e[1]}</span></>,
<>&#xa0;</>/* Non-breaking space */)
})
</span>
}
</>
},
icon: function (item, text)
{
let icon = this.getKey(item, "icon");
return <><span highlight="CompIcon">{icon ? <img src={icon}/> : <></>}</span><span class="td-strut"/>{text}</>
},
filter: function (str) <span highlight="Filter">{str}</span>,
// if "processStrings" is true, any passed strings will be surrounded by " and
// any line breaks are displayed as \n
highlight: function highlight(arg, processStrings, clip)
{
// some objects like window.JSON or getBrowsers()._browsers need the try/catch
let str = clip ? util.clip(String(arg), clip) : String(arg);
try
{
switch (arg == null ? "undefined" : typeof arg)
{
case "number":
return <span highlight="Number">{str}</span>;
case "string":
if (processStrings)
str = str.quote();
return <span highlight="String">{str}</span>;
case "boolean":
return <span highlight="Boolean">{str}</span>;
case "function":
// Vim generally doesn't like /foo*/, because */ looks like a comment terminator.
// Using /foo*(:?)/ instead.
if (processStrings)
return <span highlight="Function">{str.replace(/\{(.|\n)*(?:)/g, "{ ... }")}</span>;
return <>{arg}</>;
case "undefined":
return <span highlight="Null">{arg}</span>;
case "object":
// for java packages value.toString() would crash so badly
// that we cannot even try/catch it
if (/^\[JavaPackage.*\]$/.test(arg))
return <>[JavaPackage]</>;
if (processStrings && false)
str = template.highlightFilter(str, "\n", function () <span highlight="NonText">^J</span>);
return <span highlight="Object">{str}</span>;
case "xml":
return arg;
default:
return <![CDATA[<unknown type>]]>;
}
}
catch (e)
{
return<![CDATA[<unknown>]]>;
}
},
highlightFilter: function highlightFilter(str, filter, highlight)
{
return this.highlightSubstrings(str, (function ()
{
if (filter.length == 0)
return;
let lcstr = String.toLowerCase(str);
let lcfilter = filter.toLowerCase();
let start = 0;
while ((start = lcstr.indexOf(lcfilter, start)) > -1)
{
yield [start, filter.length];
start += filter.length;
}
})(), highlight || template.filter);
},
highlightRegexp: function highlightRegexp(str, re, highlight)
{
return this.highlightSubstrings(str, (function ()
{
let res;
while ((res = re.exec(str)) && res[0].length)
yield [res.index, res[0].length];
})(), highlight || template.filter);
},
highlightSubstrings: function highlightSubstrings(str, iter, highlight)
{
if (typeof str == "xml")
return str;
if (str == "")
return <>{str}</>;
str = String(str).replace(" ", "\u00a0");
let s = <></>;
let start = 0;
let n = 0;
for (let [i, length] in iter)
{
if (n++ > 50) // Prevent infinite loops.
return s + <>{str.substr(start)}</>;
XML.ignoreWhitespace = false;
s += <>{str.substring(start, i)}</>;
s += highlight(str.substr(i, length));
start = i + length;
}
return s + <>{str.substr(start)}</>;
},
highlightURL: function highlightURL(str, force)
{
if (force || /^[a-zA-Z]+:\/\//.test(str))
return <a highlight="URL" href="#">{str}</a>;
else
return str;
},
generic: function generic(xml)
{
return <>:{commandline.getCommand()}<br/></> + xml;
},
// every item must have a .xml property which defines how to draw itself
// @param headers is an array of strings, the text for the header columns
genericTable: function genericTable(items, format)
{
this.listCompleter(function (context) {
context.filterFunc = null;
if (format)
context.format = format;
context.completions = items;
});
},
jumps: function jumps(index, elems)
{
return this.generic(
<table>
<tr style="text-align: left;" highlight="Title">
<th colspan="2">jump</th><th>title</th><th>URI</th>
</tr>
{
this.map(Iterator(elems), function ([idx, val])
<tr>
<td class="indicator">{idx == index ? ">" : ""}</td>
<td>{Math.abs(idx - index)}</td>
<td style="width: 250px; max-width: 500px; overflow: hidden;">{val.title}</td>
<td><a href="#" highlight="URL jump-list">{val.URI.spec}</a></td>
</tr>)
}
</table>);
},
options: function options(title, opts)
{
return this.generic(
<table>
<tr highlight="Title" align="left">
<th>--- {title} ---</th>
</tr>
{
this.map(opts, function (opt)
<tr>
<td>
<span style={opt.isDefault ? "" : "font-weight: bold"}>{opt.pre}{opt.name}</span><span>{opt.value}</span>
{opt.isDefault || opt.default == null ? "" : <span class="extra-info"> (default: {opt.default})</span>}
</td>
</tr>)
}
</table>);
},
table: function table(title, data, indent)
{
let table =
<table>
<tr highlight="Title" align="left">
<th colspan="2">{title}</th>
</tr>
{
this.map(data, function (datum)
<tr>
<td style={"font-weight: bold; min-width: 150px; padding-left: " + (indent || "2ex")}>{datum[0]}</td>
<td>{template.maybeXML(datum[1])}</td>
</tr>)
}
</table>;
if (table.tr.length() > 1)
return table;
},
tabular: function tabular(headings, style, iter)
{
/* This might be mind-bogglingly slow. We'll see. */
return this.generic(
<table>
<tr highlight="Title" align="left">
{
this.map(headings, function (h)
<th>{h}</th>)
}
</tr>
{
this.map(iter, function (row)
<tr>
{
template.map(Iterator(row), function ([i, d])
<td style={style[i] || ""}>{d}</td>)
}
</tr>)
}
</table>);
},
usage: function usage(iter)
{
return this.generic(
<table>
{
this.map(iter, function (item)
<tr>
<td highlight="Title" style="padding-right: 20px">{item.name || item.names[0]}</td>
<td>{item.description}</td>
</tr>)
}
</table>);
}
};
// vim: set fdm=marker sw=4 ts=4 et:

1860
common/content/ui.js Normal file

File diff suppressed because it is too large Load Diff

597
common/content/util.js Normal file
View File

@@ -0,0 +1,597 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
const XHTML = "http://www.w3.org/1999/xhtml";
const NS = Namespace("liberator", "http://vimperator.org/namespaces/liberator");
default xml namespace = XHTML;
const util = { //{{{
Array: {
// [["a", "b"], ["c", "d"]] -> { a: "b", c: "d" }
// From Common Lisp, more or less
assocToObj: function assocToObj(assoc)
{
let obj = {};
assoc.forEach(function ([k, v]) { obj[k] = v });
return obj;
},
// flatten an array: [["foo", ["bar"]], ["baz"], "quux"] -> ["foo", ["bar"], "baz", "quux"]
flatten: function flatten(ary) Array.concat.apply([], ary),
iterator: function iterator(ary)
{
let length = ary.length;
for (let i = 0; i < length; i++)
yield ary[i];
},
iterator2: function (ary)
{
let length = ary.length;
for (let i = 0; i < length; i++)
yield [i, ary[i]];
},
uniq: function uniq(ary, unsorted)
{
let ret = [];
if (unsorted)
{
for (let [,item] in Iterator(ary))
if (ret.indexOf(item) == -1)
ret.push(item);
}
else
{
for (let [,item] in Iterator(ary.sort()))
{
if (item != last || !ret.length)
ret.push(item);
var last = item;
}
}
return ret;
}
},
// TODO: class could have better variable names/documentation
Timer: function Timer(minInterval, maxInterval, callback)
{
let timer = Components.classes["@mozilla.org/timer;1"]
.createInstance(Components.interfaces.nsITimer);
this.doneAt = 0;
this.latest = 0;
this.notify = function (aTimer)
{
timer.cancel();
this.latest = 0;
/* minInterval is the time between the completion of the command and the next firing. */
this.doneAt = Date.now() + minInterval;
try
{
callback(this.arg);
}
finally
{
this.doneAt = Date.now() + minInterval;
}
};
this.tell = function (arg)
{
if (arg !== undefined)
this.arg = arg;
let now = Date.now();
if (this.doneAt == -1)
timer.cancel();
let timeout = minInterval;
if (now > this.doneAt && this.doneAt > -1)
timeout = 0;
else if (this.latest)
timeout = Math.min(timeout, this.latest - now);
else
this.latest = now + maxInterval;
timer.initWithCallback(this, Math.max(timeout, 0), timer.TYPE_ONE_SHOT);
this.doneAt = -1;
};
this.reset = function ()
{
timer.cancel();
this.doneAt = 0;
};
this.flush = function ()
{
if (this.latest)
this.notify();
};
},
cloneObject: function cloneObject(obj)
{
if (obj instanceof Array)
return obj.slice();
let newObj = {};
for (let [k, v] in Iterator(obj))
newObj[k] = v;
return newObj;
},
clip: function clip(str, length)
{
return str.length <= length ? str : str.substr(0, length - 3) + "...";
},
compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()),
computedStyle: function computedStyle(node)
{
while (node instanceof Text && node.parentNode)
node = node.parentNode;
return node.ownerDocument.defaultView.getComputedStyle(node, null);
},
copyToClipboard: function copyToClipboard(str, verbose)
{
const clipboardHelper = Components.classes["@mozilla.org/widget/clipboardhelper;1"]
.getService(Components.interfaces.nsIClipboardHelper);
clipboardHelper.copyString(str);
if (verbose)
liberator.echo("Yanked " + str, commandline.FORCE_SINGLELINE);
},
createURI: function createURI(str)
{
const fixup = Components.classes["@mozilla.org/docshell/urifixup;1"]
.getService(Components.interfaces.nsIURIFixup);
return fixup.createFixupURI(str, fixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
},
escapeHTML: function escapeHTML(str)
{
// XXX: the following code is _much_ slower than a simple .replace()
// :history display went down from 2 to 1 second after changing
//
// var e = window.content.document.createElement("div");
// e.appendChild(window.content.document.createTextNode(str));
// return e.innerHTML;
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
},
escapeRegex: function escapeRegex(str)
{
return str.replace(/([\\{}()[\].?*+])/g, "\\$1");
},
escapeString: function escapeString(str, delimiter)
{
if (delimiter == undefined)
delimiter = '"';
return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
},
formatBytes: function formatBytes(num, decimalPlaces, humanReadable)
{
const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
let unitIndex = 0;
let tmpNum = parseInt(num, 10) || 0;
let strNum = [tmpNum + ""];
if (humanReadable)
{
while (tmpNum >= 1024)
{
tmpNum /= 1024;
if (++unitIndex > (unitVal.length - 1))
break;
}
let decPower = Math.pow(10, decimalPlaces);
strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2);
if (!strNum[1])
strNum[1] = "";
while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces)
strNum[1] += "0";
}
for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000
strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u);
if (unitIndex) // decimalPlaces only when > Bytes
strNum[0] += "." + strNum[1];
return strNum[0] + " " + unitVal[unitIndex];
},
// generates an Asciidoc help entry, "command" can also be a mapping
generateHelp: function generateHelp(command, extraHelp)
{
let start = "", end = "";
if (command instanceof liberator.Command)
start = ":";
else if (command instanceof liberator.Option)
start = end = "'";
let ret = "";
let longHelp = false;
if ((command.help && command.description) && (command.help.length + command.description.length) > 50)
longHelp = true;
// the tags which are printed on the top right
for (let j = command.names.length - 1; j >= 0; j--)
ret += "|" + start + command.names[j] + end + "| ";
if (longHelp)
ret += "+";
ret += "\n";
// the usage information for the command
let usage = command.names[0];
if (command.specs) // for :commands
usage = command.specs[0];
usage = usage.replace(/{/, "\\\\{").replace(/}/, "\\\\}");
usage = usage.replace(/'/, "\\'").replace(/`/, "\\`");
ret += "||" + start + usage + end + "||";
if (usage.length > 15)
ret += " +";
ret += "\n________________________________________________________________________________\n";
// the actual help text
if (command.description)
{
ret += command.description + "."; // the help description
if (extraHelp)
ret += " +\n" + extraHelp;
}
else
ret += "Sorry, no help available";
// add more space between entries
ret += "\n________________________________________________________________________________\n\n\n";
return ret;
},
httpGet: function httpGet(url, callback)
{
try
{
let xmlhttp = new XMLHttpRequest();
xmlhttp.mozBackgroundRequest = true;
if (callback)
{
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4)
callback(xmlhttp)
}
}
xmlhttp.open("GET", url, !!callback);
xmlhttp.send(null);
return xmlhttp;
}
catch (e)
{
liberator.log("Error opening " + url + ": " + e, 1);
}
},
identity: function identity(k) k,
map: function map(obj, fn)
{
let ary = [];
for (let i in Iterator(obj))
ary.push(fn(i));
return ary;
},
// if color = true it uses HTML markup to color certain items
objectToString: function objectToString(object, color)
{
/* Use E4X literals so html is automatically quoted
* only when it's asked for. Noone wants to see &lt;
* on their console or :map :foo in their buffer
* when they expect :map <C-f> :foo.
*/
XML.prettyPrinting = false;
XML.ignoreWhitespace = false;
if (object === null)
return "null\n";
if (typeof object != "object")
return false;
try
{ // for window.JSON
var obj = String(object);
}
catch (e)
{
obj = "[Object]";
}
obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () <span highlight="NonText">^J</span>);
let string = <><span highlight="Title Object">{obj}</span>::<br/>&#xa;</>;
let keys = [];
try // window.content often does not want to be queried with "var i in object"
{
let hasValue = !("__iterator__" in object);
if (modules.isPrototypeOf(object))
{
object = Iterator(object);
hasValue = false;
}
for (let i in object)
{
let value = <![CDATA[<no value>]]>;
try
{
value = object[i];
}
catch (e) {}
if (!hasValue)
{
if (i instanceof Array && i.length == 2)
[i, value] = i;
else
var noVal = true;
}
value = template.highlight(value, true, 150);
// FIXME: Inline style.
key = <span style="font-weight: bold;">{i}</span>;
if (!isNaN(i))
i = parseInt(i);
else if (/^[A-Z_]+$/.test(i))
i = "";
keys.push([i, <>{key}{noVal ? "" : <>: {value}</> // Vim /
}<br/>&#xa;</>]);
}
}
catch (e) {}
function compare(a, b)
{
if (!isNaN(a[0]) && !isNaN(b[0]))
return a[0] - b[0];
return String.localeCompare(a[0], b[0]);
}
string += template.map(keys.sort(compare), function (f) f[1]);
return color ? string : [s for each (s in string)].join("");
},
range: function range(start, end, reverse)
{
if (!reverse)
{
while (start < end)
yield start++;
}
else
{
while (start > end)
yield --start;
}
},
interruptableRange: function interruptableRange(start, end, time)
{
let endTime = Date.now() + time;
while (start < end)
{
if (Date.now() > endTime)
{
liberator.threadYield(true, true);
endTime = Date.now() + time;
}
yield start++;
}
},
// same as Firefox's readFromClipboard function, but needed for apps like Thunderbird
readFromClipboard: function readFromClipboard()
{
let url;
try
{
const clipboard = Components.classes['@mozilla.org/widget/clipboard;1']
.getService(Components.interfaces.nsIClipboard);
const transferable = Components.classes['@mozilla.org/widget/transferable;1']
.createInstance(Components.interfaces.nsITransferable);
transferable.addDataFlavor("text/unicode");
if (clipboard.supportsSelectionClipboard())
clipboard.getData(transferable, clipboard.kSelectionClipboard);
else
clipboard.getData(transferable, clipboard.kGlobalClipboard);
let data = {};
let dataLen = {};
transferable.getTransferData("text/unicode", data, dataLen);
if (data)
{
data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
url = data.data.substring(0, dataLen.value / 2);
}
}
catch (e) {}
return url;
},
// takes a string like 'google bla, www.osnews.com'
// and returns an array ['www.google.com/search?q=bla', 'www.osnews.com']
stringToURLArray: function stringToURLArray(str)
{
let urls = str.split(new RegExp("\s*" + options["urlseparator"] + "\s*"));
return urls.map(function (url) {
try
{
let file = io.getFile(url);
if (file.exists() && file.isReadable())
return file.path;
}
catch (e) {}
// removes spaces from the string if it starts with http:// or something like that
if (/^\w+:\/\//.test(url))
url = url.replace(/\s+/g, "");
// strip each 'URL' - makes things simpler later on
url = url.replace(/^\s+|\s+$/, "");
// if the string doesn't look like a valid URL (i.e. contains a space
// or does not contain any of: .:/) try opening it with a search engine
// or keyword bookmark
if (/\s/.test(url) || !/[.:\/]/.test(url))
{
// TODO: it would be clearer if the appropriate call to
// getSearchURL was made based on whether or not the first word was
// indeed an SE alias rather than seeing if getSearchURL can
// process the call usefully and trying again if it fails - much
// like the comments below ;-)
// check for a search engine match in the string
let searchURL = bookmarks.getSearchURL(url, false);
if (searchURL)
{
return searchURL;
}
else // no search engine match, search for the whole string in the default engine
{
searchURL = bookmarks.getSearchURL(url, true);
if (searchURL)
return searchURL;
}
}
// if we are here let Firefox handle the url and hope it does
// something useful with it :)
return url;
});
},
xmlToDom: function xmlToDom(node, doc, nodes)
{
XML.prettyPrinting = false;
switch (node.nodeKind())
{
case "text":
return doc.createTextNode(node);
case "element":
let domnode = doc.createElementNS(node.namespace(), node.localName());
for each (let attr in node.@*)
domnode.setAttributeNS(attr.name() == "highlight" ? NS.uri : attr.namespace(), attr.name(), String(attr));
for each (let child in node.*)
domnode.appendChild(arguments.callee(child, doc, nodes));
if (nodes && node.@key)
nodes[node.@key] = domnode;
return domnode;
}
}
}; //}}}
// Struct is really slow, AT LEAST 5 times slower than using structs or simple Objects
// main reason is the function ConStructor(), which i couldn't get faster.
// Maybe it's a TraceMonkey problem, for now don't use it for anything which must be fast (like bookmarks or history)
function Struct()
{
let self = this instanceof Struct ? this : new Struct();
if (!arguments.length)
return self;
let args = Array.slice(arguments);
self.__defineGetter__("length", function () args.length);
self.__defineGetter__("members", function () args.slice());
for (let arg in Iterator(args))
{
let [i, name] = arg;
self.__defineGetter__(name, function () this[i]);
self.__defineSetter__(name, function (val) { this[i] = val; });
}
function ConStructor()
{
let self = this instanceof arguments.callee ? this : new arguments.callee();
//for (let [k, v] in Iterator(Array.slice(arguments))) // That is makes using struct twice as slow as the following code:
for (let i = 0; i < arguments.length; i++)
{
if (arguments[i] != undefined)
self[i] = arguments[i];
}
return self;
}
ConStructor.prototype = self;
ConStructor.defaultValue = function (key, val)
{
let i = args.indexOf(key);
ConStructor.prototype.__defineGetter__(i, function () (this[i] = val.call(this), this[i])); // Kludge for FF 3.0
ConStructor.prototype.__defineSetter__(i, function (val) {
let value = val;
this.__defineGetter__(i, function () value);
this.__defineSetter__(i, function (val) { value = val });
});
};
return self.constructor = ConStructor;
}
Struct.prototype = {
clone: function clone()
{
return this.constructor.apply(null, this.slice());
},
// Iterator over our named members
__iterator__: function ()
{
let self = this;
return ([v, self[v]] for ([k, v] in Iterator(self.members)))
}
}
// Add no-sideeffect array methods. Can't set new Array() as the prototype or
// get length() won't work.
for (let [,k] in Iterator(["concat", "every", "filter", "forEach", "indexOf", "join", "lastIndexOf",
"map", "reduce", "reduceRight", "reverse", "slice", "some", "sort"]))
Struct.prototype[k] = Array.prototype[k];
// vim: set fdm=marker sw=4 ts=4 et:

56
common/make_jar.sh Normal file
View File

@@ -0,0 +1,56 @@
#!/bin/dash
set -e
top=$(pwd)
jar=$1
bases=$2
dirs=$3
text=$4
bin=$5
shift 5;
files="$@"
stage="$top/${jar%.*}"
mkdir -p $stage
getfiles () {
filter="\.($(echo $1 | tr ' ' '|'))$"; shift
find "$@" -not -path '*CVS*' 2>/dev/null | grep -E "$filter" || true
}
copytext () {
sed -e "s,###VERSION###,$VERSION,g" \
-e "s,###DATE###,$DATE,g" \
<"$1" >"$2"
cmp -s "$1" "$2" ||
( echo "modified: $1"; diff -u "$1" "$2" | grep '^[-+][^-+]' )
}
for base in $bases
do
(
set -e
cd $base
[ ${jar##*.} = jar ] && stage="$stage/${base##*/}"
for dir in $dirs
do
for f in $(getfiles "$bin" "$dir")
do
mkdir -p "$stage/${f%/*}"
cp $f "$stage/$f"
done
for f in $(getfiles "$text" "$dir")
do
mkdir -p "$stage/${f%/*}"
copytext "$f" "$stage/$f"
done
done
for f in $files
do
[ -f "$f" ] && copytext "$f" "$stage/$f"
done
)
done
cd $stage; zip -r "$top/$jar" *
rm -rf "$stage"

234
common/modules/storage.jsm Normal file
View File

@@ -0,0 +1,234 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
© 2008: Kris Maglione <maglione.k at Gmail>
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
var EXPORTED_SYMBOLS = ["storage"];
var prefService = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefService)
.getBranch("extensions.liberator.datastore.");
var json = Components.classes["@mozilla.org/dom/json;1"]
.createInstance(Components.interfaces.nsIJSON);
function getCharPref(name)
{
try
{
return prefService.getComplexValue(name, Components.interfaces.nsISupportsString).data;
}
catch (e) {}
}
function setCharPref(name, value)
{
var str = Components.classes['@mozilla.org/supports-string;1']
.createInstance(Components.interfaces.nsISupportsString);
str.data = value;
return prefService.setComplexValue(name, Components.interfaces.nsISupportsString, str);
}
function loadPref(name, store, type)
{
if (store)
var pref = getCharPref(name);
if (pref)
var result = json.decode(pref);
if (result instanceof type)
return result;
}
function savePref(obj)
{
if (obj.store)
setCharPref(obj.name, obj.serial)
}
var prototype = {
fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg) },
save: function () { savePref(this) },
};
function ObjectStore(name, store, data)
{
var object = data || {};
this.__defineGetter__("store", function () store);
this.__defineGetter__("name", function () name);
this.__defineGetter__("serial", function () json.encode(object));
this.set = function set(key, val)
{
var defined = key in object;
var orig = object[key];
object[key] = val;
if (!defined)
this.fireEvent("add", key);
else if (orig != val)
this.fireEvent("change", key);
};
this.remove = function remove(key)
{
var ret = object[key];
delete object[key];
this.fireEvent("remove", key);
return ret;
};
this.get = function get(val) object[val];
this.clear = function ()
{
object = {};
};
this.__iterator__ = function () Iterator(object);
}
ObjectStore.prototype = prototype;
function ArrayStore(name, store, data)
{
var array = data || [];
this.__defineGetter__("store", function () store);
this.__defineGetter__("name", function () name);
this.__defineGetter__("serial", function () json.encode(array));
this.__defineGetter__("length", function () array.length);
this.set = function set(index, value)
{
var orig = array[index];
array[index] = value;
this.fireEvent("change", index);
};
this.push = function push(value)
{
array.push(value);
this.fireEvent("push", array.length);
};
this.pop = function pop(value)
{
var ret = array.pop();
this.fireEvent("pop", array.length);
return ret;
};
this.truncate = function truncate(length, fromEnd)
{
var ret = array.length;
if (array.length > length)
{
if (fromEnd)
array.splice(0, array.length - length);
array.length = length;
this.fireEvent("truncate", length);
}
return ret;
};
// XXX: Awkward.
this.mutate = function mutate(aFuncName)
{
var funcName = aFuncName;
arguments[0] = array;
array = Array[funcName].apply(Array, arguments);
};
this.get = function get(index)
{
return index >= 0 ? array[index] : array[array.length + index];
};
this.__iterator__ = function () Iterator(array);
}
ArrayStore.prototype = prototype;
var keys = {};
var observers = {};
var storage = {
newObject: function newObject(key, constructor, store, type, reload)
{
if (!(key in keys) || reload)
{
if (key in this && !reload)
throw Error;
keys[key] = new constructor(key, store, loadPref(key, store, type || Object));
this.__defineGetter__(key, function () keys[key]);
}
return keys[key];
},
newMap: function newMap(key, store)
{
return this.newObject(key, ObjectStore, store);
},
newArray: function newArray(key, store)
{
return this.newObject(key, ArrayStore, store, Array);
},
addObserver: function addObserver(key, callback)
{
if (!(key in observers))
observers[key] = [];
if (observers[key].indexOf(callback) == -1)
observers[key].push(callback);
},
removeObserver: function (key, callback)
{
if (!(key in observers))
return;
observers[key] = observers[key].filter(function (elem) elem != callback);
if (observers[key].length == 0)
delete obsevers[key];
},
fireEvent: function fireEvent(key, event, arg)
{
for each (callback in observers[key])
callback(key, event, arg);
},
save: function save(key)
{
savePref(keys[key]);
},
saveAll: function storeAll()
{
for each (obj in keys)
savePref(obj);
},
};
// vim: set fdm=marker sw=4 sts=4 et ft=javascript:

View File

@@ -0,0 +1,11 @@
{ content = $1 ~ /^(content|skin|locale)$/ }
content && $NF ~ /^[a-z]/ { $NF = "/" name "/" $NF }
content {
sub(/^\.\./, "", $NF);
$NF = "jar:chrome/" name ".jar!" $NF
}
{
sub("^\\.\\./liberator/", "", $NF)
print
}

BIN
common/skin/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

204
common/skin/liberator.css Normal file
View File

@@ -0,0 +1,204 @@
/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/
@namespace liberator url("http://vimperator.org/namespaces/liberator");
/* Applied to all content */
[liberator|activeframe] {
-moz-binding: url(chrome://liberator/content/bindings.xml#frame);
}
[liberator|highlight~=HintImage],
[liberator|highlight~=Hint] {
z-index: 5000;
position: absolute;
}
[liberator|highlight~=Search] {
display: inline;
}
/* Applied only to completion buffer and MOW */
@-moz-document
url-prefix(chrome://liberator/) {
*:-moz-loading, *:-moz-broken { display: none !important; }
[liberator|highlight~=Completions] {
width: 100%;
display: table;
}
[liberator|highlight~=CompItem],
[liberator|highlight~=CompTitle] {
display: table-row;
}
[liberator|highlight~=Completions] > ul {
display: table-row;
}
[liberator|highlight~=CompItem] > *,
[liberator|highlight~=CompTitle] > * {
-moz-binding: url(chrome://liberator/content/bindings.xml#compitem-td);
display: table-cell;
vertical-align: middle;
}
[liberator|highlight~=CompMsg] {
height: 1.5em;
line-height: 1.5em !important;
}
.td-span {
display: inline-block;
overflow: visible;
width: 0px;
height: 1.5em;
line-height: 1.5em !important;
}
.td-strut {
display: inline-block;
vertical-align: middle;
height: 16px;
width: 0px;
}
.extra-info { color: gray; }
.times-executed, .time-average { color: green; }
.time-total { color: red; }
}
/* Applied to completion buffer, MOW, browser window */
@-moz-document
url-prefix(chrome://) {
#liberator-container {
font-family: monospace;
}
#liberator-completions {
-moz-user-focus: ignore;
overflow: -moz-scrollbars-none !important; /* does not seem to work fully */
border-width: 0px !important;
/*-moz-appearance: none !important; /* prevent an ugly 3D border */
}
/* fixes the min-height: 22px from firefox */
#status-bar, statusbarpanel {
-moz-appearance: none !important;
min-height: 18px !important;
border: none !important;
font-weight: bold;
font-family: monospace;
}
#liberator-statusline {
font-family: monospace;
margin: 0px;
}
#liberator-statusline > label {
padding: 0px 0px 0px 8px;
}
#liberator-statusline > label:first-child {
padding: 0px;
}
#liberator-statusline-field-url {
background-color: inherit;
color: inherit;
}
/* no longer at the window's bottom right corner */
.statusbar-resizerpanel {
display: none;
}
#statusbar-display, #statusbar-progresspanel {
display: none;
}
#liberator-commandline {
padding: 1px;
font-family: monospace;
/*
background-color: white;
color: black;
*/
}
#liberator-commandline-prompt {
margin: 0px;
padding: 0px;
background-color: inherit;
}
#liberator-commandline-command {
background-color: inherit;
color: inherit;
}
#liberator-visualbell {
border: none;
background-color: black;
}
#sidebar {
max-width: 90% !important;
min-width: 10% !important;
}
/* MOW */
#liberator-completions, #liberator-multiline-output, #liberator-multiline-input {
overflow: hidden;
background-color: white;
color: black;
}
#liberator-completions-content, #liberator-multiline-output-content, #liberator-multiline-input {
white-space: pre;
font-family: -moz-fixed;
margin: 0px;
}
#liberator-completions-content *, #liberator-multiline-output-content * {
font: inherit;
}
#liberator-completions-content table, #liberator-multiline-output-content table {
white-space: inherit;
border-spacing: 0px;
}
#liberator-completions-content td, #liberator-multiline-output-content td,
#liberator-completions-content th, #liberator-multiline-output-content th {
padding: 0px 2px;
}
/* for muttator's composer */
#content-frame, #appcontent {
border: 0px;
}
}
/* vim: set fdm=marker sw=4 ts=4 et: */