1 /***** BEGIN LICENSE BLOCK *****
2 Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 The contents of this file are subject to the Mozilla Public License Version
5 1.1 (the "License"); you may not use this file except in compliance with
6 the License. You may obtain a copy of the License at
7 http://www.mozilla.org/MPL/
9 Software distributed under the License is distributed on an "AS IS" basis,
10 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 for the specific language governing rights and limitations under the
14 The Initial Developer of the Original Code is Shawn Betts.
15 Portions created by the Initial Developer are Copyright (C) 2004,2005
16 by the Initial Developer. All Rights Reserved.
18 Alternatively, the contents of this file may be used under the terms of
19 either the GNU General Public License Version 2 or later (the "GPL"), or
20 the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
21 in which case the provisions of the GPL or the LGPL are applicable instead
22 of those above. If you wish to allow use of your version of this file only
23 under the terms of either the GPL or the LGPL, and not to allow others to
24 use your version of this file under the terms of the MPL, indicate your
25 decision by deleting the provisions above and replace them with the notice
26 and other provisions required by the GPL or the LGPL. If you do not delete
27 the provisions above, a recipient may use your version of this file under
28 the terms of any one of the MPL, the GPL or the LGPL.
29 ***** END LICENSE BLOCK *****/
32 * Utility functions for application scope.
37 function string_hashset() {
40 string_hashset.prototype = {
41 constructor : string_hashset,
47 contains : function(s) {
48 return (("-" + s) in this);
51 remove : function (s) {
55 for_each : function (f) {
63 function string_hashmap() {
66 string_hashmap.prototype = {
67 constructor : string_hashmap,
69 put : function(s,value) {
70 this["-" + s] = value;
73 contains : function(s) {
74 return (("-" + s) in this);
77 get : function(s, default_value) {
83 get_put_default : function(s, default_value) {
86 return (this["-" + s] = default_value);
89 remove : function (s) {
93 for_each : function (f) {
96 f(i.slice(1), this[i]);
100 for_each_value : function (f) {
101 for (var i in this) {
108 /// Window title formatting
111 * Default tile formatter. The page url is ignored. If there is a
112 * page_title, returns: "Page title - Conkeror". Otherwise, it
113 * returns just: "Conkeror".
115 function default_title_formatter (window)
117 var page_title = window.buffers.current.title;
119 if (page_title && page_title.length > 0)
120 return page_title + " - Conkeror";
125 var title_format_fn = null;
127 function set_window_title (window)
129 window.document.title = title_format_fn(window);
132 function init_window_title ()
134 title_format_fn = default_title_formatter;
136 add_hook("window_initialize_late_hook", set_window_title);
137 add_hook("current_content_buffer_location_change_hook",
139 set_window_title(buffer.window);
141 add_hook("select_buffer_hook", function (buffer) { set_window_title(buffer.window); }, true);
142 add_hook("current_buffer_title_change_hook",
144 set_window_title(buffer.window);
150 // Put the string on the clipboard
151 function writeToClipboard(str)
153 const gClipboardHelper = Components.classes["@mozilla.org/widget/clipboardhelper;1"]
154 .getService(Components.interfaces.nsIClipboardHelper);
155 gClipboardHelper.copyString(str);
159 function makeURLAbsolute (base, url)
162 var ioService = Components.classes["@mozilla.org/network/io-service;1"]
163 .getService(Components.interfaces.nsIIOService);
164 var baseURI = ioService.newURI(base, null, null);
166 return ioService.newURI (baseURI.resolve (url), null, null).spec;
170 function get_link_location (element)
172 if (element && element.getAttribute("href")) {
173 var loc = element.getAttribute("href");
174 return makeURLAbsolute(element.baseURI, loc);
180 function makeURL(aURL)
182 var ioService = Cc["@mozilla.org/network/io-service;1"]
183 .getService(Ci.nsIIOService);
184 return ioService.newURI(aURL, null, null);
187 function make_uri(uri) {
188 if (uri instanceof Ci.nsIURI)
190 var io_service = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
191 return io_service.newURI(uri, null, null);
194 function makeFileURL(aFile)
196 var ioService = Cc["@mozilla.org/network/io-service;1"]
197 .getService(Ci.nsIIOService);
198 return ioService.newFileURI(aFile);
202 function get_document_content_disposition (document_o)
204 var content_disposition = null;
206 content_disposition =
207 document_o.defaultView
208 .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
209 .getInterface(Components.interfaces.nsIDOMWindowUtils)
210 .getDocumentMetadata("content-disposition");
212 return content_disposition;
216 function set_focus_no_scroll(window, element)
218 window.document.commandDispatcher.suppressFocusScroll = true;
220 window.document.commandDispatcher.suppressFocusScroll = false;
223 function do_repeatedly_positive(func, n) {
224 var args = Array.prototype.slice.call(arguments, 2);
226 func.apply(null, args);
229 function do_repeatedly(func, n, positive_args, negative_args) {
231 do func.apply(null, negative_args); while (++n < 0);
233 while (n-- > 0) func.apply(null, positive_args);
236 // remove whitespace from the beginning and end
237 function trim_whitespace (str)
239 var tmp = new String (str);
240 return tmp.replace (/^\s+/, "").replace (/\s+$/, "");
243 function abs_point (node)
248 pt.x = node.offsetLeft;
249 pt.y = node.offsetTop;
250 // find imagemap's coordinates
251 if (node.tagName == "AREA") {
252 var coords = node.getAttribute("coords").split(",");
253 pt.x += Number(coords[0]);
254 pt.y += Number(coords[1]);
257 node = node.offsetParent;
258 // Sometimes this fails, so just return what we got.
260 while (node.tagName != "BODY") {
261 pt.x += node.offsetLeft;
262 pt.y += node.offsetTop;
263 node = node.offsetParent;
267 // while (node.tagName != "BODY") {
268 // alert("okay: " + node + " " + node.tagName + " " + pt.x + " " + pt.y);
269 // node = node.offsetParent;
275 var xul_app_info = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
276 var xul_runtime = Cc['@mozilla.org/xre/app-info;1'].getService(Ci.nsIXULRuntime);
281 // possible return values: 'Darwin', 'Linux', 'WINNT', ...
282 return xul_runtime.OS;
285 var default_directory = null;
287 var env = Cc['@mozilla.org/process/environment;1'].getService(Ci.nsIEnvironment);
288 function getenv (variable) {
289 if (env.exists (variable))
290 return env.get(variable);
294 function set_default_directory(directory_s) {
297 if ( get_os() == "WINNT")
299 directory_s = getenv ('USERPROFILE') ||
300 getenv ('HOMEDRIVE') + getenv ('HOMEPATH');
303 directory_s = getenv ('HOME');
307 default_directory = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
308 default_directory.initWithPath (directory_s);
311 set_default_directory();
313 const XHTML_NS = "http://www.w3.org/1999/xhtml";
314 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
315 const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
316 const XLINK_NS = "http://www.w3.org/1999/xlink";
318 function create_XUL(window, tag_name)
320 return window.document.createElementNS(XUL_NS, tag_name);
324 /* Used in calls to XPath evaluate */
325 function xpath_lookup_namespace(prefix) {
326 if (prefix == "xhtml")
335 function method_caller(obj, func) {
337 func.apply(obj, arguments);
341 function shell_quote(str) {
342 var s = str.replace("\"", "\\\"", "g");
343 s = s.replace("$", "\$", "g");
347 function get_window_from_frame(frame) {
349 var window = frame.QueryInterface(Ci.nsIInterfaceRequestor)
350 .getInterface(Ci.nsIWebNavigation)
351 .QueryInterface(Ci.nsIDocShellTreeItem)
353 .QueryInterface(Ci.nsIInterfaceRequestor)
354 .getInterface(Ci.nsIDOMWindow).wrappedJSObject;
355 /* window is now an XPCSafeJSObjectWrapper */
356 window.escape_wrapper(function (w) { window = w; });
357 /* window is now completely unwrapped */
364 function get_buffer_from_frame(window, frame) {
365 var count = window.buffers.count;
366 for (var i = 0; i < count; ++i) {
367 var b = window.buffers.get_buffer(i);
368 if (b.top_frame == frame)
374 var file_locator = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
376 function get_shortdoc_string(doc) {
379 var idx = doc.indexOf("\n");
381 shortdoc = doc.substring(0,idx);
388 var conkeror_source_code_path = null;
390 function source_code_reference(uri, line_number) {
392 this.line_number = line_number;
394 source_code_reference.prototype = {
396 if (this.uri.indexOf(module_uri_prefix) == 0)
397 return this.uri.substring(module_uri_prefix.length);
402 var file_uri_prefix = "file://";
403 if (this.uri.indexOf(file_uri_prefix) == 0)
404 return this.uri.substring(file_uri_prefix.length);
409 if (conkeror_source_code_path != null) {
410 var module_name = this.module_name;
411 if (module_name != null)
412 return "file://" + conkeror_source_code_path + "/modules/" + module_name;
417 open_in_editor : function() {
418 yield open_with_external_editor(this.best_uri, $line = this.line_number);
422 var get_caller_source_code_reference_ignored_functions = {};
424 function get_caller_source_code_reference(extra_frames_back) {
425 /* Skip at least this function itself and whoever called it (and
426 * more if the caller wants to be skipped). */
427 var frames_to_skip = 2;
428 if (extra_frames_back != null)
429 frames_to_skip += extra_frames_back;
431 for (let f = Components.stack; f != null; f = f.caller) {
432 if (frames_to_skip > 0) {
436 if (get_caller_source_code_reference_ignored_functions[f.name])
438 return new source_code_reference(f.filename, f.lineNumber);
444 function ignore_function_for_get_caller_source_code_reference(func_name) {
445 get_caller_source_code_reference_ignored_functions[func_name] = 1;
448 require_later("external-editor.js");
450 function dom_generator(document, ns) {
451 this.document = document;
454 dom_generator.prototype = {
455 element : function(tag, parent) {
456 var node = this.document.createElementNS(this.ns, tag);
458 if (parent != null && (parent instanceof Ci.nsIDOMNode)) {
459 parent.appendChild(node);
462 for (; i < arguments.length; i += 2)
463 node.setAttribute(arguments[i], arguments[i+1]);
467 text : function(str, parent) {
468 var node = this.document.createTextNode(str);
470 parent.appendChild(node);
475 stylesheet_link : function(href, parent) {
476 var node = this.element("link");
477 node.setAttribute("rel", "stylesheet");
478 node.setAttribute("type", "text/css");
479 node.setAttribute("href", href);
481 parent.appendChild(node);
486 add_stylesheet : function (url) {
487 var head = this.document.documentElement.firstChild;
488 this.stylesheet_link(url, head);
493 * Generates a QueryInterface function suitable for an implemenation
494 * of an XPCOM interface. Unlike XPCOMUtils, this uses the Function
495 * constructor to generate a slightly more efficient version. The
496 * arguments can be either Strings or elements of
497 * Components.interfaces.
499 function generate_QI() {
500 var args = Array.prototype.slice.call(arguments).map(String).concat(["nsISupports"]);
502 Array.prototype.map.call(args,
504 "iid.equals(Components.interfaces." + x + ")")
506 ") return this; throw Components.results.NS_ERROR_NO_INTERFACE;";
507 return new Function("iid", fstr);
510 function set_branch_pref(branch, name, value) {
511 if (typeof(value) == "string") {
512 branch.setCharPref(name, value);
513 } else if (typeof(value) == "number") {
514 branch.setIntPref(name, value);
515 } else if (typeof(value) == "boolean") {
516 branch.setBoolPref(name, value);
520 function default_pref(name, value) {
521 var branch = preferences.getDefaultBranch(null);
522 set_branch_pref(branch, name, value);
525 function user_pref(name, value) {
526 var branch = preferences.getBranch(null);
527 set_branch_pref(branch, name, value);
530 function pref_is_locked(name, value) {
531 var branch = preferences.getBranch(null);
532 return branch.prefIsLocked(name);
535 function lock_pref(name, value) {
536 var branch = preferences.getBranch(null);
537 if (branch.prefIsLocked(name))
538 branch.unlockPref(name);
539 default_pref(name, value);
540 branch.lockPref(name);
543 function unlock_pref(name) {
544 var branch = preferences.getBranch(null);
545 branch.unlockPref(name);
548 function get_branch_pref(branch, name) {
549 switch (branch.getPrefType(name)) {
550 case branch.PREF_STRING:
551 return branch.getCharPref(name);
552 case branch.PREF_INT:
553 return branch.getIntPref(name);
554 case branch.PREF_BOOL:
555 return branch.getBoolPref(name);
561 function get_pref(name) {
562 var branch = preferences.getBranch(null);
563 return get_branch_pref(branch, name);
566 function get_default_pref(name) {
567 var branch = preferences.getDefaultBranch(null);
568 return get_branch_pref(branch, name);
571 function clear_pref(name) {
572 var branch = preferences.getBranch(null);
573 return branch.clearUserPref(name);
576 function clear_default_pref(name) {
577 var branch = preferences.getDefaultBranch(null);
578 return branch.clearUserPref(name);
581 function pref_has_user_value(name) {
582 var branch = preferences.getBranch(null);
583 return branch.prefHasUserValue(name);
586 function pref_has_default_value(name) {
587 var branch = preferences.getDefaultBranch(null);
588 return branch.prefHasUserValue(name);
591 function session_pref (name, value) {
592 try { clear_pref (name); }
594 return default_pref (name, value);
597 const USER_AGENT_OVERRIDE_PREF = "general.useragent.override";
599 function set_user_agent(str) {
600 lock_pref(USER_AGENT_OVERRIDE_PREF, str);
603 function define_builtin_commands(prefix, do_command_function, toggle_mark, mark_active_predicate) {
605 // Specify a docstring
606 function D(cmd, docstring) {
607 var o = new String(cmd);
612 // Specify a forward/reverse pair
615 o.is_reverse_pair = true;
619 // Specify a movement/select command pair.
620 function S(command, movement, select) {
621 var o = [movement, select];
623 o.is_move_select_pair = true;
627 var builtin_commands = [
628 S(D("beginning-of-line", "Move or extend the selection to the beginning of the current line."),
629 D("cmd_beginLine", "Move point to the beginning of the current line."),
630 D("cmd_selectBeginLine", "Extend selection to the beginning of the current line.")),
631 S(D("end-of-line", "Move or extend the selection to the end of the current line."),
632 D("cmd_endLine", "Move point to the end of the current line."),
633 D("cmd_selectEndLine", "Extend selection to the end of the current line.")),
634 D("cmd_copy", "Copy the selection into the clipboard."),
636 D("cmd_cut", "Cut the selection into the clipboard."),
638 D("cmd_deleteToBeginningOfLine", "Delete to the beginning of the current line."),
639 D("cmd_deleteToEndOfLine", "Delete to the end of the current line."),
640 S(D("beginning-of-first-line", "Move or extend the selection to the beginning of the first line."),
641 D("cmd_moveTop", "Move point to the beginning of the first line."),
642 D("cmd_selectTop", "Extend selection to the beginning of the first line.")),
643 S(D("end-of-last-line", "Move or extend the selection to the end of the last line."),
644 D("cmd_moveBottom", "Move point to the end of the last line."),
645 D("cmd_selectBottom", "Extend selection to the end of the last line.")),
646 D("cmd_selectAll", "Select all."),
647 "cmd_scrollBeginLine",
649 D("cmd_scrollTop", "Scroll to the top of the buffer."),
650 D("cmd_scrollBottom", "Scroll to the bottom of the buffer.")];
652 var builtin_commands_with_count = [
653 R(S(D("forward-char", "Move or extend the selection forward one character."),
654 D("cmd_charNext", "Move point forward one character."),
655 D("cmd_selectCharNext", "Extend selection forward one character.")),
656 S(D("backward-char", "Move or extend the selection backward one character."),
657 D("cmd_charPrevious", "Move point backward one character."),
658 D("cmd_selectCharPrevious", "Extend selection backward one character."))),
659 R(D("cmd_deleteCharForward", "Delete the following character."),
660 D("cmd_deleteCharBackward", "Delete the previous character.")),
661 R(D("cmd_deleteWordForward", "Delete the following word."),
662 D("cmd_deleteWordBackward", "Delete the previous word.")),
663 R(S(D("forward-line", "Move or extend the selection forward one line."),
664 D("cmd_lineNext", "Move point forward one line."),
665 "cmd_selectLineNext", "Extend selection forward one line."),
666 S(D("backward-line", "Move or extend the selection backward one line."),
667 D("cmd_linePrevious", "Move point backward one line."),
668 D("cmd_selectLinePrevious", "Extend selection backward one line."))),
669 R(S(D("forward-page", "Move or extend the selection forward one page."),
670 D("cmd_movePageDown", "Move point forward one page."),
671 D("cmd_selectPageDown", "Extend selection forward one page.")),
672 S(D("backward-page", "Move or extend the selection backward one page."),
673 D("cmd_movePageUp", "Move point backward one page."),
674 D("cmd_selectPageUp", "Extend selection backward one page."))),
675 R(D("cmd_undo", "Undo last editing action."),
676 D("cmd_redo", "Redo last editing action.")),
677 R(S(D("forward-word", "Move or extend the selection forward one word."),
678 D("cmd_wordNext", "Move point forward one word."),
679 D("cmd_selectWordNext", "Extend selection forward one word.")),
680 S(D("backward-word", "Move or extend the selection backward one word."),
681 D("cmd_wordPrevious", "Move point backward one word."),
682 D("cmd_selectWordPrevious", "Extend selection backward one word."))),
683 R(D("cmd_scrollPageUp", "Scroll up one page."),
684 D("cmd_scrollPageDown", "Scroll down one page.")),
685 R(D("cmd_scrollLineUp", "Scroll up one line."),
686 D("cmd_scrollLineDown", "Scroll down one line.")),
687 R(D("cmd_scrollLeft", "Scroll left."),
688 D("cmd_scrollRight", "Scroll right.")),
689 D("cmd_paste", "Insert the contents of the clipboard.")];
691 interactive(prefix + "set-mark",
692 "Toggle whether the mark is active.\n" +
693 "When the mark is active, movement commands affect the selection.",
696 function doc_for_builtin(c) {
700 return s + "Run the built-in command " + c + ".";
703 function define_simple_command(c) {
704 interactive(prefix + c, doc_for_builtin(c), function (I) { do_command_function(I, c); });
707 function get_move_select_doc_string(c) {
708 return c.command.doc +
709 "\nSpecifically, if the mark is inactive, runs `" + prefix + c[0] + "'. " +
710 "Otherwise, runs `" + prefix + c[1] + "'.\n" +
711 "To toggle whether the mark is active, use `" + prefix + "set-mark'.";
714 for each (let c_temp in builtin_commands) {
716 if (c.is_move_select_pair) {
717 interactive(prefix + c.command, get_move_select_doc_string(c), function (I) {
718 do_command_function(I, mark_active_predicate(I) ? c[1] : c[0]);
720 define_simple_command(c[0]);
721 define_simple_command(c[1]);
724 define_simple_command(c);
727 function get_reverse_pair_doc_string(main_doc, alt_command) {
728 return main_doc + "\n" +
729 "The prefix argument specifies a repeat count for this command. " +
730 "If the count is negative, `" + prefix + alt_command + "' is performed instead with " +
731 "a corresponding positive repeat count.";
734 function define_simple_reverse_pair(a, b) {
735 interactive(prefix + a, get_reverse_pair_doc_string(doc_for_builtin(a), b),
737 do_repeatedly(do_command_function, I.p, [I, a], [I, b]);
739 interactive(prefix + b, get_reverse_pair_doc_string(doc_for_builtin(b), a),
741 do_repeatedly(do_command_function, I.p, [I, b], [I, a]);
745 for each (let c_temp in builtin_commands_with_count)
748 if (c.is_reverse_pair) {
749 if (c[0].is_move_select_pair) {
750 interactive(prefix + c[0].command, get_reverse_pair_doc_string(get_move_select_doc_string(c[0]),
753 var idx = mark_active_predicate(I) ? 1 : 0;
754 do_repeatedly(do_command_function, I.p, [I, c[0][idx]], [I, c[1][idx]]);
756 interactive(prefix + c[1].command, get_reverse_pair_doc_string(get_move_select_doc_string(c[1]),
759 var idx = mark_active_predicate(I) ? 1 : 0;
760 do_repeatedly(do_command_function, I.p, [I, c[1][idx]], [I, c[0][idx]]);
762 define_simple_reverse_pair(c[0][0], c[1][0]);
763 define_simple_reverse_pair(c[0][1], c[1][1]);
765 define_simple_reverse_pair(c[0], c[1]);
767 let doc = doc_for_builtin(c) +
768 "\nThe prefix argument specifies a positive repeat count for this command.";
769 interactive(prefix + c, doc, function (I) {
770 do_repeatedly_positive(do_command_function, I.p, I, c);
776 var observer_service = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
778 function abort(str) {
779 var e = new Error(str);
780 e.__proto__ = abort.prototype;
783 abort.prototype.__proto__ = Error.prototype;
786 function get_temporary_file(name) {
789 var file = file_locator.get("TmpD", Ci.nsIFile);
791 // Create the file now to ensure that no exploits are possible
792 file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
797 /* FIXME: This should be moved somewhere else, perhaps. */
798 function create_info_panel(window, panel_class, row_arr) {
799 /* Show information panel above minibuffer */
801 var g = new dom_generator(window.document, XUL_NS);
803 var p = g.element("vbox", "class", "panel " + panel_class, "flex", "0");
804 var grid = g.element("grid", p);
805 var cols = g.element("columns", grid);
806 g.element("column", cols, "flex", "0");
807 g.element("column", cols, "flex", "1");
809 var rows = g.element("rows", grid);
812 for each (let [row_class, row_label, row_value] in row_arr) {
813 row = g.element("row", rows, "class", row_class);
814 g.element("label", row,
816 "class", "panel-row-label");
817 g.element("label", row,
819 "class", "panel-row-value");
821 window.minibuffer.insert_before(p);
823 p.destroy = function () {
824 this.parentNode.removeChild(this);
831 // read_from_x_primary_selection favors the X PRIMARY SELECTION, when
832 // it exists. The builtin cmd_paste always uses X CLIPBOARD. So this
833 // is an auxiliary utility, in case you need to work with the primary
836 function read_from_x_primary_selection ()
839 var clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
840 .getService(Components.interfaces.nsIClipboard);
842 // Create tranferable that will transfer the text.
843 var trans = Components.classes["@mozilla.org/widget/transferable;1"]
844 .createInstance(Components.interfaces.nsITransferable);
846 trans.addDataFlavor("text/unicode");
847 // If available, use selection clipboard, otherwise global one
848 if (clipboard.supportsSelectionClipboard())
849 clipboard.getData(trans, clipboard.kSelectionClipboard);
851 clipboard.getData(trans, clipboard.kGlobalClipboard);
855 trans.getTransferData("text/unicode", data, dataLen);
858 data = data.value.QueryInterface(Components.interfaces.nsISupportsString);
859 return data.data.substring(0, dataLen.value / 2);
865 var user_variables = new string_hashmap();
867 function define_variable(name, default_value, doc) {
868 conkeror[name] = default_value;
869 user_variables.put(name, {
870 default_value: default_value,
872 shortdoc: get_shortdoc_string(doc),
873 source_code_reference: get_caller_source_code_reference() });
877 function register_user_stylesheet(url)
879 var uri = makeURL(url);
880 var sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
881 sss.loadAndRegisterSheet(uri, sss.USER_SHEET);
884 function unregister_user_stylesheet(url)
886 var uri = makeURL(url);
887 var sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService);
888 if (sss.sheetRegistered(uri, sss.USER_SHEET))
889 ss.unregisterSheet(uri, sss.USER_SHEET);
892 function predicate_alist_match(alist, key) {
893 for each (let i in alist) {