CmdUtils: Overhauled makeSearchCommand(). Made absUrl() slightly faster.

* No longer requires parser.title to contain an <a>.
* Added/Renamed parser options.
** parser.html
** parser.body <- parser.preview
** parser.baseUrl <- parser.baseurl
* Refined the doc.
* Added To(Do|Localize)s.
This commit is contained in:
satyr 2010-03-19 10:15:01 +09:00
Родитель 95104ae621
Коммит 7cb3a6a823
2 изменённых файлов: 226 добавлений и 261 удалений

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

@ -180,7 +180,7 @@ function getHiddenWindow() Utils.hiddenWindow;
// as reference names aren't cannonical across feeds.
//
// {{{id}}} is the id or name of the command.
function getCommand(id) commandSource.getCommand(id);
// === {{{ CmdUtils.executeCommand(command, args) }}} ===
@ -837,74 +837,70 @@ function CreateAlias(options) {
}
// === {{{ CmdUtils.makeSearchCommand(options) }}} ===
// A specialized version of {{{CmdUtils.CreateCommand()}}}, this lets
// A specialized version of {{{CmdUtils.CreateCommand()}}}. This lets
// you make commands that interface with search engines, without
// having to write so much boilerplate code.
// Also see https://wiki.mozilla.org/Labs/Ubiquity/Writing_A_Search_Command .
//
// {{{options}}} as the argument of {{{CmdUtils.CreateCommand()}}},
// {{{options}}} is same as the argument of {{{CmdUtils.CreateCommand()}}},
// except that instead of {{{options.arguments}}}, {{{options.execute}}},
// and {{{options.preview}}} you only need a single property:
// and {{{options.preview}}}, you only need a single property:
// *{{{url}}}\\
// The URL of a search results page from the search
// engine of your choice. Must contain the literal string {{{{QUERY}}}} or
// {{{%s}}}, which will be replaced with the user's search term
// to generate a URL that should point to the correct page of search
// results. (We're assuming that the user's search term appears in
// the URL of the search results page, which is true for most search
// engines.) For example: {{{http://www.google.com/search?q={QUERY}}}}
// The URL of a search results page from the search engine of your choice.
// Must contain the literal string {{{{QUERY}}}} or {{{%s}}}, which will be
// replaced with the user's search term to generate a URL that should point to
// the correct page of search results. (We're assuming that the user's search
// term appears in the URL of the search results page, which is true for most
// search engines.) For example: {{{http://www.google.com/search?q={QUERY}}}}
//
// Also note that {{{options.icon}}} if not passed, will be generated from
// the URL passed in {{{options.url}}}, and {{{options.description}}} if
// not passed, will be auto generated from a template and {{{options.name}}}.
//
// The {{{options.execute}}}, {{{options.preview}}}, and
// {{{options.takes}}} properties are all automatically generated for you
// from {{{options.url}}}, so all you need to provide is {{{options.url}}}
// and {{{options.name}}}. You can choose to provide other optional
// properties, which work the same way as they do for
// {{{CmdUtils.CreateCommand()}}}. You can also override the auto-generated
// {{{preview()}}} function by providing your own as {{{options.preview}}}.
// If not specified, {{{options.name}}}, {{{options.icon}}},
// {{{options.description}}}, {{{options.execute}}} will be auto generated.
//
// Other optional parameters of {{{options}}} are:
// *{{{postData}}}\\
// Will make the command use POST instead of GET,
// and the data (key:value pairs or string) are all passed to the URL passed in
// {{{options.url}}}. Instead of passing the search params in the URL, pass
// it (along with any other params) like so:
// {{{
// postData: {"q": "{QUERY}", "hl": "en"}
// postData: "q={QUERY}&hl=en"
// }}}
// When this is done, the query will be substituted in as usual.
//
// Makes the command use POST instead of GET, and the data
// (key:value pairs or string) are all passed to the {{{options.url}}}.
// Instead of including the search params in the URL, pass it
// (along with any other params) like so:
// {{{ {"q": "{QUERY}", "hl": "en"} }}} or {{{ "q={QUERY}&hl=en" }}}.
// When this is done, the query will be substituted in as usual.
// *{{{defaultUrl}}}\\
// Specifies the URL that will be opened in the case
// where the user has not provided a search string.
//
// An extra option {{{options.parser}}} can be passed, which will make
// Ubiquity automatically generate a keyboard navigatable preview of
// the results. It is passed as an object containing at the very least
// {{{options.parser.title}}}, either a jQuery selector that matches the
// titles of the results or a function given a single argument (the container
// of the result) that must return a string to be used as title.
// It is highly recommended that you include {{{options.parser.container}}},
// a jQuery selector that will match an element that groups
// result-data. If this is not passed, Ubiquity will fall back to a
// fragile method of pairing titles, previews and thumbnails, which
// might not always work. {{{options.parser.preview}}} can either be a
// jQuery selector that will match the preview returned by the search
// provider or a function that will receive a single argument (the
// container grouping the result-data) and must return a string that will
// be used as preview or a jQuery object; {{{options.parser.baseurl}}},
// a string that will be prefixed to relative links, such that relative paths
// will still work out of context. If not passed, it will be auto-generated
// from {{{options.url}}} (and thus //may// be incorrect)
// {{{options.parser.thumbnail}}}, a jQuery selector that will match a
// thumbnail which will automatically be displayed in the
// preview. Note: if it doesn't point to an {{{<img>}}} element,
// ubiquity will try and find a child of the node of type {{{img}}}
// inside the element, and use the first-found one.
// {{{options.parser.maxResults (= 4)}}} specifies the max number of results.
// {{{options.charset}}} specifies the query charset.
// A URL string that will be opened in the case
// where the user has not provided a search string.
// *{{{charset}}}\\
// A string specifying the character set of query.
// *{{{parser}}}\\
// Generates keyboard navigatable previews by parsing the search results.
// It is passed as an object containing following properties.
// The ones marked as //path// expect either a jQuery selector string,
// a JSON path string (like {{{"granma.mom.me"}}}). Each of them can also be
// a filter function that receivs a parent context and returns a result of
// same type (jQuery object or string).
// *{{{parser.type}}}\\
// A string that's passed to {{{jQuery.ajax()}}}'s {{{type}}} parameter when
// requesting. If {{{"json"}}}, the parser expects JSON paths.
// *{{{parser.title}}}\\
// //Required//. The //path// to the title of each result.
// *{{{parser.container}}} //Recommended// //Path//\\
// A //path// to each container that groups each of
// title/body/href/thumbnail result sets.
// *{{{parser.body}}}\\
// A //path// to the content of each result.
// *{{{parser.href}}}\\
// A //path// to the URL of each result.
// Should point to an {{{<a>}}} if jQuery mode.
// *{{{parser.thumbnail}}} //Path//\\
// A //path// to the thumbnail URL of each result.
// Should point to an {{{<img>}}} if jQuery mode.
// *{{{parser.baseUrl}}}\\
// A URL string that will be the base for relative links, such that they will
// still work out of context. If not passed, it will be auto-generated from
// {{{options.url}}} (and thus //may// be incorrect).
// *{{{parser.maxResults}}}\\
// An integer specifying the max number of results. Defaults to 4.
// *{{{parser.html}}}\\
// JSON mode only. An array of strings specifying //path//s
// that should be treated as HTML.
//
// Examples:
// {{{
@ -936,7 +932,6 @@ function CreateAlias(options) {
// });
//
// CmdUtils.makeSearchCommand({
// names: ["video.baidu", "\u767E\u5EA6\u89C6\u9891"],
// url: "http://video.baidu.com/v?word={QUERY}",
// charset: "gb2312",
// parser: {
@ -949,197 +944,167 @@ function CreateAlias(options) {
// }}}
function makeSearchCommand(options) {
const {jQuery, noun_arb_text} = this.__globalObject, CU = this;
function insertQuery(target, query, charset) {
var re = /%s|{QUERY}/g;
var fn = charset ? escape : encodeURIComponent;
if (charset) query = Utils.convertFromUnicode(charset, query);
if (typeof target === "object") {
var ret = {};
for (var key in target) ret[key] = target[key].replace(re, query);
return ret;
}
return target && target.replace(re, fn(query));
}
options.arguments = {"object search term": noun_arb_text};
options.execute = function searchExecute({object: {text}}) {
if (!text && "defaultUrl" in options)
Utils.openUrlInBrowser(options.defaultUrl);
else
Utils.openUrlInBrowser(
insertQuery(options.url, text, charset),
insertQuery(options.postData, text, charset));
};
var [baseurl, domain] = /^.*?:\/\/([^?#/]+)/(options.url) || [""];
var [baseUrl, domain] = /^\w+:\/\/([^?#/]+)/(options.url) || [""];
var [name] = [].concat(options.names || options.name);
if (!name) name = options.name = domain;
var htmlName = Utils.escapeHtml(name);
var {charset} = options;
if (!("icon" in options)) {
// guess where the favicon is
options.icon = baseurl + "/favicon.ico";
}
if (!("description" in options)) {
// generate description from the name of the seach command
// options.description = "Searches " + htmlName + " for your words.";
if (!("icon" in options)) options.icon = baseUrl + "/favicon.ico";
if (!("description" in options))
// "Searches %s for your words."
options.description = L(
"ubiquity.cmdutils.searchdescription",
"defaultUrl" in options ? htmlName.link(options.defaultUrl) : htmlName);
}
if ("parser" in options) {
let {parser} = options;
if ("type" in parser) parser.type = parser.type.toLowerCase();
if (!("baseurl" in parser)) parser.baseurl = baseurl;
}
"preview" in options || (options.preview = function searchPreview(pblock,
args) {
const Klass = "search-command";
var {text, html} = args.object;
if (!text) return void this.previewDefault(pblock);
var {parser} = options;
//errorToLocalize
pblock.innerHTML = (
"<div class='" + Klass + "'>" +
L("ubiquity.cmdutils.searchcmd", htmlName, html) +
(parser ? "<p class='loading'>Loading results...</p>" : "") +
"</div>");
if (!parser) return;
var url = insertQuery(parser.url || options.url, text, charset);
if ("postData" in options)
var postData = insertQuery(options.postData, text, charset);
function searchParser(data) {
var template = "", results = [], sane = true;
//errorToLocalize
if (!data)
template = "<p class='error'>Error parsing search results.</p>";
else if (parser.type === "json") {
for each (let p in parser.container.split(".")) data = data[p];
for (let key in data) {
let result = {}, d = data[key];
result.title = d[parser.title];
result.href = d[parser.href];
if ("preview" in parser)
result.preview = (typeof parser.preview === "function"
? parser.preview(d)
: d[parser.preview]);
if ("thumbnail" in parser)
result.thumbnail = d[parser.thumbnail];
results.push(result);
}
if (!("arguments" in options) || !("argument" in options))
options.argument = this.__globalObject.noun_arb_text;
if (!("execute" in options)) options.execute = makeSearchCommand.execute;
if (!("preview" in options)) {
options.preview = makeSearchCommand.preview;
if ("parser" in options) let ({parser} = options) {
function fallback(n3w, old) {
if (n3w in parser || !(old in parser)) return;
Utils.reportWarning(
"makeSearchCommand: parser." + old + " is deprecated. " +
"Use parser." + n3w + " instead.", 2);
parser[n3w] = parser[old];
}
else {
let div = pblock.ownerDocument.createElement("div");
div.innerHTML = data;
let doc = jQuery(div);
if ("container" in parser) {
doc.find(parser.container).each(function eachContainer() {
let result = {}, $this = jQuery(this);
result.title = (typeof parser.title === "function"
? parser.title(this)
: $this.find(parser.title));
if ("preview" in parser)
result.preview = (typeof parser.preview === "function"
? parser.preview(this)
: $this.find(parser.preview));
if ("href" in parser)
result.href = (typeof parser.href === "function"
? parser.href(this)
: $this.find(parser.href));
if ("thumbnail" in parser)
result.thumbnail = $this.find(parser.thumbnail);
results.push(result);
});
}
else {
//errorToLocalize
Utils.reportWarning(name + " : " +
"falling back to fragile parsing");
let titles = doc.find(parser.title);
if ("preview" in parser) {
var previews = doc.find(parser.preview);
sane = titles.length === previews.length;
}
if ("thumbnail" in parser) {
var thumbnails = doc.find(parser.thumbnail);
sane = titles.length === thumbnails.length;
}
for (let i = 0, len = titles.length; i < len; ++i) {
let result = {title: titles.eq(i)};
if (sane && previews)
result.preview = previews.eq(i);
if (sane && thumbnails)
result.thumbnail = thumbnails.eq(i);
results.push(result);
}
}
results = results.filter(function filterResults(result) {
var {title, thumbnail, preview, href} = result;
if (!(title || "").length) return false;
if (!href) {
if (title[0].nodeName !== "A") title = title.find("A:first");
result.href = title.attr("href");
}
result.title = title.html();
if ((thumbnail || "").length) {
if (thumbnail[0].nodeName !== "IMG")
thumbnail = thumbnail.find("img:first");
result.thumbnail = thumbnail.attr("src");
}
if (preview && typeof preview !== "string")
result.preview = preview.html();
return true;
});
}
if (data && results.length) {
template = "<dl class='list'>";
let max = Math.min(results.length, parser.maxResults || 4);
let {baseurl} = parser;
let {escapeHtml, uri} = Utils;
for (let i = 0; i < max; ++i) {
let result = results[i], key = i < 35 ? (i+1).toString(36) : "-";
template += (
"<dt class='title'><kbd>" + key + "</kbd> <a href='" +
escapeHtml(uri({uri: result.href, base: baseurl}).spec) +
"' accesskey='" + key + "'>" + result.title + "</a></dt>");
if ("thumbnail" in result)
template += (
"<dd class='thumbnail'><img src='" +
escapeHtml(uri({uri: result.thumbnail, base: baseurl}).spec) +
"'/></dd>");
if ("preview" in result)
template += "<dd class='preview'>" + result.preview + "</dd>";
}
template += "</dl>";
sane || Utils.reportWarning(
name + " : we did not find an equal amount of titles, " +
"previews and thumbnails");
}
//errorToLocalize
else template = "<p class='empty'>No results.</p>";
pblock.innerHTML = (
"<div class='" + Klass + "'>Results for <strong>" + html +
"</strong>:" + template + "</div>");
fallback("body", "preview");
fallback("baseUrl", "baseurl");
if (!("baseUrl" in parser)) parser.baseUrl = baseUrl;
if ("type" in parser) parser.type = parser.type.toLowerCase();
}
var params = {
url: url,
dataType: parser.type || "html",
success: searchParser,
error: function searchError(xhr) {
pblock.innerHTML = (
"<div class='" + Klass + "'><span class='error'>" +
xhr.status + " " + xhr.statusText + "</span></div>");
},
};
if (postData) {
params.type = "POST";
params.data = postData;
}
CU.previewAjax(pblock, params);
});
}
return this.CreateCommand(options);
}
makeSearchCommand.query = function searchQuery(target, query, charset) {
var re = /%s|{QUERY}/g;
var fn = charset ? escape : encodeURIComponent;
if (charset) query = Utils.convertFromUnicode(charset, query);
if (typeof target === "object") {
var ret = {};
for (var key in target) ret[key] = target[key].replace(re, query);
return ret;
}
return target && target.replace(re, fn(query));
};
makeSearchCommand.execute = function searchExecute({object: {text}}) {
if (!text && "defaultUrl" in this)
Utils.openUrlInBrowser(this.defaultUrl);
else
Utils.openUrlInBrowser(
makeSearchCommand.query(this.url, text, this.charset),
makeSearchCommand.query(this.postData, text, this.charset));
};
makeSearchCommand.preview = function searchPreview(pblock, args) {
var {text, html} = args.object;
if (!text) return void this.previewDefault(pblock);
function put() {
pblock.innerHTML =
"<div class='search-command'>" + Array.join(arguments, "") + "</div>";
}
var {parser} = this;
put(L("ubiquity.cmdutils.searchcmd", Utils.escapeHtml(this.name), html),
//ToLocalize
parser ? "<p class='loading'>Loading results...</p>" : "");
if (!parser) return;
var params = {
url: makeSearchCommand.query(parser.url || this.url, text, this.charset),
dataType: parser.type || "text",
success: searchParse,
error: function searchError(xhr) {
put("<em class='error'>", xhr.status, " ", xhr.statusText, "</em>");
},
};
if ("postData" in this) {
params.type = "POST";
params.data = makeSearchCommand.query(this.postData, text, this.charset);
}
var global = parser.__parent__;
global.CmdUtils.previewAjax(pblock, params);
function searchParse(data) {
if (!data) {
//ToLocalize
put("<em class='error'>Error parsing search results.</em>");
return;
}
var list = "", results = [], {$} = global, {escapeHtml} = Utils;
var keys = ["title", "body", "href", "thumbnail"];
if (parser.type === "json") {
function dig(dat, key) {
var path = parser[key];
if (typeof path === "function") return path(dat);
for each (let p in path && path.split(".")) dat = dat[p] || 0;
return dat;
}
if ("container" in parser)
for each (let dat in dig(data, "container")) {
let res = {};
for each (let key in keys) res[key] = dig(dat, key);
results.push(res);
}
else {
let vals = [dig(data, k) for each (k in keys)];
results = [keys.reduce(function (r, k, i) (r[k] = vals[i][j], r), {})
for (j in vals[0])];
}
let noEscape = parser.html || "";
for each (let key in keys) if (!~noEscape.indexOf(key))
for each (let r in results) r[key] = r[key] && escapeHtml(r[key]);
}
else {
let $root = $(pblock.cloneNode(0));
//TODO: Strip in-line scripts
$root[0].innerHTML = data;
function find($_, key) let (path = parser[key])
!path ? $() : path.call ? path.call($_, $_) : $_.find(path);
if ("container" in parser)
find($root, "container").each(function eachContainer() {
var res = {}, $this = $(this);
for each (let k in keys) res[k] = find($this, k);
results.push(res);
});
else {
let qs = [find($root, k) for each (k in keys)];
results = [keys.reduce(function (r, k, i) (r[k] = qs[i].eq(j), r), {})
for (j in Utils.seq(qs[0].length))];
}
function toHtml(res, key) { res[key] = res[key].html() }
function toAttr(res, key, lnm, anm) {
var $_ = res[key], atr = ($_.is(lnm) ? $_ : $_.find(lnm)).attr(anm);
res[key] = atr && escapeHtml(atr);
}
for each (let res in results) {
if (!res.href.length) res.href = res.title;
toHtml(res, "title");
toHtml(res, "body");
toAttr(res, "href", "a", "href");
toAttr(res, "thumbnail", "img", "src");
}
}
//TODO: Deal with XML documents
let i = 0, max = parser.maxResults || 4;
for each (let {title, href, body, thumbnail} in results) if (title) {
if (href) {
let key = i < 35 ? (i+1).toString(36) : "-";
title = ("<kbd>" + key + "</kbd> <a href='" + href +
"' accesskey='" + key + "'>" + title + "</a>");
}
list += "<dt class='title'>" + title + "</dt>";
if (thumbnail)
list += "<dd class='thumbnail'><img src='" + thumbnail + "'/></dd>";
if (body)
list += "<dd class='body'>" + body + "</dd>";
if (++i >= max) break;
}
//ToLocalize
put(list
? ("<span class='found'>Results for <strong>" + html +
"</strong>:</span><dl class='list'>" + list + "</dl>")
: "<span class='empty'>No results for " + html + ".</span>");
global.CmdUtils.absUrl(pblock, parser.baseUrl);
}
};
// === {{{ CmdUtils.makeBookmarkletCommand(options) }}} ===
// Creates and registers a Ubiquity command based on a bookmarklet.
@ -1378,17 +1343,13 @@ function absUrl(data, sourceUrl) {
function absUrl_gsub(_, a, q, path) (
a + "=" + q + uri({uri: path, base: sourceUrl}).spec + q));
case "object": {
(this.__globalObject.jQuery(data)
.find("*").andSelf()
.each(function absUrl_iter() {
if (!("getAttribute" in this)) return;
var attr, path = (
this.getAttribute(attr = "href") ||
this.getAttribute(attr = "src" ) ||
this.getAttribute(attr = "action"));
if (path !== null)
this.setAttribute(attr, uri({uri: path, base: sourceUrl}).spec);
}));
let $data = this.__globalObject.jQuery(data);
for each (let name in ["href", "src", "action"])
$data.find("*[" + name + "]").andSelf().each(function absUrl_each(){
if (!("getAttribute" in this)) return;
var {spec} = uri({uri: this.getAttribute(name), base: sourceUrl});
this.setAttribute(name, spec);
});
return data;
}
case "xml": return XML(absUrl.call(this, data.toXMLString(), sourceUrl));

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

@ -420,6 +420,7 @@ CmdUtils.makeSearchCommand({
container: "#main > table > tbody > tr",
title: "td + td + td > a",
thumbnail: "td:first > a > img",
body: "td + td + td > *:not(a)",
maxResults: 8,
},
});
@ -439,15 +440,15 @@ CmdUtils.makeSearchCommand({
Utils.paramsToString({
appid: ("wZ.3jHnV34GC4QakIuzfgHTGiU..1SfNPw" +
"PAuasmt.L5ytoIPOuZAdP1txE4s6KfRBp9"),
results: 6,
results: 10,
output: "json",
}, "?query=%s&")),
type: "json",
container: "ResultSet.Result",
href: "Url",
title: "Title",
preview: "Summary",
maxResults: 6,
body: "Summary",
maxResults: 10,
},
});
@ -769,7 +770,8 @@ CmdUtils.makeSearchCommand({
parser: {
container: "#results li",
title: "h3 > a",
preview: "p",
body: "p",
maxResults: 10,
},
});
@ -779,10 +781,11 @@ CmdUtils.makeSearchCommand({
defaultUrl: "http://www.ebay.com/",
icon: "chrome://ubiquity/skin/icons/ebay.ico",
parser: {
container: ".pcell",
title: ".title",
preview: ".prchold",
container: ".pcell, .sml",
title: ".title, .dtl",
body: ".prchold, .prc",
thumbnail: "img",
maxResults: 30,
},
});
@ -794,6 +797,7 @@ CmdUtils.makeSearchCommand({
parser: {
container: "#result-table > tbody > tr > td",
title: "a:first",
body: "div > div + div",
maxResults: 10,
},
});
@ -806,7 +810,7 @@ CmdUtils.makeSearchCommand({
parser: {
container: "#new_left > a + div",
title: ".DsAndEntryName a",
preview: ".content",
body: ".content",
},
});