Merge branch 'master' of ssh://git.indexdata.com:222/home/git/private/mkws
authorJohn Malconian <malc@indexdata.com>
Mon, 28 Apr 2014 18:00:32 +0000 (18:00 +0000)
committerJohn Malconian <malc@indexdata.com>
Mon, 28 Apr 2014 18:00:32 +0000 (18:00 +0000)
133 files changed:
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
Makefile
README
doc/.gitignore [new file with mode: 0644]
doc/Makefile [new file with mode: 0644]
doc/README.markdown [new file with mode: 0644]
doc/library-configuration.txt [new file with mode: 0644]
doc/mkws-developer.txt [new file with mode: 0644]
doc/mkws-doc.css [new file with mode: 0644]
doc/whitepaper.markdown [new file with mode: 0644]
examples/apache2/example-dev [new file with mode: 0644]
examples/apache2/example-dev-px [new file with mode: 0644]
examples/apache2/example-dev-ssl-px [new file with mode: 0644]
examples/apache2/example-ssl-px [new file with mode: 0644]
examples/apache2/mkws-examples
examples/apache2/mkws-examples-mike
examples/htdocs/Makefile
examples/htdocs/README [new file with mode: 0644]
examples/htdocs/auto-paratext-minimal.html [new file with mode: 0644]
examples/htdocs/auto-paratext.html [new file with mode: 0644]
examples/htdocs/auto.html
examples/htdocs/auto2.html [new file with mode: 0644]
examples/htdocs/auto3.html [new file with mode: 0644]
examples/htdocs/dict.html
examples/htdocs/heikki-motd.html [new file with mode: 0644]
examples/htdocs/heikki.html [new file with mode: 0644]
examples/htdocs/images.html [new file with mode: 0644]
examples/htdocs/index.html
examples/htdocs/jakub.html
examples/htdocs/jasmine-local-popup.html [new file with mode: 0644]
examples/htdocs/jasmine-popup.html
examples/htdocs/jasmine-pp2.html
examples/htdocs/jasmine.html
examples/htdocs/jquery.html
examples/htdocs/language.html
examples/htdocs/local-auto.html [new file with mode: 0644]
examples/htdocs/local-auto3.html [new file with mode: 0644]
examples/htdocs/localauth.html
examples/htdocs/lolcat.html [new file with mode: 0644]
examples/htdocs/lowlevel.html
examples/htdocs/mike.html
examples/htdocs/mike2.html [deleted file]
examples/htdocs/minimal.html
examples/htdocs/mkws-widget-reference.css [new file with mode: 0644]
examples/htdocs/mkws-widget-reference.js [new file with mode: 0644]
examples/htdocs/mobile.html
examples/htdocs/popup.html
examples/htdocs/reference-universe.html [new file with mode: 0644]
examples/htdocs/stateful.html [new file with mode: 0644]
examples/htdocs/templates.html
examples/htdocs/two-teams.html [new file with mode: 0644]
examples/htdocs/wolfram.html
examples/htdocs/wolfram2.html
examples/jasmine/SpecRunner.html [new file with mode: 0644]
examples/jasmine/lib/jasmine-1.3.1/MIT.LICENSE [new file with mode: 0644]
examples/jasmine/lib/jasmine-1.3.1/jasmine-html.js [new file with mode: 0644]
examples/jasmine/lib/jasmine-1.3.1/jasmine.css [new file with mode: 0644]
examples/jasmine/lib/jasmine-1.3.1/jasmine.js [new file with mode: 0644]
examples/jasmine/lib/jasmine-1.3.1/jasmine_favicon.png [new file with mode: 0644]
examples/jasmine/spec/PlayerSpec.js [new file with mode: 0644]
examples/jasmine/spec/SpecHelper.js [new file with mode: 0644]
examples/jasmine/src/Player.js [new file with mode: 0644]
examples/jasmine/src/Song.js [new file with mode: 0644]
notes/2013-06-24--todo [deleted file]
notes/developers.txt [new file with mode: 0644]
notes/using-mkadmin
src/.gitignore [new file with mode: 0644]
src/Makefile [new file with mode: 0644]
src/NEWS [new file with mode: 0644]
src/VERSION [new file with mode: 0644]
src/mkws-core.js [new file with mode: 0644]
src/mkws-filter.js [new file with mode: 0644]
src/mkws-handlebars.js [new file with mode: 0644]
src/mkws-jquery.js [new file with mode: 0644]
src/mkws-team.js [new file with mode: 0644]
src/mkws-widget-authname.js [new file with mode: 0644]
src/mkws-widget-builder.js [new file with mode: 0644]
src/mkws-widget-categories.js [new file with mode: 0644]
src/mkws-widget-log.js [new file with mode: 0644]
src/mkws-widget-record.js [new file with mode: 0644]
src/mkws-widget-termlists.js [new file with mode: 0644]
src/mkws-widgets.js [new file with mode: 0644]
test/.gitignore [new file with mode: 0644]
test/Makefile
test/README.txt
test/bin/apache-template-update [new file with mode: 0755]
test/bin/bomb.pl [new file with mode: 0755]
test/etc/logrotate.d/mkws [new file with mode: 0644]
test/images/.gitkeep [new file with mode: 0644]
test/logs/.gitkeep [new file with mode: 0644]
test/package.json [new file with mode: 0644]
test/phantom/evaluate.js [new file with mode: 0644]
test/phantom/run-jasmine.js [new file with mode: 0644]
test/phantom/screenshot.js [new file with mode: 0644]
test/spec-dev/mkws.spec.js [new file with mode: 0644]
test/spec-dev/parseXML.js [new file with mode: 0644]
test/spec-dev/parseXML.spec.js [new file with mode: 0644]
test/spec/jquery.spec.js
test/spec/jsdom.spec.js
test/spec/mkws-config.js
test/spec/mkws-index-jsdom-remote.spec.js [deleted file]
test/spec/mkws-index-jsdom.spec.js [deleted file]
test/spec/mkws-index-simple.spec.js [deleted file]
test/spec/mkws-pazpar2.js
test/spec/mkws_utils.js [deleted file]
tools/apache2/README
tools/apache2/jasmine-dev.template [new file with mode: 0644]
tools/apache2/mkws-dev
tools/apache2/mkws-dev-proxy [deleted file]
tools/apache2/mkws-dev-px [new file with mode: 0644]
tools/apache2/mkws-dev-ssl-px [new file with mode: 0644]
tools/apache2/mkws-heikki [new file with mode: 0644]
tools/apache2/mkws-live
tools/apache2/mkws-mike-mac
tools/apache2/mkws-ne [new file with mode: 0644]
tools/apache2/mkws-ssl-px [new file with mode: 0644]
tools/apache2/mkws-test [new file with mode: 0644]
tools/bin/mkws-bootstrap.sh [new file with mode: 0755]
tools/bin/nagios-service-proxy.sh [new file with mode: 0755]
tools/htdocs/.gitignore
tools/htdocs/Makefile [deleted file]
tools/htdocs/NEWS [deleted file]
tools/htdocs/README.markdown [deleted file]
tools/htdocs/VERSION [deleted file]
tools/htdocs/debugging-notes.txt [deleted file]
tools/htdocs/handlebars-test.html [deleted file]
tools/htdocs/html-structure.txt [deleted file]
tools/htdocs/index.html
tools/htdocs/mkws-doc.css [deleted file]
tools/htdocs/mkws.css
tools/htdocs/mkws.js [deleted file]
tools/htdocs/whitepaper.markdown [deleted file]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..b25c15b
--- /dev/null
@@ -0,0 +1 @@
+*~
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..39d8b2e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,678 @@
+LICENSE file of MKWS: the MasterKey Widget Set
+Copyright (C) 2013-2014 Index Data.
+
+
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
index 62ced87..9504c36 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,16 +1,31 @@
-# Copyright (c) 2013 IndexData ApS. http://indexdata.com
+# Copyright (c) 2013-2014 IndexData ApS. http://indexdata.com
 
-all clean:
-       ${MAKE} -C./tools/htdocs $@
-       ${MAKE} -C./examples/htdocs $@
+all:
+       ${MAKE} -C./src $@
+       ${MAKE} -C./doc $@
 
-pz2api-git-checkout distclean:
-       ${MAKE} -C./tools/htdocs $@
+clean distclean:
+       ${MAKE} -C./src $@
+       ${MAKE} -C./doc $@
+       ${MAKE} -C./examples/htdocs $@
+       ${MAKE} -C./test $@
 
 check-js:
        ${MAKE} -C./test check
 
-check: distclean all
+phantomjs p:
+       ${MAKE} -C./test $@
+
+# must be called once after GIT checkout
+setup: 
+#why?  ${MAKE} -C./tools/htdocs mkws-js-min
+       ${MAKE} -C./test node-modules
+
+check: setup check-js
+       @echo ""
+       @echo "To run jasmine regression tests, type: make phantomjs"
 
 help:
-       @echo "make [ all | clean | pz2api-git-checkout | check-js ]"
+       @echo "make [ all | setup | clean | distclean ]"
+       @echo "     [ check | check-js | phantomjs ]"
+
diff --git a/README b/README
index 3b99f61..55d911c 100644 (file)
--- a/README
+++ b/README
@@ -5,24 +5,35 @@ The MasterKey Widget Set, or MKWS, is a project to create some very
 simple HTML/JS/CSS widgets that can be dropped into ANY website,
 irrespective of CMS or lack thereof, to enable MasterKey searching.
 
-The top level bug for discussing this is
-        https://jira.indexdata.com/browse/MKWS-1
-and a high-level description can be found at
-        https://twiki.indexdata.com/twiki/bin/view/ID/MasterKeyWidgetSet
-
 
 WHAT'S WHAT
 ===========
 
-README -- this file
+README   -- this file
 Makefile -- delegates to tools/htdocs/Makefile
-tools -- the tools that make up the Widget Set
+tools   -- the tools that make up the Widget Set
 examples -- examples of applications that use MKWS
-notes -- internal documents, not for customers
+
+notes    -- internal documents, not for customers
 
 
 Required devel tools
 ====================
 
-on debian, you will need: curl git-core pandoc
+On debian, you will need:
+$ sudo apt-get install curl git-core pandoc yui-compressor node-js
+
+On Debian 7 (wheezy), you do not need git-core, plain git will do, but
+you probably have that on a development box already. Unfortunately, node-js
+is not available for wheezy. Either you can get it from wheezy-backports,
+or you can download the source from http://nodejs.org/download/ and build
+it yourself. Looks like you need node and npm, make install puts them
+into /usr/local/bin.
+
+For apache setup, see tools/apache2/README
+
+NEWS
+=========
+
+see tools/htdocs/NEWS
 
diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644 (file)
index 0000000..e727760
--- /dev/null
@@ -0,0 +1,6 @@
+README.html
+README.odt
+README.pdf
+whitepaper.html
+whitepaper.odt
+whitepaper.pdf
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644 (file)
index 0000000..75a9e2d
--- /dev/null
@@ -0,0 +1,52 @@
+# Copyright (c) 2013-2014 IndexData ApS. http://indexdata.com
+
+DOCS = README.html README.odt README.pdf \
+       whitepaper.html whitepaper.odt whitepaper.pdf
+
+INSTALLABLE = README.html whitepaper.html mkws-doc.css
+INSTALLED = $(INSTALLABLE:%=../tools/htdocs/%)
+
+install: $(INSTALLED)
+
+uninstall:
+       rm -f $(INSTALLED)
+
+../tools/htdocs/%: %
+       rm -f $@
+       cp -p $? $@
+       chmod -w $@
+
+all: $(DOCS)
+
+# For a description of pandoc's markdown format, see:
+# http://johnmacfarlane.net/pandoc/demo/example9/pandocs-markdown.html -->
+
+# for older pandoc (<1.9) run first:
+# perl -i.bak -npe 's/"(Authors|Subjects)": "(.*?)"/"$1": "test"/' tools/htdocs/whitepaper.markdown
+#
+%.html: %.markdown
+       rm -f $@
+       pandoc --standalone --toc -c mkws-doc.css $< | sed '/^<col width="[0-9]*%" \/>$//d' > $@
+       chmod ugo-w $@
+
+%.odt: %.markdown
+       rm -f $@
+       pandoc --standalone $< -o $@
+       chmod ugo-w $@
+
+# ### In order to compile the whitepaper, which has tables, to PDF,
+# you will need to install the Debian package
+#      texlive-latex-recommended
+%.pdf: %.markdown
+       rm -f $@
+       pandoc --standalone $< -o $@
+       chmod ugo-w $@
+
+clean:
+       rm -f $(DOCS)
+
+distclean: clean uninstall
+
+help:
+       @echo "make [ all | install | clean | distclean ]"
+
diff --git a/doc/README.markdown b/doc/README.markdown
new file mode 100644 (file)
index 0000000..49d415d
--- /dev/null
@@ -0,0 +1,102 @@
+% The MasterKey Widget Set
+% Mike Taylor; Wolfram Schneider
+% 10 July 2013
+
+
+Introduction
+------------
+
+This is the MasterKey Widget Set. The initial version was based on the
+"jsdemo" application distributed with pazpar2, but it is now far
+removed from those beginnnings.
+
+As much of the searching functionality as possible is hosted on
+       <http://mkws.indexdata.com/>
+so that very simple websites such as
+       <http://example.indexdata.com/>
+can have MasterKey searching with minimal effort.
+
+The following files are hosted on `mkws.indexdata.com`:
+
+* `mkws.js`
+* `/pazpar2/js/pz2.js`
+* `mkws-complete.js` -- a single file consisting of `mkws.js`,
+  jQuery (which it uses), Handlebars (ditto) and `pz2.js`
+* `mkws.css`
+
+
+Supported Browsers
+------------------
+
+Any modern browser will work fine. JavaScript must be enabled.
+
+* IE8 or later
+* Firefox 17 or later
+* Google Chrome 27 or later
+* Safari 6 or later
+* Opera  12 or later
+* iOS 6.x (iPhone, iPad)
+* Android 4.x
+
+Not supported: IE6, IE7
+
+
+Configuring a client (short version)
+------------------------------------
+
+The application's HTML must contains the following elements as well as
+whatever makes up the application itself:
+
+Prerequisites:
+
+~~~
+       <link rel="stylesheet" href="http://mkws.indexdata.com/mkws.css" />
+       <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+~~~
+
+Then the following special `<div>`s can be added (with no content), and
+will be filled in by MKWS:
+
+* `<div id="mkwsSwitch"></div>` -- switch between record and target views
+* `<div id="mkwsLang"></div>  ` -- switch between English, Danish and German
+* `<div id="mkwsSearch"></div>` -- search box and button
+* `<div id="mkwsResults"></div>` -- result list, including pager/sorting
+* `<div id="mkwsTargets"></div>` -- target list, including status
+* `<div id="mkwsStat"></div>` -- summary statistics
+
+You can configure and control the client by creating an `mkws_config`
+object _before_ loading the widget-set.  Here is an example of all
+possible options:
+
+~~~
+    <script type="text/javascript">
+      var mkws_config = {
+        use_service_proxy: true,    /* true, flase: use service proxy instead pazpar2 */
+        show_lang: true,            /* true, false: show/hide language menu */
+        show_sort: true,            /* true, false: show/hide sort menu */
+        show_perpage: true,         /* true, false: show/hide perpage menu */
+        lang_options: ["en", "de", "da"],
+                                    /* display languages links for given languages, [] for all */
+        facets: ["xtargets", "subject", "author"],
+                                    /* display facets, in this order, [] for none */
+        sort_default: "relevance",  /* "relevance", "title:1", "date:0", "date:1" */
+        query_width: 50,            /* 5..50 */
+        perpage_default: 20,        /* 10, 20, 30, 50 */
+        lang: "en",                 /* "en", "de", "da" */
+        debug_level: 0,             /* debug level for development: 0..2 */
+
+        responsive_design_wodth: 600,    /* page reflows for devices < 600 pixels wide */
+        pazpar2_url: "/service-proxy/",            /* URL */
+        service_proxy_auth: "/service-proxy-auth", /* URL */
+        // TODO: language_*, perpage_options, sort_options
+      };
+    </script>
+~~~
+
+For much more detail, see
+[the MKWS whitepaper](whitepaper.html).
+
+
+- - -
+
+Copyright 2013 IndexData ApS. <http://indexdata.com>
diff --git a/doc/library-configuration.txt b/doc/library-configuration.txt
new file mode 100644 (file)
index 0000000..7abcfc0
--- /dev/null
@@ -0,0 +1,75 @@
+MKWS Target Selection
+=====================
+
+
+1. Selecting targets within the library
+---------------------------------------
+
+MKWS applications can choose what subset of the available targets to
+use, by means of several alternative settings on individual widgets or
+in the mkws_config structure:
+
+* targets -- contains a Pazpar2 targets string, typically of the form
+  "pz:id=" or "pz:id~" followed by a pipe-separated list of low-level
+  target IDs. At present, these IDs are based on ZURLs, so a typical
+  value would be something like:
+       pz:id~josiah.brown.edu:210/innopac|connect.indexdata.com:9000/mit_opencourseware'
+
+* targetfilter -- contains a CQL query which is used to find relevant
+  targets from the relvant library. For example,
+       udb==Google_Images
+
+* target -- contains a single UDB, that of the sole target to be
+  used. For example
+       Google_Images
+
+
+2. Changing the library
+-----------------------
+
+Some MKWS applications will want to define their own library providing
+a different range of available targets. This is particularly important
+in the case of applications that authenticate onto subscription
+resources by means of credentials stored in MKAdmin, in that such
+library accounts need to prohibit unauthorised access.
+
+Setting up such a library is a two-stage process.
+
+Stage A (on MKAdmin)
+
+Create the library:
+       - Make a new library on http://mkc-admin.indexdata.com/console/
+       - Select relevant targets
+       - Add authentication credentials as necessary
+       - Create an end-user account
+       - Set its username and password
+
+Stage B (on the application's web-server):
+
+Authentication onto the library can be achieved by a single HTTP GET
+to the relevant Service Proxy, passing in the credentials and thereby
+initiating an HTTP session. This can most simply be done just by
+setting service_proxy_auth to a URL such as
+       http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=MIKE&password=SWORDFISH
+
+However, doing so reveals the the credentials to public view -- to
+anyone who does View Source on the MKWS application. This may be
+acceptable for some libraries, but is intolerable for those which
+provide authenticated access to subscription resources. For such
+circumstances, a more elaborate approach is necessary. The idea is to
+make a local URL that is used for authentication onto the Service
+Proxy, hiding the credentials, and to use local mechanisms to limit
+access to that local authentication URL. Here is one way to do it when
+Apache2 is the application's web-server:
+
+       - Add a rewriting authentication alias to the configuration:
+               RewriteEngine on
+               RewriteRule /spauth/ http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=U&password=PW [P]
+       - Extend the MKWS configuration to set service_proxy_auth:
+               http://application.com/spauth/
+       - Protect access to /apauth/ (e.g. using a .htaccess file).
+
+Once such a library has been set up, and access to it established,
+target selection within the set that it makes available can be done
+using the mechanisms above.
+
diff --git a/doc/mkws-developer.txt b/doc/mkws-developer.txt
new file mode 100644 (file)
index 0000000..7561587
--- /dev/null
@@ -0,0 +1,187 @@
+INTRODUCTION
+============
+
+Development with MKWS consists primarily of defining new types of
+widgets. These can interact with the core functionality is several
+defined ways.
+
+You create a new widget type by calling the mkws.registerWidgetType
+function, passing in the widget name and a function. The name is used
+to recognise HTML elements as being widgets of this type -- for
+example, if you register a "Foo" widget, elements like <div
+class="mkwsFoo"> will be widgets of this type.
+
+The function promotes a bare widget object (passed as `this') into a
+widget of the appropriate type. MKWS doesn't use classes or explicit
+prototypes: it just makes objects that have the necessary
+behaviours. Widgets have *no* behaviours that they have to provide:
+you can make a doesn't-do-anything-at-all widget if you like:
+
+       mkws.registerWidgetType('Sluggard', function() {});
+
+More commonly, widgets will subscribe to one or more events, so that
+they're notified when something interesting happens. For example, the
+"Log" widget asks to be notified when a "log" event happens, and
+appends the logged message to its node, as follows:
+
+       mkws.registerWidgetType('Log', function() {
+           var that = this;
+
+           this.team.queue("log").subscribe(function(teamName, timestamp, message) {
+               $(that.node).append(teamName + ": " + timestamp + message + "<br/>");
+           });
+       });
+
+This simple widget illustrates several important points:
+
+* The base widget object (`this') has several baked-in properties and
+  methods that are available to individual widgets. These include
+  this.team (the team that this widget is a part of) and this.node
+  (the DOM element of the widget).
+
+* The team object (`this.team') also has baked-in properties and
+  methods. These include the queue function, which takes an event-name
+  as its argument. It's possible to subscribe to an event's queue
+  using this.team.queue("EVENT").subscribe. The argument is a function
+  which is called whenever the event is published. The arguments to
+  the function are different for different events.
+
+* The value of `this' is lost inside the subscribe callback, so it
+  must be saved if it's to be used inside that callback (typically as
+  a local variable named `that').
+
+
+SPECIALISATION (INHERITANCE)
+============================
+
+Many widgets are simple specialisations of existing widgets. For
+example, the "Record" widget is the same as the "Records" widget
+except that it defaults to displaying a single record. It's defined as
+follows:
+
+       mkws.registerWidgetType('Record', function() {
+           mkws.promotionFunction('Records').call(this);
+           if (!this.config.maxrecs) this.config.maxrecs = 1;
+       });
+
+Remember that when a promotion function is called, it's passed a base
+widget object that's not specialised for any particular task. To make
+a specialised widget, first promote that base widget into the type
+that you want to specialise from -- in this case, "Records" -- using
+the promotion function that's been registered for that type.
+
+Once this has been done, the specialisations can be introduced. In
+this case, it's a very matter of changing the "maxrecs" configuration
+setting to 1 unless it's already been given an explicit value. (That
+would occur if the HTML used an element like <div class="mkwsRecord"
+maxrecs="2">, though it's not obvious why anyone would do that.)
+
+
+WIDGET PROPERTIES AND METHODS
+=============================
+
+String this.type
+       A string containing the type of the widget.
+
+Team this.team
+       The team object to which this widget belongs. The team has
+       several additional important properties and methods, described
+       below.
+
+DOMElement this.node
+       The DOM element of the widget
+
+Hash this.config
+       A table of configuration values for the widget. This table
+       inherits missing values from the team's configuration, which
+       in turn inherits from the top-level MKWS configuration, which
+       inherits from the default configuration. Instances of widgets
+       in HTML can set configuration items as HTML attributes, as in
+       <div class="mkwsRecords" maxrecs="2">.
+
+String this.toString()
+       A function returning a string that briefly names this
+       widget. Can be useful in logging.
+
+Void this.log(string)
+       A function to log a string for debugging purposes. The string
+       is written on the browser console, and also published to any
+       "log" subcribers.
+
+String this.value()
+       A function returning the value of the widget's HTML element.
+
+
+TEAM METHODS
+============
+
+Since the team object is supposed to be opaque to widgets, all access
+is via the following API methods rather than direct access to
+properties.
+
+String team.name()
+Bool team.submitted()
+Num team.perpage()
+Num team.totalRecordCount()
+Num team.currentPage();
+String team.currentRecordId()
+String team.currentRecordData()
+       Simple accessor functions that provide the ability to read
+       properties of the team.
+
+Array team.filters()
+       Another accessor function, providing access to the array of
+       prevailing filters (which narrow the search results by means
+       of Pazpar2 filters and limits). This is really too complicated
+       an object for the widgets to be given access to, but it's
+       convenient to do it this way. See the "Navi" widget, which is
+       the only place it's used.
+
+Hash team.config()
+       Access to the team's configuration settings. There is almost
+       certainly no reason to use this: the settings that haven't
+       been overridden are accessible via this.config.
+
+Void team.set_sortOrder(string)
+Void team.set_perpage(number)
+       "Setter" functions for the team's sortOrder and perpage
+       functions. Unlikely to be needed outside of the "Sort" and
+       "Perpage" widgets.
+
+Queue team.queue(eventName)
+       Returns the queue associated with the named event: this can be
+       used to subscribe to the event (or more rarely to publish it).
+
+Bool team.targetFiltered(targetId)
+       Indicates whether the specified target has been filtered by
+       selection as a facet.
+
+Void team.newSearch(query, sortOrder, maxrecs, perpage, limit, targets, targetfilter)
+       Starts a new search with the specified parameters. All but the
+       query may be omitted, in which case the prevailing defaults
+       are used.
+
+Void team.reShow()
+       Using the existing search, re-shows the result records after a
+       change in sort-order, per-page count, etc.
+
+String team.recordElementId(recordId)
+       Utility function for converting a record identifer (returned
+       from Pazpar2) into a version suitable for use as an HTML
+       element ID.
+
+String team.renderDetails(recordData)
+       Utility function returns an HTML rendering of the record
+       represented by the specified data.
+
+Template team.loadTemplate(templateName)
+       Loads (or retrieves from cache) the named Handlebars template,
+       and returns it in a form that can be invoked as a function,
+       passed a data-set.
+
+Some of these methods either (A) are really too low-level and should
+not be exposed, or (B) should be widget-level methods. The present
+infelicities reflect the fact that some code that rightly belongs in
+widgets is still in the team. When we finish migrating it, the widget
+API should get simpler.
+
diff --git a/doc/mkws-doc.css b/doc/mkws-doc.css
new file mode 100644 (file)
index 0000000..a58c10f
--- /dev/null
@@ -0,0 +1,76 @@
+body {
+    font-family: Times, "Times Roman", "Times New Roman";
+}
+
+h1, h2, h3, h4 {
+    color: #68a;
+    font-weight: bold;
+    font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
+}
+
+h2 a, h3 a, h4 a, div#TOC a {
+    color: #68a;
+    text-decoration: none;
+}
+
+h2, h3 {
+    /* Default spacing is way off in both Chrome and Firefox */
+    margin-bottom: -0.5em;
+}
+
+h1 {
+    background: #e0e8f8;
+    padding: 0.2em;
+    font-weight: normal;
+}
+
+h2.author {
+    font-size: 120%;
+    color: black
+}
+
+h3.date {
+    font-size: 100%;
+    color: black
+}
+
+body > p, ul, ol, pre, table, h4 {
+    margin-left: 10%;
+}
+
+p, ul {
+    max-width: 40em;
+}
+
+pre {
+    background: #eee;
+}
+
+table tr th {
+    color: white;
+    background: #68a;
+    font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
+}
+
+th, td {
+    padding: 0.2em 0.5em;
+    vertical-align: top;
+}
+
+table tr:nth-child(odd) {
+    background: #a9c6e3;
+}
+
+table tr:nth-child(even) {
+    background: #bfdcf8;
+}
+
+/*
+ * Works with the HTML emitted by pandoc. It would better if pandoc
+ * were to emit class names that we can use. But it doesn't.
+ */  
+body > p:last-of-type {
+    font-size: small;
+    max-width: none;
+    text-align: right;
+}
diff --git a/doc/whitepaper.markdown b/doc/whitepaper.markdown
new file mode 100644 (file)
index 0000000..376da7a
--- /dev/null
@@ -0,0 +1,595 @@
+% Embedded metasearching with the MasterKey Widget Set
+% Mike Taylor
+% July-September 2013
+
+
+Introduction
+------------
+
+There are lots of practical problems in building resource discovery
+solutions. One of the biggest, and most ubiquitous is incorporating
+metasearching functionality into existing web-sites -- for example,
+content-management systems, library catalogues or intranets. In
+general, even when access to core metasearching functionality is
+provided by simple web-services such as
+[Pazpar2](http://www.indexdata.com/pazpar2), integration work is seen
+as a major part of most projects.
+
+Index Data provides several different toolkits for communicating with
+its metasearching middleware, trading off varying degrees of
+flexibility against convenience:
+
+* pz2.js -- a low-level JavaScript library for interrogating the
+  Service Proxy and Pazpar2. It allows the HTML/JavaScript programmer
+  to create JavaScript applications display facets, records, etc. that
+  are fetched from the metasearching middleware.
+
+* masterkey-ui-core -- a higher-level, complex JavaScript library that
+  uses pz2.js to provide the pieces needed for building a
+  full-featured JavaScript application.
+
+* MasterKey Demo UI -- an example of a searching application built on
+  top of masterkey-ui-core. Available as a public demo at
+  http://mk2.indexdata.com/
+
+* MKDru -- a toolkit for embedding MasterKey-like searching into
+  Drupal sites.
+
+All of these approaches require programming to a greater or lesser
+extent. Against this backdrop, we introduced MKWS (the MasterKey
+Widget Set) -- a set of simple, very high-level HTML+CSS+JavaScript
+components that can be incorporated into any web-site to provide
+MasterKey searching facilities. By placing `<div>`s with well-known
+identifiers in any HTML page, the various components of an application
+can be embedded: search-boxes, results areas, target information, etc.
+
+
+Simple Example
+--------------
+
+The following is a complete MKWS-based searching application:
+
+    <html>
+      <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+        <title>MKWS demo client</title>
+        <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+        <link rel="stylesheet" href="http://mkws.indexdata.com/mkws.css" />
+      </head>
+      <body>
+        <div id="mkwsSearch"></div>
+        <div id="mkwsResults"></div>
+      </body>
+    </html>
+
+Go ahead, try it! You don't even need a web-server. Just copy and
+paste this HTML into a file on your computer -- `/tmp/magic.html`,
+say -- and point your web-browser at it:
+`file:///tmp/magic.html`. Just like that, you have working
+metasearching.
+
+
+How the example works
+---------------------
+
+If you know any HTML, the structure of the file will be familar to
+you: the `<html>` element at the top level contains a `<head>` and a
+`<body>`. In addition to whatever else you might want to put on your
+page, you can add MKWS elements.
+
+These fall into two categories. First, the prerequisites in the HTML
+header, which are loaded from the tool site mkws.indexdata.com:
+
+* `mkws-complete.js`
+  contains all the JavaScript needed by the widget-set.
+
+* `mkws.css`
+  provides the default CSS styling 
+
+Second, within the HTML body, `<div>` elements with special IDs that
+begin `mkws` can be provided. These are filled in by the MKWS code,
+and provide the components of the searching UI. The very simple
+application above has only two such components: a search box and a
+results area. But more are supported. The main `<div>`s are:
+
+* `mkwsSearch` -- provides the search box and button.
+
+* `mkwsResults` -- provides the results area, including a list of
+   brief records (which open out into full versions when clicked),
+   paging for large results sets, facets for refining a search,
+   sorting facilities, etc.
+
+* `mkwsLang` -- provides links to switch between one of several
+   different UI languages. By default, English, Danish and German are
+   provided.
+
+* `mkwsSwitch` -- provides links to switch between a view of the
+   result records and of the targets that provide them. Only
+   meaningful when `mkwsTargets` is also provided.
+
+* `mkwsTargets` -- the area where per-target information will appear
+   when selected by the link in the `mkwsSwitch` area. Of interest
+   mostly for fault diagnosis rather than for end-users.
+
+* `mkwsStat` --provides a status line summarising the statistics of
+   the various targets.
+
+To see all of these working together, just put them all into the HTML
+`<body>` like so:
+
+        <div id="mkwsSwitch"></div>
+        <div id="mkwsLang"></div>
+        <div id="mkwsSearch"></div>
+        <div id="mkwsResults"></div>
+        <div id="mkwsTargets"></div>
+        <div id="mkwsStat"></div>
+
+Configuration
+-------------
+
+Many aspects of the behaviour of MKWS can be modified by setting
+parameters into the `mkws_config` object. **This must be done *before*
+including the MKWS JavaScript** so that when that code is executed it
+can refer to the configuration values. So the HTML header looks like
+this:
+
+        <script type="text/javascript">
+          var mkws_config = {
+            lang: "da",
+            sort_default: "title",
+            query_width: 60
+          };
+        </script>
+        <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+
+This configuration sets the UI language to Danish (rather than the
+default of English), initially sorts search results by title rather
+than relevance (though as always this can be changed in the UI) and
+makes the search box a bit wider than the default.
+
+The full set of supported configuration items is described in the
+reference guide below.
+
+
+Control over HTML and CSS
+-------------------------
+
+More sophisticated applications will not simply place the `<div>`s
+together, but position them carefully within an existing page
+framework -- such as a Drupal template, an OPAC or a SharePoint page.
+
+While it's convenient for simple applications to use a monolithic
+`mkwsResults` area which contains record, facets, sorting options,
+etc., customised layouts may wish to treat each of these components
+separately. In this case, `mkwsResults` can be omitted, and the
+following lower-level components provided instead:
+
+* `mkwsTermlists` -- provides the facets
+
+* `mkwsRanking` -- provides the options for how records are sorted and
+   how many are included on each page of results.
+
+* `mkwsPager` -- provides the links for navigating back and forth
+   through the pages of records.
+
+* `mkwsNavi` -- when a search result has been narrowed by one or more
+   facets, this area shows the names of those facets, and allows the
+   selected values to be clicked in order to remove them.
+
+* `mkwsRecords` -- lists the actual result records.
+
+Customisation of MKWS searching widgets can also be achieved by
+overriding the styles set in the toolkit's CSS stylesheet. The default
+styles can be inspected in `mkws.css` and overridden in any
+styles that appears later in the HTML than that file. At the simplest
+level, this might just mean changing fonts, sizes and colours, but
+more fundamental changes are also possible.
+
+To properly apply styles, it's necessary to understand how the HTML is
+structured, e.g. which elements are nested within which
+containers. The structures used by the widget-set are described in the
+reference guide below.
+
+
+Refinements
+-----------
+
+
+### Message of the day
+
+Some applications might like to open with content in the area that
+will subsequently be filled with result-records -- a message of the
+day, a welcome message or a help page. This can be done by placing an
+`mkwsMOTD` division anywhere on the page. It will be moved into the
+`mkwsResults` area and initially displayed, but will be hidden when a
+search is made.
+
+
+### Customised display using Handlebars templates
+
+Certain aspects of the widget-set's display can be customised by
+providing Handlebars templates with well-known classes that begin with
+the string `mkwsTemplate_`. At present, the supported templates are:
+
+* `mkwsTemplate_Summary` -- used for each summary record in a list of
+  results.
+
+* `mkwsTemplate_Record` -- used when displaying a full record.
+
+For both of these the metadata record is passed in, and its fields can
+be referenced in the template. As well as the metadata fields
+(`md-*`), two special fields are provided to the `mkwsTemplate_Summary`
+template, for creating popup links for full records. These are `_id`,
+which must be provided as the `id` attribute of a link tag, and
+`_onclick`, which must be provided as the `onclick` attribute.
+
+For example, an application can install a simple author+title summary
+record in place of the usual one providing the following template:
+
+        <script class="mkwsTemplate_Summary" type="text/x-handlebars-template">
+          {{#if md-author}}
+            <span>{{md-author}}</span>
+          {{/if}}
+          <a href="#" id="{{_id}}" onclick="{{_onclick}}">
+            <b>{{md-title}}</b>
+          </a>
+        </script>
+
+For details of Handlebars template syntax, see
+[the online documentation](http://handlebarsjs.com/).
+
+
+### Responsive design
+
+Metasearching applications may need to appear differently on
+small-screened mobile devices, or change their appearance when
+screen-width changes (as when a small device is rotated). To achieve
+this, MKWS supports responsive design which will move the termlists to
+the bottom on narrow screens and to the sidebar on wide screens.
+
+To turn on this behaviour, set the `responsive_design_width` to the desired
+threshhold width in pixels. For example:
+
+        <script type="text/javascript">
+            var mkws_config = {
+                responsive_design_width: 990
+            };
+        </script>
+
+If individual result-related components are in use in place of the
+all-in-one mkwsResults, then the redesigned application needs to
+specify the locations where the termlists should appear in both
+cases. In this case, wrap the wide-screen `mkwsTermlists` element in a
+`mkwsTermlistContainer1` element; and provide an
+`mkwsTermlistContainer2` element in the place where the narrow-screen
+termlists should appear.
+
+
+### Popup results with jQuery UI
+
+The [jQuery UI library](http://en.wikipedia.org/wiki/JQuery_UI)
+can be used to construct MKWS applications in which the only component
+generally visible on the page is a search box, and the results appear
+in a popup. The key part of such an application is this invocation of
+the MKWS jQuery plugin:
+
+        <script type="text/javascript">
+          jQuery.pazpar2({ "layout":"popup", width:800, height:500 });
+        </script>
+
+The necessary scaffolding can be seen in an example application,
+http://example.indexdata.com/index-popup.html
+
+
+### Authentication and target configuration
+
+By default, MKWS configures itself to use a demonstration account on a
+service hosted by mkws.indexdata.com. This account (username `demo`,
+password `demo`) provides access to about a dozen free data
+sources. Authentication onto this service is via an authentication URL
+on the same MKWS server, so no explicit configuration is needed.
+
+In order to search in a customised set of targets, including
+subscription resources, it's necessary to create an account with
+Index Data's hosted service proxy, and protect that account with
+authentication tokens (to prevent unauthorised use of subscription
+resources). But in order to gain access to those resources, the
+authentication tokens have to be available to the widgets in some way,
+and simple embedding them in the JavaScript configuration is not
+acceptable because they are easy to read from there.
+
+The solution to this problem is in three steps.
+
+<b>First</b>
+the application's web-server creates a rewriting rule that takes an
+innocuous URL like
+http://example.indexdata.com/service-proxy-auth/
+and rewrites it as an access to Index Data's authentication service
+with authentication credentials embedded. This can be done using
+Apache2 directives such as
+
+    RewriteEngine on
+    RewriteRule /service-proxy-auth/
+        http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=U&password=PW [P]
+
+Because the credentials appear only in the application's web-server
+configuration, they are not visible to malicious users.
+
+<b>Second</b>, the broader application that includes MKWS widgets must
+protect access to the authentication URL on its own web-server. This
+can be done using IP authentication, a local username/password scheme,
+Kerberos or any other means.
+
+<b>Third</b>, the MKWS application must be configured to use the
+application-hosted authentication URL instead of the default one. This
+is done by means of the `service_proxy_auth` configuration element,
+which should be set to the authentication URL.
+
+Once these three steps are taken, the MKWS application will
+authenticate by means of a special URL on the application's web
+server, which the application prevents unauthorised access to, and the
+underlying credentials are hidden.
+
+
+Reference Guide
+---------------
+
+### Configuration object
+
+The configuration object `mkws_config` may be created before including
+the MKWS JavaScript code to modify default behaviour. This structure
+is a key-value lookup table, whose entries are described in the table
+below. All entries are optional, but if specified must be given values
+of the specified type. If ommitted, each setting takes the indicated
+default value; long default values are in footnotes to keep the table
+reasonably narrow.
+
+---
+Element                   Type    Default   Description
+--------                  -----   --------- ------------
+debug_level               int     1         Level of debugging output to emit. 0 = none, 1 = messages, 2 = messages with
+                                            datestamps, 3 = messages with datestamps and stack-traces.
+
+facets                    array   *Note 1*  Ordered list of names of facets to display. Supported facet names are 
+                                            `xtargets`, `subject` and `author`.
+
+lang                      string  en        Code of the default language to display the UI in. Supported language codes are `en` =
+                                            English, `de` = German, `da` = Danish, and whatever additional languages are configured
+                                            using `language_*` entries (see below).
+
+lang_options              array   []        A list of the languages to offer as options. If empty (the default), then all
+                                            configured languages are listed.
+
+language_*                hash              Support for any number of languages can be added by providing entries whose name is
+                                            `language_` followed by the code of the language. See the separate section below for
+                                            details.
+
+pazpar2_url               string  *Note 2*  The URL used to access the metasearch middleware. This service must be configured to
+                                            provide search results, facets, etc. It may be either unmediated or Pazpar2 the
+                                            MasterKey Service Proxy, which mediates access to an underlying Pazpar2 instance. In
+                                            the latter case, `service_proxy_auth` must be provided.
+
+perpage_default           string  20        The initial value for the number of records to show on each page.
+
+perpage_options           array   *Note 3*  A list of candidate page sizes. Users can choose between these to determine how many
+                                            records are displayed on each page of results.
+
+query_width               int     50        The width of the query box, in characters.
+
+responsive_design_width   int               If defined, then the facets display moves between two locations as the screen-width
+                                            varies, as described above. The specified number is the threshhold width, in pixels,
+                                            at which the facets move between their two locations.
+
+service_proxy_auth        url     *Note 4*  A URL which, when `use_service_proxy` is true, is fetched once at the beginning of each
+                                            session to authenticate the user and establish a session that encompasses a defined set
+                                            of targets to search in.
+
+service_proxy_auth_domain domain            Can be set to the domain for which `service_proxy_auth` proxies authentication, so
+                                            that cookies are rewritten to appear to be from this domain. In general, this is not
+                                            necessary, as this setting defaults to the domain of `pazpar2_url`.
+
+show_lang                 bool    true      Indicates whether or not to display the language menu.
+
+show_perpage              bool    true      Indicates whether or not to display the perpage menu.
+
+show_sort                 bool    true      Indicates whether or not to display the sort menu.
+
+sort_default              string  relevance The label of the default sort criterion to use. Must be one of those in the `sort`
+                                            array.
+
+sort_options              array   *Note 6*  List of supported sort criteria. Each element of the list is itself a two-element list:
+                                            the first element of each sublist is a pazpar2 sort-expression such as `data:0` and
+                                            the second is a human-readable label such as `newest`.
+
+use_service_proxy         bool    true      If true, then a Service Proxy is used to deliver searching services rather than raw
+                                            Pazpar2.
+---
+
+Perhaps we should get rid of the `show_lang`, `show_perpage` and
+`show_sort` configuration items, and simply display the relevant menus
+only when their containers are provided -- e.g. an `mkwsLang` element
+for the language menu. But for now we retain these, as an easier route
+to lightly customise the display than my changing providing a full HTML
+structure.
+
+#### Notes
+
+1. ["sources", "subjects", "authors"]
+
+2. /pazpar2/search.pz2
+
+3. [10, 20, 30, 50]
+
+4. http://mkws.indexdata.com/service-proxy-auth
+
+5. http://mkws.indexdata.com/service-proxy/
+
+6. [["relevance"], ["title:1", "title"], ["date:0", "newest"], ["date:1", "oldest"]]
+
+
+### Language specification
+
+Support for another UI language can be added by providing an entry in
+the `mkws_config` object whose name is `language_` followed by the
+name of the language: for example, `language_French` to support
+French. Then value of this entry must be a key-value lookup table,
+mapping the English-language strings of the UI into their equivalents
+in the specified language. For example:
+
+            var mkws_config = {
+              language_French: {
+                "Authors": "Auteurs",
+                "Subjects": "Sujets",
+                // ... and others ...
+              }
+            }
+
+The following strings occurring in the UI can be translated:
+`Displaying`,
+`Next`,
+`Prev`,
+`Records`,
+`Search`,
+`Sort by`,
+`Targets`,
+`Termlists`,
+`and show`,
+`found`,
+`of`,
+`per page`
+and
+`to`.
+
+In addition, facet names can be translated:
+`Authors`,
+`Sources`
+and
+`Subjects`.
+
+Finally, the names of fields in the full-record display can be
+translated. These include, but may not be limited to:
+`Author`,
+`Date`,
+`Location`,
+`Subject`
+and
+`Title`.
+
+
+
+### jQuery plugin invocation
+
+The MasterKey Widget Set can be invoked as a jQuery plugin rather than
+by providing an HTML skeleton explicitly. When this approach is used,
+the invocation is a single line of JavaScript:
+
+        <script>jQuery.pazpar2();</script>
+
+This code should be inserted in the page at the position where the
+metasearch should occur.
+
+When invoking this plugin, a key-value lookup table of named options
+may be passed in to modify the default behaviour, as in the exaple
+above. The available options are as follows:
+
+---
+Element    Type    Default           Description
+--------   -----   ---------         ------------
+layout     string  popup             Specifies how the user interface should
+                                     appear. Options are `table` (the default,
+                                     with facets at the bottom), `div` (with
+                                     facets at the side) and `popup` (to
+                                     obtain a popup window).
+
+width      int     880               Width of the popup window (if used), in
+                                     pixels.
+
+height     int     760               Height of the popup window (if used), in
+                                     pixels.
+
+id_button  string  input#mkwsButton  (Never change this.)
+
+id_popup   string  #mkwsPopup        (Never change this.)
+---
+
+Note that when using the `popup` layout, facilities from the jQuery UI
+toolkit are used, so it's necessary to include both CSS and JavaScript
+from that toolkit. The relevant lines are:
+
+    <script src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
+    <link rel="stylesheet" type="text/css"
+          href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
+
+
+### The structure of the HTML generated by the MKWS widgets
+
+In order to override the default CSS styles provided by the MasterKey Widget
+Set, it's necessary to understand that structure of the HTML elements that are
+generated within the components. This knowledge make it possible, for example,
+to style each `<div>` with class `term` but only when it occurs inside an
+element with ID `#mkwsTermlists`, so as to avoid inadvertently styling other
+elements using the same class in the non-MKWS parts of the page.
+
+The HTML structure is as follows. As in CSS, #ID indicates a unique identifier
+and .CLASS indicates an instance of a class.
+
+    #mkwsSwitch
+      a*
+
+    #mkwsLang
+      ( a | span )*
+
+    #mkwsSearch
+      form
+        input#mkwsQuery type=text
+        input#mkwsButton type=submit
+
+    #mkwsBlanket
+      (no contents -- used only for masking)
+
+    #mkwsResults
+      table
+        tbody
+          tr
+            td
+              #mkwsTermlists
+                div.title
+                div.facet*
+                  div.termtitle
+                  ( a span br )*
+            td
+              div#mkwsRanking
+                form#mkwsSelect
+                  select#mkwsSort
+                  select#mkwsPerpage
+              #mkwsPager
+              #mkwsNavi
+              #mkwsRecords
+                div.record*
+                  span (for sequence number)
+                  a (for title)
+                  span (for other information such as author)
+                  div.details (sometimes)
+                    table
+                      tbody
+                        tr*
+                          th
+                          td
+    #mkwsTargets
+      #mkwsBytarget
+        table
+          thead
+            tr*
+              td*
+          tbody
+            tr*
+              td*
+
+    #mkwsStat
+      span.head
+      span.clients
+      span.records
+
+- - -
+
+Copyright (C) 2013 by IndexData ApS, <http://www.indexdata.com>
diff --git a/examples/apache2/example-dev b/examples/apache2/example-dev
new file mode 100644 (file)
index 0000000..c0aff12
--- /dev/null
@@ -0,0 +1,11 @@
+# This is for the example-dev.indexdata.com, running on dart
+
+<VirtualHost *:80>
+    ServerName example-dev.indexdata.com
+    DocumentRoot /home/indexdata/mkws-dev/examples/htdocs/
+    ErrorLog /var/log/apache2/example-dev-error.log
+    CustomLog /var/log/apache2/example-dev.log combined
+
+    RewriteEngine on
+</VirtualHost>
+
diff --git a/examples/apache2/example-dev-px b/examples/apache2/example-dev-px
new file mode 100644 (file)
index 0000000..49b747e
--- /dev/null
@@ -0,0 +1,19 @@
+<VirtualHost *:80>
+    ServerName example-dev.indexdata.com
+
+    ProxyRequests off
+    ProxyVia On
+    ProxyPreserveHost On
+    <Proxy *>
+      Order deny,allow
+      Allow from all
+    </Proxy>
+
+    ProxyPass         / http://dart:80/
+    ProxyPassReverse  / http://dart:80/
+
+    # These are the logs for the proxying operation
+    ErrorLog /var/log/apache2/example-dev-px-error.log
+    CustomLog /var/log/apache2/example-dev-px-access.log combined
+</VirtualHost>
+
diff --git a/examples/apache2/example-dev-ssl-px b/examples/apache2/example-dev-ssl-px
new file mode 100644 (file)
index 0000000..04106db
--- /dev/null
@@ -0,0 +1,48 @@
+# A very simple configuration to proxy the irspy
+
+<VirtualHost *:443>
+    ServerName example.indexdata.com 
+    ServerAlias example-dev.indexdata.com
+
+  <IfModule mod_ssl.c>
+    SSLEngine on
+    SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown
+
+    #SSLCertificateFile /etc/ssl/certs/indexdata.com/id.cert
+    #SSLCertificateKeyFile /etc/ssl/certs/indexdata.com/id.key
+
+    SSLProxyEngine on
+  </IfModule>
+
+    # Remove the X-Forwarded-For header, as the proxy appends to it,
+    # and we need a clean ip address for the statistics
+    # RequestHeader unset X-Forwarded-For early
+    # Never mind
+
+    # ProxyRequests off
+    <Proxy *>
+      Order deny,allow
+      Allow from all
+    </Proxy>
+
+    ProxyPreserveHost On
+    ProxyPass         / http://dart/
+    ProxyPassReverse  / http://dart/
+
+    # Experiments to hunt down bu 3716
+    # Increase buffer size so that we don't go for chunked stuff
+    # ProxyIOBufferSize 8192
+    # Didn't help
+    # Disable gzipping
+    # RequestHeader unset Accept-Encoding
+    # Didn't help 
+    # ProxyReceiveBufferSize 8192
+    # Didn't help 
+    SetEnv force-proxy-request-1.0 1
+    SetEnv proxy-nokeepalive 1
+
+    # These are the logs for the proxying operation
+    ErrorLog     /var/log/apache2/example-dev-ssl-error.log
+    CustomLog    /var/log/apache2/example-dev-ssl-access.log combined
+</VirtualHost>
+
diff --git a/examples/apache2/example-ssl-px b/examples/apache2/example-ssl-px
new file mode 100644 (file)
index 0000000..159b28b
--- /dev/null
@@ -0,0 +1,48 @@
+# A very simple configuration to proxy the irspy
+
+<VirtualHost *:443>
+    ServerName example.indexdata.com 
+    ServerAlias example-dev.indexdata.com
+
+  <IfModule mod_ssl.c>
+    SSLEngine on
+    SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown
+
+    #SSLCertificateFile /etc/ssl/certs/indexdata.com/id.cert
+    #SSLCertificateKeyFile /etc/ssl/certs/indexdata.com/id.key
+
+    SSLProxyEngine on
+  </IfModule>
+
+    # Remove the X-Forwarded-For header, as the proxy appends to it,
+    # and we need a clean ip address for the statistics
+    # RequestHeader unset X-Forwarded-For early
+    # Never mind
+
+    # ProxyRequests off
+    <Proxy *>
+      Order deny,allow
+      Allow from all
+    </Proxy>
+
+    ProxyPreserveHost On
+    ProxyPass         / http://caliban/
+    ProxyPassReverse  / http://caliban/
+
+    # Experiments to hunt down bu 3716
+    # Increase buffer size so that we don't go for chunked stuff
+    # ProxyIOBufferSize 8192
+    # Didn't help
+    # Disable gzipping
+    # RequestHeader unset Accept-Encoding
+    # Didn't help 
+    # ProxyReceiveBufferSize 8192
+    # Didn't help 
+    SetEnv force-proxy-request-1.0 1
+    SetEnv proxy-nokeepalive 1
+
+    # These are the logs for the proxying operation
+    ErrorLog     /var/log/apache2/example-ssl-error.log
+    CustomLog    /var/log/apache2/example-ssl-access.log combined
+</VirtualHost>
+
index 460e154..472071b 100644 (file)
@@ -7,7 +7,7 @@
     CustomLog /var/log/apache2/mkws-examples-access.log combined
 
     RewriteEngine on
-    RewriteRule /service-proxy-auth/ http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=demo&password=demo [P]
+    RewriteRule /service-proxy-auth http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=mkws&password=mkws [P]
     #RewriteLog /var/log/apache2/mkws-examples-rewrite.log
     #RewriteLogLevel 9
 </VirtualHost>
index bf09873..c867600 100644 (file)
@@ -1,11 +1,13 @@
 <VirtualHost *:80>
     ServerName x.example.indexdata.com
     DocumentRoot /usr/local/src/git/mkws/examples/htdocs/
+    Alias /tools/htdocs/ /usr/local/src/git/mkws/tools/htdocs/
+    Alias /src/ /usr/local/src/git/mkws/src/
     ErrorLog /var/log/apache2/mkws-examples-error.log
     CustomLog /var/log/apache2/mkws-examples-access.log combined
 
     RewriteEngine on
-    RewriteRule      /service-proxy-auth/ http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=demo&password=demo [P]
+    RewriteRule      /service-proxy-auth/ http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=mkws&password=mkws [P]
     #RewriteLog /var/log/apache2/mkws-examples-rewrite.log
     #RewriteLogLevel 9
 </VirtualHost>
index 0441bb8..683cb0e 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2013 IndexData ApS. http://indexdata.com
+# Copyright (c) 2013-2014 IndexData ApS. http://indexdata.com
 
 all: apache-config.txt jasmine-links
 
@@ -8,8 +8,6 @@ apache-config.txt: ../apache2/mkws-examples-mike
        chmod ugo-w $@
 
 jasmine-links:
-       ln -fs ../../../jasmine .
-       ln -fs ../../test .
 
 help:
        @echo "make [ all | clean | jasmine-links ]"
diff --git a/examples/htdocs/README b/examples/htdocs/README
new file mode 100644 (file)
index 0000000..a1f96b1
--- /dev/null
@@ -0,0 +1,34 @@
+Development
+===========================================
+
+please run first in this directory: 
+$ make jasmine-links
+
+
+jasmine.html      - jasmine test with standard HTML page. 
+
+jasmine-popup.html - jasmine test with MKWS popup. No HTML, only JavaScript.
+                    Returns better readable test results as jasmine.html, 
+                    but it is less flexible.
+
+
+Public demo pages
+-----------------
+auto.html
+dict.html
+index.html
+jquery.html 
+language.html
+localauth.html
+lowlevel.html
+minimal.html 
+mobile.html
+popup.html 
+
+Jasmine test pages
+------------------
+jasmine-popup.html     - jasmine test with jquery popup, best for JS/HTML testing
+jasmine-local-popup.html  jasmine test with local SP and jquery popup, best for development
+jasmine-pp2.html       - running with local pazpar2 instead SP
+jasmine.html           - standard jasmine test
+
diff --git a/examples/htdocs/auto-paratext-minimal.html b/examples/htdocs/auto-paratext-minimal.html
new file mode 100644 (file)
index 0000000..24f14cc
--- /dev/null
@@ -0,0 +1,14 @@
+<html>
+  <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
+  <script type="text/javascript">
+    mkws_config = {
+      service_proxy_auth: "http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=paratext&password=paratext_mkc",
+      perpage_default: 5,
+   };
+  </script>
+  <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
+  <div style="width: 30em; background: #f0f0f0">
+    <h2>Reference results from Paratext</h2>
+    <div class='mkwsRecords' autosearch='!param!q' sort='position'></div>
+  </div>
+</html>
diff --git a/examples/htdocs/auto-paratext.html b/examples/htdocs/auto-paratext.html
new file mode 100644 (file)
index 0000000..b7c0b88
--- /dev/null
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta name="copyright" content="(c) 1999-2013 IndexData ApS, http://indexdata.com" />
+    <title>MKWS demo: Automatic search</title>
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript">
+      mkws_config = {
+        service_proxy_auth: "http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=paratext&password=paratext_mkc",
+       perpage_default: 5,
+     };
+    </script>
+    <script type="text/javascript" src="//code.jquery.com/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/pazpar2/js/pz2.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws.js"></script>
+    <style type="text/css">
+      .panel {
+       font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
+       width: 25em;
+       float: right;
+        background: #f0f0f0;
+       border: 1px solid #b0b0b0;
+       border-radius: 1em;
+        margin-left: 1em;
+      }
+      .panel h2 {
+       color: #606060;
+       padding-left: 5px;
+      }
+      .entrybody {
+        max-width: 40em;
+      }
+      .wp-caption-text {
+        font-size: small;
+        background: #f0f0f0;
+        padding: 0.5em 1em;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>How big was <i>Amphicoelias fragillimus</i>?  I mean, really?</h1>
+    <p>
+      <i>February 19, 2010</i>
+    </p>
+
+    <div class="panel">
+      <h2>Reference results from Paratext</h2>
+      <div class='mkwsRecords' autosearch='!param!q' sort='position'></div>
+    </div>
+
+    <div class="entrybody">
+      <p>Lovers of fine sauropods will be well aware that, along with the inadequately described Indian titanosaur <em>Bruhathkayosarus</em>, the other of the truly super-giant sauropods is <em>Amphicoelias fragillimus</em>.  Known only from a single neural arch of a dorsal vertebra, which was figured and briefly described by Cope (1878) and almost immediately either lost or destroyed, it's the classic "one that got away", the animal that sauropod aficionados cry into their beer about late at night.</p>
+      <div id="attachment_2526" style="width: 471px" class="wp-caption alignnone"><a href="http://svpow.files.wordpress.com/2010/02/osbornmook1921-fig17-amphicoelias-fragillimus.png"><img class="size-full wp-image-2526" title="OsbornMook1921-fig17-amphicoelias-fragillimus" alt="" src="http://svpow.files.wordpress.com/2010/02/osbornmook1921-fig17-amphicoelias-fragillimus.png?w=360"   /></a><p class="wp-caption-text">Amphicoelias fragillimus, holotype dorsal vertebral neural arch in posterior view. From Osborn and Mook (1921:fig. 21), which in turn was gently tweaked from Cope (1878:unnumbered and only figure).</p></div>
+      <p>I'm not going to write about <em>A</em>. <em>fragillimus</em> in detail here, because Darren's so recently covered it in detail over at Tetrapod Zoology -- read <a href="http://scienceblogs.com/tetrapodzoology/2009/12/biggest_sauropod_ever_part_i.php">Part 1</a> and <a href="http://scienceblogs.com/tetrapodzoology/2010/01/biggest_sauropod_ever_part_ii.php">Part 2</a> right now if you've not already done so.  The bottom line is that it was a diplodocoid roughly twice as big as <em>Diplodocus</em> in linear dimension (so about eight times as heavy).  That makes it very very roughly 50 m long and 100 tonnes in mass.</p>
+      <p><em>But Mike!</em>, you say, <em>Isn't it terribly naive to go calculating masses and all from a single figure of part of a single bone?</em></p>
+      <p>Why, yes!  Yes, it is!  And that is what this post is about.</p>
+      <p>As I write, the go-to paper on <em>A</em>. <em>fragillimus</em> is Ken Carpenter's (2006) re-evaluation, which carefully and tentatively estimated a length of 58 m, and a mass of around 122,400 kg.</p>
+      <p>As it happens, Matt and a colleague submitted a conference abstract a few days ago, and he ran it past me for comments before finalising.  In passing, he'd written "there is no evidence for sauropods larger than 150 metric tons and it is possible that the largest sauropods did not exceed 100 tons".  I replied:</p>
+      <p style="padding-left:30px;">I think that is VERY unlikely. [...] the evidence for <em>Amphicoelias fragillimus</em> looks very convincing, Carpenter's (2006) mass estimate of 122.4 tonnes is conservative, being extrapolated from Greg Paul's ultra-light 11.5 tonne <em>Diplodocus</em>.</p>
+      <p>Carpenter's estimate is based on a reconstruction of the illustrated vertebra, which when complete he calculated would have been 2.7 m tall.  That is 2.2 times the height of the corresponding vertebra in <em>Diplodocus</em>, and the whole animal was considered as it might be if it were like Diplo scaled up by that factor.  Here is his reconstruction of the vertebra, based on Cope's figure of the smaller but better represented species <em>Amphicoelias altus</em>:</p>
+      <div id="attachment_2527" style="width: 490px" class="wp-caption alignnone"><a href="http://svpow.files.wordpress.com/2010/02/carpenter2006-fig1.png"><img class="size-full wp-image-2527" title="Carpenter2006-fig1" alt="" src="http://svpow.files.wordpress.com/2010/02/carpenter2006-fig1.png?w=480&#038;h=562" width="480" height="562" /></a><p class="wp-caption-text">One possible reconstruction of the Alphicoelias fragillimus vertebra, from Carpenter (2006:fig. 1).  Part A is Cope's original figure annotated with lamina designations; part C is Cope's illustration of an Amphocoelias altus dorsal; part B is Carpenter's reconstruction of the former after the latter.</p></div>
+      <p>Matt's answer to me was:</p>
+      <p style="padding-left:30px;">First, Paul's ultra-light 11.5 tonne Dippy is not far off from my 12 tonne version that you frequently cite, and mine should be lighter because it doesn't include large air sacs (density of 0.8 instead of a more likely 0.7). If my Dippy had an SG of 0.7, it would have massed only 10.25 tonnes. Second, Carpenter skewed [...] in the direction of large size for <em>Amphicoelias</em>. I don't <em>necessarily</em> think he's wrong, but his favoured estimate is at the extreme of what the data will support. Let's say that <em>Amphicoelias</em> was evenly twice as large as Dippy in linear terms; that could still give it a mass as low as 90 tonnes. And that's not including the near-certainty that <em>Amphicoelias</em> had a much higher ASP than <em>Diplodocus</em>. If <em>Amphicoelias</em> was to <em>Diplodocus</em> as <em>Sauroposeidon</em> was to <em>Brachiosaurus</em> -- pneumatic bones about half as dense -- then 1/10 of its volume weighed Â½ as much as it would if it were vanilla scaled up Dippy, and we might be able to knock off another 5 tonnes.</p>
+      <p>There's lots of good stuff here, and there was more back and forth following, which I won't trouble you with.  But what I came away with was the idea that maybe the scale factor was wrong.  And the thing to do, I thought, was to make my own sealed-room reconstruction and see how it compared.</p>
+      <p>So I extracted the A.f. figure from Osborn and Mook, and deleted their dotted reconstruction lines.  Then I went and did something else for a while, so that any memory of where those lines might have been had a chance to fade.  I was careful not look at Carpenter's reconstruction, so I could be confident mine would be indepedent.  Then I photoshopped the cleaned <em>A</em>. <em>fragillimus</em> figure into a copy the <em>A</em>. <em>altus</em> figure, scaled it to fit the best as I saw it, and measured the results.  Here it is:</p>
+      <div id="attachment_2532" style="width: 490px" class="wp-caption alignnone"><a href="http://svpow.files.wordpress.com/2010/02/scaled-composite-re-exported1.png"><img class="size-full wp-image-2532" title="scaled-composite-RE-EXPORTED" alt="" src="http://svpow.files.wordpress.com/2010/02/scaled-composite-re-exported1.png?w=480&#038;h=511" width="480" height="511" /></a><p class="wp-caption-text">My scaling of a complete Amphicoelias fragillimus vertebra: on the left, Cope's figure of the only known vertebra; on the right, Cope's figure of an A. altus dorsal vertebra, scaled to match the preserved parts of the former.  Height of the latter scaled according to the measured height of the former.</p></div>
+      <p>As you can see, when I measured my scaled-to-the-size-of-A.f. <em>Amphicoelias</em> vertebra, it was "only" 2293 mm tall, compared with 2700 mm in Ken's reconstruction.  In other words, mine is only 85% as tall, which translates to 0.85^3 = 61% as massive.  So if this reconstruction is right, the big boy is "only" 1.87 times as long as Diplodocus in linear dimension -- maybe 49 meters long -- and would likely come in well below the 100-tonne threshhold.  Using Matt's (2005) 12-tonne estimate for <em>Diplodocus</em>, we'd get a mere 78.5 tonnes for <em>Amphicoelias fragillimus</em>.  So maybe Matt called that right.</p>
+      <div id="attachment_2529" style="width: 490px" class="wp-caption alignnone"><a href="http://svpow.files.wordpress.com/2010/02/2006-09-28-amnh-dinos-124.jpg"><img class="size-full wp-image-2529" title="2006-09-28 AMNH dinos 124" alt="" src="http://svpow.files.wordpress.com/2010/02/2006-09-28-amnh-dinos-124.jpg?w=480&#038;h=360" width="480" height="360" /></a><p class="wp-caption-text">Amphicoelias altus dorsal vertebra, almost certainly the holotype, in left lateral view, lying on its back.  Photograph by Matt Wedel, from the collections of the AMNH.  I can't believe -- can't BELIEVE -- that I didn't take ten minutes to look at this vertebra when I was in that basement last February.  What a doofus.</p></div>
+      <h2>The Punchline</h2>
+      <p>Folks -- please remember, the punchline is not "<em>Amphicoelias fragillimus</em> only weighed 78.5 tonnes rather than 122.4 tonnes".  The punchline is "when you extrapolate the mass of an extinct animal of uncertain affinities from a 132-year-old figure of a partial bone which has not been seen in more than a century, you need to recognise that the error-bars are massive and anything resembling certainty is way misplaced."</p>
+      <p>Caveat estimator!</p>
+      <h2>References</h2>
+      <div id="_mcePaste">
+       <ul>
+         <li>Carpenter, Kenneth.  2006.  Biggest of the big: A critical re-evalustion of the mega-sauropod <em>Amphicoelias fragillimus</em> Cope, 1878.  pp. 131-137 in J. Foster and S. G. Lucas (eds.), Paleontology and Geology of the Upper Jurassic Morrison Formation.  New Mexico Museum of Natural History and Science Bulletin 36.</li>
+         <li>Cope, Edward Drinker.  1878.  Geology and Palaeontology: a new species of <em>Amphicoelias</em>.  The American Naturalist 12 (8): 563-566.</li>
+         <li>Osborn, Henry Fairfield, and Charles C. Mook.  1921.  <em>Camarasaurus</em>, <em>Amphicoelias</em> and other sauropods of Cope.  Memoirs of the American Museum of Natural History, n.s. 3:247-387, and plates LX-LXXXV.</li>
+       </ul>
+      </div>
+    </div>
+
+    <p style="color:grey">
+      <a href="http://mkws.indexdata.com/">About MKWS</p>
+    </p>
+  </body>
+</html>
index f1d957e..21ab5b2 100644 (file)
@@ -5,34 +5,38 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <meta name="copyright" content="(c) 1999-2013 IndexData ApS, http://indexdata.com" />
     <title>MKWS demo: Automatic search</title>
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
-    <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
     <style type="text/css">
-      td { vertical-align: top }
+      td { vertical-align: top; overflow: word-break }
     </style>
   </head>
   <body>
     <h1>A site about stuff</h1>
-    <table border="1" width="100%">
+    <table border="1" width="100%" style="table-layout:fixed">
+      <col width="50%"/>
+      <col width="50%"/>
       <tr>
-       <td width="50%">
+       <td>
          <h2>Welcome</h2>
          Main site content goes here.
        </td>
-       <td width="50%">
+       <td>
          <h2>News</h2>
 
 
-<div id='mkwsRecords'
+<div class='mkwsRecords'
        autosearch='mike'
        sort='relevance'
        targets='pz:id~josiah.brown.edu:210/innopac|connect.indexdata.com:9000/mit_opencourseware'
 >results will appear here</div>
 
 
-       <div id='mkwsTermlists'/>
        </td>
       </tr>
     </table>
+    <p style="color:grey">
+      <a href="http://mkws.indexdata.com/">About MKWS</p>
+    </p>
   </body>
 </html>
diff --git a/examples/htdocs/auto2.html b/examples/htdocs/auto2.html
new file mode 100644 (file)
index 0000000..bb53d7e
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta name="copyright" content="(c) 1999-2013 IndexData ApS, http://indexdata.com" />
+    <title>MKWS demo: Multiple automatic searches</title>
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
+    <style type="text/css">
+      td { vertical-align: top; overflow: word-break }
+    </style>
+  </head>
+  <body>
+    <h1>Dinosaurs and Museums</h1>
+    <table border="1" width="100%" style="table-layout:fixed">
+      <col width="40%"/>
+      <col width="30%"/>
+      <col width="30%"/>
+      <tr>
+       <td>
+         <h2>Welcome</h2>
+         Main site content goes here.
+       </td>
+       <td>
+         <h2>News</h2>
+<div class='mkwsRecords mkwsTeam_news'
+       autosearch='museum'
+       sort='relevance'
+       targets='pz:id~josiah.brown.edu:210/innopac|connect.indexdata.com:9000/mit_opencourseware'
+>News will appear here</div>
+       </td>
+       <td>
+         <h2>Blog</h2>
+<div class='mkwsRecords mkwsTeam_blog'
+       autosearch='dinosaur'
+       sort='relevance'
+       targets='pz:id~josiah.brown.edu:210/innopac|connect.indexdata.com:9000/mit_opencourseware'
+>Blog entries will appear here</div>
+       </td>
+      </tr>
+    </table>
+    <p style="color:grey">
+      <a href="http://mkws.indexdata.com/">About MKWS</p>
+    </p>
+  </body>
+</html>
diff --git a/examples/htdocs/auto3.html b/examples/htdocs/auto3.html
new file mode 100644 (file)
index 0000000..6f60df0
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta name="copyright" content="(c) 1999-2013 IndexData ApS, http://indexdata.com" />
+    <title>MKWS demo: Multiple automatic searches</title>
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
+    <style type="text/css">
+      h1 { text-align: center; }
+      h1, h2 {
+       font-family: Impact, Hevetica, Arial, Sans Serif;
+      }
+      td {
+        font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
+       vertical-align: top;
+       overflow: word-break;
+       background: #f0f0f0;
+        padding: 0.5em 1em;
+       border-radius: 1em;
+      }
+      a {
+        color: #000060;
+       text-decoration: none;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>Dinosaurs</h1>
+    <table border="0" width="100%" style="table-layout:fixed">
+      <col width="33%"/>
+      <col width="33%"/>
+      <col width="33%"/>
+      <tr>
+       <td style="background: #f8f8e0">
+         <h2>PLOS ONE</h2>
+<div class='mkwsRecords mkwsTeam_plos'
+       autosearch='dinosaur'
+       sort='relevance'
+       targets='pz:id~lui.indexdata.com:8080/solr4/#6265'
+>PLOS ONE articles will appear here</div>
+       </td>
+       <td style="background: #e0f8f0">
+         <h2>Project Gutenberg</h2>
+<div class='mkwsRecords mkwsTeam_gutenberg'
+       autosearch='dinosaur'
+       sort='relevance'
+       targets='pz:id~lui.indexdata.com:8080/solr4/#3552'
+>Free e-books will appear here</div>
+       </td>
+       <td style="background: #e0f0f8">
+         <h2>Library of Congress</h2>
+<div class='mkwsRecords mkwsTeam_blog'
+       autosearch='dinosaur'
+       sort='relevance'
+       targets='pz:id~lui.indexdata.com:8080/solr4/#5802'
+>Library catalog entries will appear here</div>
+       </td>
+      </tr>
+    </table>
+    <p style="color:grey; text-align: right">
+      <a href="http://mkws.indexdata.com/">About MKWS</p>
+    </p>
+  </body>
+</html>
index 4088637..9aac803 100644 (file)
@@ -6,11 +6,11 @@
       var mkws_config = {\r
             //responsive_design_width: 990\r
             //perpage_default: 10,\r
-            service_proxy_auth: "http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=dic&password=dic"\r
+            service_proxy_auth: "//mkws.indexdata.com/service-proxy/?command=auth&action=login&username=dic&password=dic"\r
         };\r
     </script>\r
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />\r
-    <script src="http://mkws.indexdata.com/mkws-complete.js"></script>\r
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />\r
+    <script src="//mkws.indexdata.com/mkws-complete.js"></script>\r
   </head>\r
   <body>\r
 \r
        <p>Modern computers based on <a href="http://en.wikipedia.org/wiki/Integrated_circuit" title="Integrated circuit">integrated circuits</a> are millions to billions of times more capable than the early machines, and occupy a fraction of the space.<sup id="cite_ref-2" class="reference"><a href="http://en.wikipedia.org/wiki/Computer#cite_note-2"><span>[</span>2<span>]</span></a></sup> Simple computers are small enough to fit into <a href="http://en.wikipedia.org/wiki/Mobile_device" title="Mobile device">mobile devices</a>, and <a href="http://en.wikipedia.org/wiki/Mobile_computing" title="Mobile computing">mobile computers</a> can be powered by small <a href="http://en.wikipedia.org/wiki/Battery_(electricity)" title="Battery (electricity)">batteries</a>. Personal computers in their various forms are <a href="http://en.wikipedia.org/wiki/Icon" title="Icon">icons</a> of the <a href="http://en.wikipedia.org/wiki/Information_Age" title="Information Age">Information Age</a> and are what most people think of as “computers.” However, the <a href="http://en.wikipedia.org/wiki/Embedded_system" title="Embedded system">embedded computers</a> found in many devices from <a href="http://en.wikipedia.org/wiki/Digital_audio_player" title="Digital audio player" class="mw-redirect">MP3 players</a> to <a href="http://en.wikipedia.org/wiki/Fighter_aircraft" title="Fighter aircraft">fighter aircraft</a> and from toys to <a href="http://en.wikipedia.org/wiki/Industrial_robot" title="Industrial robot">industrial robots</a> are the most numerous.</p>\r
       </td>\r
       <td width="40%" align="top">\r
-       <div id="mkwsSearch"></div>\r
-       <div id="mkwsRecords"></div>\r
-       <div id="mkwsPager"></div>\r
+       <div class="mkwsSearch"></div>\r
+       <div class="mkwsRecords"></div>\r
       </td>\r
     </tr>\r
   </table>\r
     <script type="text/javascript">\r
-      $("#mkwsSearch").hide();\r
-      $("#mkwsPager").hide();\r
+      $(".mkwsSearch").hide();\r
 \r
       document.onclick = clickfunc;\r
       var selectedtext="";\r
@@ -55,9 +53,9 @@
         //console.log("click: " + sel + "  clicking=" + clicking );\r
         if ( sel != "" && ! clicking ) {\r
           clicking  = true;\r
-          $("input#mkwsQuery").val(sel);\r
+          $("input.mkwsQuery").val(sel);\r
           //console.log("click: Set value " + sel + "  clicking=" + clicking );\r
-          $("input#mkwsButton").trigger("click");\r
+          $("input.mkwsButton").trigger("click");\r
           clicking = false;\r
         }\r
       }\r
diff --git a/examples/htdocs/heikki-motd.html b/examples/htdocs/heikki-motd.html
new file mode 100644 (file)
index 0000000..f46036a
--- /dev/null
@@ -0,0 +1,40 @@
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>Heikkis MOTD test</title>
+
+    <script type="text/javascript">
+      var mkws_config = {
+          //show_perpage: false,
+          //show_sort: false,
+          //perpage_default: 10,
+          //sort_default: "title:1",
+          //debug_level: 1
+      };
+    </script>
+    <script type="text/javascript" src="tools/htdocs/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="tools/htdocs/pz2.js"></script>
+    <script type="text/javascript" src="tools/htdocs/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="tools/htdocs/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="tools/htdocs/mkws.js"></script>
+
+    <style type="text/css">
+      #mkwsTermlists div.facet {
+      float:left;
+      width: 30%;
+      margin: 0.3em;
+      }
+      #mkwsStat {
+      text-align: right;
+      }
+    </style>
+
+  </head>
+  <body>
+    <h1>Heikkis test for the MOTD</h1>
+    <div class="mkwsMOTD">This is the mkwsMOTD div </div>
+    <br/>The MOTD should not be visible above this line.
+    <div class="mkwsSearch"></div>
+    <div class="mkwsResults"></div>
+  </body>
+</html>
diff --git a/examples/htdocs/heikki.html b/examples/htdocs/heikki.html
new file mode 100644 (file)
index 0000000..eda424c
--- /dev/null
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>MKWS demo: Heikki's playground</title>
+    <link rel="stylesheet" type="text/css" href="tools/htdocs/mkws.css" />
+    <script type="text/javascript">
+      var mkws_config = {
+         // pazpar2_url : "/service-proxy/",
+         show_perpage: false,
+         show_sort: false,
+         perpage_default: 10,
+         sort_default: "title:1",
+         // service_proxy_auth : "/service-proxy-auth"
+      };
+    </script>
+    <script type="text/javascript" src="tools/htdocs/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="tools/htdocs/pz2.js"></script>
+    <script type="text/javascript" src="tools/htdocs/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="tools/htdocs/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="tools/htdocs/mkws.js"></script>
+    <style type="text/css">
+      #mkwsTermlists div.facet {
+      float:left;
+      width: 30%;
+      margin: 0.3em;
+      }
+      #mkwsStat {
+      text-align: right;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>Heikki's test</h1>
+    <div class="mkwsMOTD">Welcome to Heikki's little test</div>
+    <table width="100%" border="0">
+      <tr>
+        <td>
+         <div class="mkwsSwitch"></div>
+          <div class="mkwsLang"></div>
+          <div class="mkwsSearch"></div>
+        </td>
+      </tr>
+      <tr>
+        <td>
+          <div style="height:500px; overflow: auto">
+            <div class="mkwsPager"></div>
+            <div class="mkwsNavi"></div>
+            <div class="mkwsRecords"></div>
+            <div class="mkwsTargets"></div>
+            <div class="mkwsRanking"></div>
+          </div>
+        </td>
+      </tr>
+      <tr>
+        <td>
+          <div style="height:300px; overflow: hidden">
+            <div class="mkwsTermlists"></div>
+          </div>
+        </td>
+      </tr>
+      <tr>
+        <td>
+          <div class="mkwsStat"></div>
+        </td>
+      </tr>
+    </table>
+  </body>
+</html>
diff --git a/examples/htdocs/images.html b/examples/htdocs/images.html
new file mode 100644 (file)
index 0000000..ff92c1e
--- /dev/null
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>MKWS demo: Images</title>
+    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+    <script class="mkwsTemplate_Summary" type="text/x-handlebars-template">
+      <a href="#" id="{{_id}}" onclick="{{_onclick}}">
+        {{#each md-thumburl}}
+         <img src="{{this}}" alt="{{../md-title}}"/>
+        {{/each}}
+       <br/>
+       {{md-title}}
+      {{#if md-title-remainder}}
+        <span>{{md-title-remainder}}</span>
+      {{/if}}
+      {{#if md-title-responsibility}}
+       <span><i>{{md-title-responsibility}}</i></span>
+      {{/if}}
+      </a>
+    </script>
+  </head>
+  <body>
+    <div class='mkwsRecords' autosearch='!param!q'
+       targets='pz:id=lui.indexdata.com:8080/solr4/#5853'
+       >results will appear here</div>
+  </body>
+</html>
index a943c30..981d83c 100644 (file)
@@ -2,15 +2,16 @@
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo client</title>
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
-    <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
   </head>
   <body>
-    <div id="mkwsSwitch"></div>
-    <div id="mkwsLang"></div>
-    <div id="mkwsSearch"></div>
-    <div id="mkwsResults"></div>
-    <div id="mkwsTargets"></div>
-    <div id="mkwsStat"></div>
+    <div class="mkwsSwitch"></div>
+    <div class="mkwsLang"></div>
+    <div class="mkwsSearch"></div>
+    <div class="mkwsResults"></div>
+    <div class="mkwsTargets"></div>
+    <div class="mkwsStat"></div>
+    <div class="mkwsBuilder"></div>
   </body>
 </html>
index 6a06b22..846352d 100644 (file)
     <script type="text/javascript" src="http://mkws-origin/mkws-complete.js"></script>
   </head>
   <body>
-    <div id="mkwsSwitch"></div>
-    <div id="mkwsLang"></div>
-    <div id="mkwsSearch"></div>
-    <div id="mkwsResults"></div>
-    <div id="mkwsTargets"></div>
-    <div id="mkwsStat"></div>
+    <div class="mkwsSwitch"></div>
+    <div class="mkwsLang"></div>
+    <div class="mkwsSearch"></div>
+    <div class="mkwsResults"></div>
+    <div class="mkwsTargets"></div>
+    <div class="mkwsStat"></div>
   </body>
 </html>
diff --git a/examples/htdocs/jasmine-local-popup.html b/examples/htdocs/jasmine-local-popup.html
new file mode 100644 (file)
index 0000000..739b373
--- /dev/null
@@ -0,0 +1,75 @@
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>MKWS demo: jQuery popup plugin with jasmine test framework</title>
+
+    <link rel="stylesheet" type="text/css" href="//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
+    <link rel="stylesheet" type="text/css" href="tools/htdocs/mkws.css" />
+
+    <script type="text/javascript" src="src/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="//code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
+    <script type="text/javascript" src="src/pz2.js"></script>
+    <script type="text/javascript" src="src/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="src/handlebars-v1.1.2.js"></script>
+    <!-- <script type="text/javascript" src="src/mkws.js"></script> -->
+    <script type="text/javascript" src="src/mkws-handlebars.js"></script>
+    <script type="text/javascript" src="src/mkws-core.js"></script>
+    <script type="text/javascript" src="src/mkws-team.js"></script>
+    <script type="text/javascript" src="src/mkws-filter.js"></script>
+    <script type="text/javascript" src="src/mkws-widgets.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-termlists.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-authname.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-log.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-record.js"></script>
+
+    <script type="text/javascript" src="src/mkws-jquery.js"></script>
+
+    <link rel="shortcut icon" type="image/png" href="jasmine/lib/jasmine-1.3.1/jasmine_favicon.png">
+    <link rel="stylesheet" type="text/css" href="jasmine/lib/jasmine-1.3.1/jasmine.css">
+    <script type="text/javascript" src="jasmine/lib/jasmine-1.3.1/jasmine.js"></script>
+    <script type="text/javascript" src="jasmine/lib/jasmine-1.3.1/jasmine-html.js"></script>
+
+    <script type="text/javascript" src="test/spec/true.spec.js"></script>
+    <script type="text/javascript" src="test/spec/mkws-config.js"></script>
+    <script type="text/javascript" src="test/spec/mkws-pazpar2.js"></script>
+
+    <script type="text/javascript" src="test/js/mkws-jasmine-run.js"></script>
+    <script type="text/javascript"> mkws_jasmine_init(500); </script>
+  </head>
+
+  <body>
+    <script type="text/javascript">
+
+    // using a local service proxy !!!
+    var mkws_config = {
+      perpage_default: 10,
+      pazpar2_url:          "/service-proxy/",
+      service_proxy_auth:   "/service-proxy-testauth"
+    };
+
+    var jasmine_config = {
+      search_query: "netbsd",
+      expected_hits: 10,
+      // active_clients: 17,
+      check_motd: false,
+      show_record_url: true
+    };
+
+    jQuery.pazpar2({
+        "layout": "popup",               /* "table" [default], "div", "popup" */
+        "width": 990,                    /* popup width, should be at least 800 */
+        "height": 760                    /* popup height, should be at least 600 */
+    });
+    </script>
+
+    <pre>
+An embryonic MasterKey Widget Set
+=================================
+
+This directory contains an embryonic MasterKey Widget Set, based
+initially on "jsdemo" though now far removed from those beginnnings.
+[...]
+    </pre>
+
+  </body>
+</html>
index ee22551..91e770c 100644 (file)
@@ -3,36 +3,54 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo: jQuery popup plugin with jasmine test framework</title>
 
-    <link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
-
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
-    <script type="text/javascript" src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/pazpar2/js/pz2.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/jquery.json-2.4.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/handlebars-v1.1.2.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/mkws.js"></script>
-
-    <link rel="shortcut icon" type="image/png" href="../../../jasmine/lib/jasmine-1.3.1/jasmine_favicon.png">
-    <link rel="stylesheet" type="text/css" href="../../../jasmine/lib/jasmine-1.3.1/jasmine.css">
-    <script type="text/javascript" src="../../../jasmine/lib/jasmine-1.3.1/jasmine.js"></script>
-    <script type="text/javascript" src="../../../jasmine/lib/jasmine-1.3.1/jasmine-html.js"></script>
-
-    <script type="text/javascript" src="../../test/spec/true.spec.js"></script>
-    <script type="text/javascript" src="../../test/spec/mkws-config.js"></script>
-    <script type="text/javascript" src="../../test/spec/mkws-pazpar2.js"></script>
-
-    <script type="text/javascript" src="../../test/js/mkws-jasmine-run.js"></script>
+    <link rel="stylesheet" type="text/css" href="//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
+    <link rel="stylesheet" type="text/css" href="tools/htdocs/mkws.css" />
+
+    <script type="text/javascript" src="//code.jquery.com/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="//code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
+    <script type="text/javascript" src="//git.indexdata.com/?p=pazpar2.git;a=blob_plain;f=js/pz2.js;hb=HEAD"></script>
+    <script type="text/javascript" src="//jquery-json.googlecode.com/files/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="//builds.handlebarsjs.com.s3.amazonaws.com/handlebars-v1.1.2.js"></script>
+
+    <!-- <script type="text/javascript" src="src/mkws.js"></script> -->
+    <script type="text/javascript" src="src/mkws-handlebars.js"></script>
+    <script type="text/javascript" src="src/mkws-core.js"></script>
+    <script type="text/javascript" src="src/mkws-team.js"></script>
+    <script type="text/javascript" src="src/mkws-filter.js"></script>
+    <script type="text/javascript" src="src/mkws-widgets.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-termlists.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-authname.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-log.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-record.js"></script>
+    <script type="text/javascript" src="src/mkws-jquery.js"></script>
+
+    <link rel="shortcut icon" type="image/png" href="jasmine/lib/jasmine-1.3.1/jasmine_favicon.png">
+    <link rel="stylesheet" type="text/css" href="jasmine/lib/jasmine-1.3.1/jasmine.css">
+    <script type="text/javascript" src="jasmine/lib/jasmine-1.3.1/jasmine.js"></script>
+    <script type="text/javascript" src="jasmine/lib/jasmine-1.3.1/jasmine-html.js"></script>
+
+    <script type="text/javascript" src="test/spec/true.spec.js"></script>
+    <script type="text/javascript" src="test/spec/mkws-config.js"></script>
+    <script type="text/javascript" src="test/spec/mkws-pazpar2.js"></script>
+
+    <script type="text/javascript" src="test/js/mkws-jasmine-run.js"></script>
     <script type="text/javascript"> mkws_jasmine_init(500); </script>
   </head>
 
   <body>
     <script type="text/javascript">
+    var mkws_config = {
+      perpage_default: 10,
+      // active_clients: 17,
+      pazpar2_url:          "//mkws.indexdata.com/service-proxy/",
+      service_proxy_auth:   "//mkws.indexdata.com/service-proxy-testauth"
+    };
+
     jQuery.pazpar2({
         "layout": "popup",               /* "table" [default], "div", "popup" */
-        "id_button": "input#mkwsButton", /* submit button id in search field */
-        "id_popup": "#mkwsPopup",        /* internal id of popup window */
-        "width": 880,                    /* popup width, should be at least 800 */
+        "id_button": "input.mkwsButton", /* submit button id in search field */
+        "id_popup": ".mkwsPopup",        /* internal id of popup window */
+        "width": 990,                    /* popup width, should be at least 800 */
         "height": 760                    /* popup height, should be at least 600 */
     });
     </script>
@@ -45,5 +63,6 @@ This directory contains an embryonic MasterKey Widget Set, based
 initially on "jsdemo" though now far removed from those beginnnings.
 [...]
     </pre>
+     <div id="testMOTD"><div class="mkwsMOTD">This is the mkwsMOTD div</div></div>
   </body>
 </html>
index 0fda53f..60aa55f 100644 (file)
@@ -2,44 +2,49 @@
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo jasmine test framework</title>
-    <link rel="stylesheet" type="text/css" href="../../tools/htdocs/mkws.css" />
     <script type="text/javascript">
       var mkws_config = {
-         jasmine: { "show_record_url": false },
          use_service_proxy: false,
+         disable_facet_authors_search: true, // does not work with raw pazpar2
          pazpar2_url : "/pazpar2/",
          perpage_default: 10
       };
+
+      var jasmine_config = {
+       "show_record_url": false // URLs not configured for pp2
+      };
     </script>
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.4.4.min.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/pazpar2/js/pz2.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/handlebars-v1.1.2.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/jquery.json-2.4.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/mkws.js"></script>
+    <script type="text/javascript" src="//code.jquery.com/jquery-1.7.2.min.js"></script>
+    <script type="text/javascript" src="src/pz2.js"></script>
+    <script type="text/javascript" src="src/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="src/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="src/mkws.js"></script>
+
+    <link rel="stylesheet" type="text/css" href="tools/htdocs/mkws.css" />
     <style type="text/css">
-      #mkwsTermlists div.facet {
+      .mkwsTermlists div.facet {
       float:left;
       width: 30%;
       margin: 0.3em;
       }
-      #mkwsStat {
+      .mkwsStat {
       text-align: right;
       }
     </style>
 
   <!-- SECTION jasmine -->
-    <link rel="shortcut icon" type="image/png" href="../../../jasmine/lib/jasmine-1.3.1/jasmine_favicon.png">
-    <link rel="stylesheet" type="text/css" href="../../../jasmine/lib/jasmine-1.3.1/jasmine.css">
-    <script type="text/javascript" src="../../../jasmine/lib/jasmine-1.3.1/jasmine.js"></script>
-    <script type="text/javascript" src="../../../jasmine/lib/jasmine-1.3.1/jasmine-html.js"></script>
+    <link rel="shortcut icon" type="image/png" href="jasmine/lib/jasmine-1.3.1/jasmine_favicon.png">
+    <link rel="stylesheet" type="text/css" href="jasmine/lib/jasmine-1.3.1/jasmine.css">
+    <script type="text/javascript" src="jasmine/lib/jasmine-1.3.1/jasmine.js"></script>
+    <script type="text/javascript" src="jasmine/lib/jasmine-1.3.1/jasmine-html.js"></script>
 
     <!-- include MKWS spec files ... -->
-    <script type="text/javascript" src="../../test/spec/true.spec.js"></script>
-    <script type="text/javascript" src="../../test/spec/mkws-config.js"></script>
-    <script type="text/javascript" src="../../test/spec/mkws-pazpar2.js"></script>
+    <script type="text/javascript" src="test/spec/true.spec.js"></script>
+    <script type="text/javascript" src="test/spec/mkws-config.js"></script>
+    <script type="text/javascript" src="test/spec/mkws-pazpar2.js"></script>
 
     <!-- init and run jasmine -->
-    <script type="text/javascript" src="../../test/js/mkws-jasmine-run.js"></script>
+    <script type="text/javascript" src="test/js/mkws-jasmine-run.js"></script>
     <script type="text/javascript">
       mkws_jasmine_init(500);
     </script>
     <table width="100%" border="0">
       <tr>
         <td>
-          <div id="mkwsSwitch"></div>
-          <div id="mkwsLang"></div>
-          <div id="mkwsSearch"></div>
+          <div class="mkwsSwitch"></div>
+          <div class="mkwsLang"></div>
+          <div class="mkwsSearch"></div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:500px; overflow: auto">
-            <div id="mkwsPager"></div>
-            <div id="mkwsNavi"></div>
-            <div id="mkwsRecords"></div>
-            <div id="mkwsTargets"></div>
-            <div id="mkwsRanking"></div>
+            <div class="mkwsPager"></div>
+            <div class="mkwsNavi"></div>
+            <div class="mkwsRecords"></div>
+            <div class="mkwsTargets"></div>
+            <div class="mkwsRanking"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:300px; overflow: hidden">
-            <div id="mkwsTermlists"></div>
+            <div class="mkwsTermlists"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
-          <div id="mkwsStat"></div>
+          <div class="mkwsStat"></div>
         </td>
       </tr>
     </table>
 
+    <div id="testMOTD"><div class="mkwsMOTD">This is the mkwsMOTD div</div></div>
   </body>
 </html>
index f9f034b..66183f1 100644 (file)
@@ -2,45 +2,43 @@
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo jasmine test framework</title>
-    <link rel="stylesheet" type="text/css" href="../../tools/htdocs/mkws.css" />
+    <link rel="stylesheet" type="text/css" href="tools/htdocs/mkws.css" />
     <script type="text/javascript">
       var mkws_config = {
+         pazpar2_url : "//mkws.indexdata.com/service-proxy/",
+         service_proxy_auth: "//mkws.indexdata.com/service-proxy-testauth",
          perpage_default: 10
-         /*
-         pazpar2_url : "/service-proxy/",
-         service_proxy_auth : "/service-proxy-auth/",
-         */
       };
     </script>
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.4.4.min.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/pazpar2/js/pz2.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/handlebars-v1.1.2.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/jquery.json-2.4.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/mkws.js"></script>
+    <script type="text/javascript" src="//code.jquery.com/jquery-1.7.2.min.js"></script> 
+    <script type="text/javascript" src="src/pz2.js"></script>
+    <script type="text/javascript" src="src/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="src/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="src/mkws.js"></script>
     <style type="text/css">
-      #mkwsTermlists div.facet {
+      .mkwsTermlists div.facet {
       float:left;
       width: 30%;
       margin: 0.3em;
       }
-      #mkwsStat {
+      .mkwsStat {
       text-align: right;
       }
     </style>
 
   <!-- SECTION jasmine -->
-    <link rel="shortcut icon" type="image/png" href="../../../jasmine/lib/jasmine-1.3.1/jasmine_favicon.png">
-    <link rel="stylesheet" type="text/css" href="../../../jasmine/lib/jasmine-1.3.1/jasmine.css">
-    <script type="text/javascript" src="../../../jasmine/lib/jasmine-1.3.1/jasmine.js"></script>
-    <script type="text/javascript" src="../../../jasmine/lib/jasmine-1.3.1/jasmine-html.js"></script>
+    <link rel="shortcut icon" type="image/png" href="jasmine/lib/jasmine-1.3.1/jasmine_favicon.png">
+    <link rel="stylesheet" type="text/css" href="jasmine/lib/jasmine-1.3.1/jasmine.css">
+    <script type="text/javascript" src="jasmine/lib/jasmine-1.3.1/jasmine.js"></script>
+    <script type="text/javascript" src="jasmine/lib/jasmine-1.3.1/jasmine-html.js"></script>
 
     <!-- include MKWS spec files ... -->
-    <script type="text/javascript" src="../../test/spec/true.spec.js"></script>
-    <script type="text/javascript" src="../../test/spec/mkws-config.js"></script>
-    <script type="text/javascript" src="../../test/spec/mkws-pazpar2.js"></script>
+    <script type="text/javascript" src="test/spec/true.spec.js"></script>
+    <script type="text/javascript" src="test/spec/mkws-config.js"></script>
+    <script type="text/javascript" src="test/spec/mkws-pazpar2.js"></script>
 
     <!-- init and run jasmine -->
-    <script type="text/javascript" src="../../test/js/mkws-jasmine-run.js"></script>
+    <script type="text/javascript" src="test/js/mkws-jasmine-run.js"></script>
     <script type="text/javascript">
       mkws_jasmine_init(500);
     </script>
 
   </head>
   <body>
+    <div id="testMOTD"><div class="mkwsMOTD">This is the mkwsMOTD div</div></div>
     <table width="100%" border="0">
       <tr>
         <td>
-          <div id="mkwsSwitch"></div>
-          <div id="mkwsLang"></div>
-          <div id="mkwsSearch"></div>
+          <div class="mkwsSwitch"></div>
+          <div class="mkwsLang"></div>
+          <div class="mkwsSearch"></div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:500px; overflow: auto">
-            <div id="mkwsPager"></div>
-            <div id="mkwsNavi"></div>
-            <div id="mkwsRecords"></div>
-            <div id="mkwsTargets"></div>
-            <div id="mkwsRanking"></div>
+            <div class="mkwsPager"></div>
+            <div class="mkwsNavi"></div>
+            <div class="mkwsRecords"></div>
+            <div class="mkwsTargets"></div>
+            <div class="mkwsRanking"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:300px; overflow: hidden">
-            <div id="mkwsTermlists"></div>
+            <div class="mkwsTermlists"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
-          <div id="mkwsStat"></div>
+          <div class="mkwsStat"></div>
         </td>
       </tr>
     </table>
index e26bf8b..6ebb30f 100644 (file)
@@ -2,18 +2,18 @@
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo: jQuery plugin</title>
-    <link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
+    <link rel="stylesheet" type="text/css" href="//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
 
-    <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
-    <script src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
+    <script src="//code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
   </head>
   <body>
     <script type="text/javascript">
     jQuery.pazpar2({
         "layout": "popup",               /* "table" [default], "div", "popup" */
-        "id_button": "input#mkwsButton", /* submit button id in search field */
-        "id_popup": "#mkwsPopup",        /* internal id of popup window */
+        "id_button": "input.mkwsButton", /* submit button id in search field */
+        "id_popup": ".mkwsPopup",        /* internal id of popup window */
         "width": 880,                    /* popup width, should be at least 800 */ 
         "height": 760                    /* popup height, should be at least 600 */
     });
index 7c003df..3ac6968 100644 (file)
@@ -5,7 +5,7 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <meta name="copyright" content="(c) 1999-2013 IndexData ApS, http://indexdata.com" />
     <title>MKWS demo: full configuration</title>
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
     <script type="text/javascript">
        var mkws_config = {
                lang: "da",
                }
        };
     </script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
   </head>
   <body>
-    <div id="mkwsSwitch"></div>
-    <div id="mkwsLang"></div>
-    <div id="mkwsSearch"></div>
-    <div id="mkwsResults"></div>
-    <div id="mkwsTargets"></div>
-    <div id="mkwsMOTD">
+    <div class="mkwsSwitch"></div>
+    <div class="mkwsLang"></div>
+    <div class="mkwsSearch"></div>
+    <div class="mkwsResults"></div>
+    <div class="mkwsTargets"></div>
+    <div class="mkwsMOTD">
       <p>
        <b>Welcome to the MasterKey Widget Set demo.</b>
       </p>
@@ -79,7 +79,7 @@
       </p>
     </div>
     <div id="footer">
-      <div id="mkwsStat"></div>
+      <div class="mkwsStat"></div>
       <span>Powered by MKWS &copy; 2013 <a target="_new" href="http://www.indexdata.com">Index Data</a></span>
     </div>
   </body>
diff --git a/examples/htdocs/local-auto.html b/examples/htdocs/local-auto.html
new file mode 100644 (file)
index 0000000..3c5f5b0
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta name="copyright" content="(c) 1999-2013 IndexData ApS, http://indexdata.com" />
+    <title>MKWS demo: Mike's playground</title>
+    <link rel="stylesheet" type="text/css" href="http://x.mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="http://x.mkws.indexdata.com/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="http://x.mkws.indexdata.com/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="http://x.mkws.indexdata.com/pazpar2/js/pz2.js"></script>
+    <script type="text/javascript" src="http://x.mkws.indexdata.com/mkws.js"></script>
+    <style type="text/css">
+      td { vertical-align: top }
+    </style>
+  </head>
+  <body>
+    <h1>A site about stuff</h1>
+    <table border="1" width="100%">
+      <tr>
+       <td width="40%">
+         <h2>Welcome</h2>
+         Main site content goes here.
+       </td>
+       <td width="30%">
+         <h2>News</h2>
+<div class='mkwsRecords mkwsTeam_news'
+       autosearch='museum'
+       sort='relevance'
+       targets='pz:id~josiah.brown.edu:210/innopac|connect.indexdata.com:9000/mit_opencourseware'
+>News will appear here</div>
+       <div class='mkwsTermlists mkwsTeam_news'/>
+       </td>
+       <td width="30%">
+         <h2>Blog</h2>
+<div class='mkwsRecords mkwsTeam_blog'
+       autosearch='dinosaur'
+       sort='relevance'
+       targets='pz:id~josiah.brown.edu:210/innopac|connect.indexdata.com:9000/mit_opencourseware'
+>Blog entries will appear here</div>
+       <div class='mkwsTermlists mkwsTeam_blog'/>
+       </td>
+      </tr>
+    </table>
+  </body>
+</html>
diff --git a/examples/htdocs/local-auto3.html b/examples/htdocs/local-auto3.html
new file mode 100644 (file)
index 0000000..cb496db
--- /dev/null
@@ -0,0 +1,69 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>MKWS demo: Mike's playground</title>
+    <link rel="stylesheet" type="text/css" href="http://x.mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="http://x.mkws.indexdata.com/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="http://x.mkws.indexdata.com/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="http://x.mkws.indexdata.com/pazpar2/js/pz2.js"></script>
+    <script type="text/javascript" src="http://x.mkws.indexdata.com/mkws.js"></script>
+    <style type="text/css">
+      h1 { text-align: center; }
+      h1, h2 {
+       font-family: Impact, Hevetica, Arial, Sans Serif;
+      }
+      td {
+        font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
+       vertical-align: top;
+       overflow: word-break;
+       background: #f0f0f0;
+        padding: 0.5em 1em;
+       border-radius: 1em;
+      }
+      a {
+        color: #000060;
+       text-decoration: none;
+      }
+    </style>
+  </head>
+  <body>
+    <h1>Dinosaurs</h1>
+    <table border="0" width="100%" style="table-layout:fixed">
+      <col width="33%"/>
+      <col width="33%"/>
+      <col width="33%"/>
+      <tr>
+       <td style="background: #f8f8e0">
+         <h2>PLOS ONE</h2>
+<div class='mkwsRecords mkwsTeam_plos'
+       autosearch='dinosaur'
+       sort='relevance'
+       targets='pz:id~lui.indexdata.com:8080/solr4/#6265'
+>PLOS ONE articles will appear here</div>
+       </td>
+       <td style="background: #e0f8f0">
+         <h2>Project Gutenberg</h2>
+<div class='mkwsRecords mkwsTeam_gutenberg'
+       autosearch='dinosaur'
+       sort='relevance'
+       targets='pz:id~lui.indexdata.com:8080/solr4/#3552'
+>Free e-books will appear here</div>
+       </td>
+       <td style="background: #e0f0f8">
+         <h2>Library of Congress</h2>
+<div class='mkwsRecords mkwsTeam_blog'
+       autosearch='dinosaur'
+       sort='relevance'
+       targets='pz:id~lui.indexdata.com:8080/solr4/#5802'
+>Library catalog entries will appear here</div>
+       </td>
+      </tr>
+    </table>
+    <p style="color:grey; text-align: right">
+      <a href="http://mkws.indexdata.com/">About MKWS</p>
+    </p>
+  </body>
+</html>
index 6c7e811..239a5e8 100644 (file)
@@ -2,7 +2,7 @@
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo client</title>
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
     <script type="text/javascript">
       var mkws_config = {
         service_proxy_auth: "http://example.indexdata.com/service-proxy-auth/",
       // See also the Apache2 RewriteRule needed to make this work:
       // http://example.indexdata.com/apache-config.txt
     </script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
   </head>
   <body>
-    <div id="mkwsSwitch"></div>
-    <div id="mkwsLang"></div>
-    <div id="mkwsSearch"></div>
-    <div id="mkwsResults"></div>
-    <div id="mkwsTargets"></div>
-    <div id="mkwsStat"></div>
+    <div class="mkwsSwitch"></div>
+    <div class="mkwsLang"></div>
+    <div class="mkwsSearch"></div>
+    <div class="mkwsResults"></div>
+    <div class="mkwsTargets"></div>
+    <div class="mkwsStat"></div>
   </body>
 </html>
diff --git a/examples/htdocs/lolcat.html b/examples/htdocs/lolcat.html
new file mode 100644 (file)
index 0000000..89c6790
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>MKWS demo: LOLcat demo</title>
+    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript">mkws_config = { perpage_default: 1 }</script>
+    <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+    <script class="mkwsTemplate_Summary" type="text/x-handlebars-template">
+      <a href="#" id="{{_id}}" onclick="{{_onclick}}">
+        {{#first md-thumburl}}
+         <img src="{{this}}" alt="{{../md-title}}"/>
+        {{/first}}
+       <br/>
+      </a>
+    </script>
+  </head>
+  <body>
+    <div class='mkwsRecords' autosearch='cat' sort='date:0'
+       targets='pz:id=localhost:9003/flickr_api'
+       >kitteh will appear here</div>
+  </body>
+</html>
index d2e8ed7..f8b417f 100644 (file)
@@ -2,15 +2,15 @@
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo: low-level subcomponents</title>
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
-    <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
     <style type="text/css">
-      #mkwsTermlists div.facet {
+      .mkwsTermlists div.facet {
       float:left;
       width: 30%;
       margin: 0.3em;
       }
-      #mkwsStat {
+      .mkwsStat {
       text-align: right;
       }
     </style>
     <table width="100%" border="0">
       <tr>
         <td>
-          <div id="mkwsSwitch"></div>
-          <div id="mkwsLang"></div>
-          <div id="mkwsSearch"></div>
+          <div class="mkwsSwitch"></div>
+          <div class="mkwsLang"></div>
+          <div class="mkwsSearch"></div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:500px; overflow: auto">
-            <div id="mkwsPager"></div>
-            <div id="mkwsNavi"></div>
-            <div id="mkwsRecords"></div>
-            <div id="mkwsTargets"></div>
-            <div id="mkwsRanking"></div>
+            <div class="mkwsPager"></div>
+            <div class="mkwsNavi"></div>
+            <div class="mkwsRecords"></div>
+            <div class="mkwsTargets"></div>
+            <div class="mkwsRanking"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:300px; overflow: hidden">
-            <div id="mkwsTermlists"></div>
+            <div class="mkwsTermlists"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
-          <div id="mkwsStat"></div>
+          <div class="mkwsStat"></div>
         </td>
       </tr>
     </table>
index 47f5ed3..b887ecf 100644 (file)
@@ -1,27 +1,37 @@
-<html>
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-    <title>MKWS demo client</title>
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
+    <title>MKWS demo: Mike's playground</title>
+    <link rel="stylesheet" type="text/css" href="http://x.mkws.indexdata.com/mkws.css" />
+    <style type="text/css">
+      .mkwsAuthname {
+        font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
+        float: left;
+        padding-left: 1em;
+        padding-top: 0.4em;
+        color: #132194;
+        font-weight: bold;
+      }
+    </style>
     <script type="text/javascript">
-      var mkws_config = {
-        service_proxy_auth: "http://x.example.indexdata.com/service-proxy-auth/",
-      };
-      // See also the Apache2 RewriteRule needed to make this work:
-      // http://example.indexdata.com/apache-config.txt
+      var mkws_config = { service_proxy_auth: "//mkws.indexdata.com/service-proxy-testauth" };
+      var ourQuery = 'dinosaur';
     </script>
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
     <script type="text/javascript" src="http://x.mkws.indexdata.com/jquery.json-2.4.js"></script>
     <script type="text/javascript" src="http://x.mkws.indexdata.com/handlebars-v1.1.2.js"></script>
     <script type="text/javascript" src="http://x.mkws.indexdata.com/pazpar2/js/pz2.js"></script>
     <script type="text/javascript" src="http://x.mkws.indexdata.com/mkws.js"></script>
   </head>
   <body>
-    <div id="mkwsSwitch"></div>
-    <div id="mkwsLang"></div>
-    <div id="mkwsSearch"></div>
-    <div id="mkwsResults"></div>
-    <div id="mkwsTargets"></div>
-    <div id="mkwsStat"></div>
+    <div class="mkwsSwitch"></div>
+    <div class="mkwsLang"></div>
+    <div class="mkwsSearch"></div>
+    <div class="mkwsResults"></div>
+    <div class="mkwsTargets"></div>
+    <div class="mkwsStat"></div>
+    <div class="mkwsBuilder"></div>
   </body>
 </html>
diff --git a/examples/htdocs/mike2.html b/examples/htdocs/mike2.html
deleted file mode 100644 (file)
index 565c51e..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml">
-  <head>
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-    <meta name="copyright" content="(c) 1999-2013 IndexData ApS, http://indexdata.com" />
-    <title>MKWS demo: Mike's playground</title>
-    <link rel="stylesheet" type="text/css" href="http://x.mkws.indexdata.com/mkws.css" />
-    <script type="text/javascript">
-       var mkws_config = {
-       sort_default: "title",
-       };
-    </script>
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
-    <script type="text/javascript" src="http://x.mkws.indexdata.com/jquery.json-2.4.js"></script>
-    <script type="text/javascript" src="http://x.mkws.indexdata.com/handlebars-v1.1.2.js"></script>
-    <script type="text/javascript" src="http://x.mkws.indexdata.com/pazpar2/js/pz2.js"></script>
-    <script type="text/javascript" src="http://x.mkws.indexdata.com/mkws.js"></script>
-  </head>
-  <body>
-    <div id="mkwsSwitch"></div>
-    <div id="mkwsLang"></div>
-    <div id="mkwsSearch"></div>
-    <div id="mkwsResults"></div>
-    <div id="mkwsTargets"></div>
-    <div id="mkwsStat"></div>
-  </body>
-</html>
index 318a0cb..b95069a 100644 (file)
@@ -1,3 +1,3 @@
-<script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
-<div id="mkwsSearch"></div>
-<div id="mkwsResults"></div>
+<script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
+<div class="mkwsSearch"></div>
+<div class="mkwsResults"></div>
diff --git a/examples/htdocs/mkws-widget-reference.css b/examples/htdocs/mkws-widget-reference.css
new file mode 100644 (file)
index 0000000..88d1fb2
--- /dev/null
@@ -0,0 +1,24 @@
+.mkwsReference {
+    max-width: 40em;
+    font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
+    background: #f0f0e0;
+    padding: 0.5em 1em;
+    border: 1px solid #c0c0a0;
+    border-radius: 1em;
+    -moz-border-radius: 1em;
+    -webkit-border-radius: 1em;
+}
+
+.mkwsReference h1 a {
+    color: #806020;
+}
+
+.mkwsReference a {
+    text-decoration: none;
+}
+
+.mkwsReference img {
+    float:right;
+    margin: 0 0 1em 2em;
+    border: 0.5em solid white;
+}
diff --git a/examples/htdocs/mkws-widget-reference.js b/examples/htdocs/mkws-widget-reference.js
new file mode 100644 (file)
index 0000000..fc2b920
--- /dev/null
@@ -0,0 +1,19 @@
+mkws.registerWidgetType('Reference', function() {
+    mkws.promotionFunction('Record').call(this);
+    if (!this.config.target) this.config.target = 'wikimedia_wikipedia_single_result';
+    if (!this.config.template) this.config.template = 'Reference';
+
+    this.team.registerTemplate('Reference', '\
+  <img src="{{md-thumburl}}" alt="{{md-title}}">\
+  <h1><a href="{{md-electronic-url}}">{{md-title}}</a></h1>\
+{{#if md-title-remainder}}\
+  <b>{{md-title-remainder}}</b>\
+{{/if}}\
+{{#if md-title-responsibility}}\
+  <i>{{md-title-responsibility}}</i>\
+{{/if}}\
+  <p>\
+    {{md-description}}\
+  </p>\
+');
+});
index c9dcd87..5d78b2d 100644 (file)
@@ -5,32 +5,17 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <meta name="copyright" content="(c) 1999-2013 IndexData ApS, http://indexdata.com" />
     <title>MKWS demo: mobile-screen resizing</title>
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
     <script type="text/javascript">
-       var mkws_config = {
-               lang: "da",
-               debug_level: 1,
-               use_service_proxy: true,
-               show_lang: true,
-               lang_options: ["da", "en"],
-               sort_default: "relevance",
-               query_width: 50,
-               facets: ["authors", "sources", "subjects"],
-               responsive_design_width: 990,
-               perpage_default: 20
-        };
+      var mkws_config = { responsive_design_width: 990 };
     </script>
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.4.4.min.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/pazpar2/js/pz2.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/handlebars-v1.1.2.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/jquery.json-2.4.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/mkws.js"></script>
+    <script src="//mkws.indexdata.com/mkws-complete.js"></script>
   </head>
   <body>
-    <div id="mkwsLang"></div>
-    <div id="mkwsSearch"></div>
-    <div id="mkwsResults"></div>
-    <div id="mkwsTargets"></div>
-    <div id="mkwsStat"></div>
+    <div class="mkwsLang"></div>
+    <div class="mkwsSearch"></div>
+    <div class="mkwsResults"></div>
+    <div class="mkwsTargets"></div>
+    <div class="mkwsStat"></div>
   </body>
 </html>
index 29aff8d..60529bb 100644 (file)
@@ -3,8 +3,8 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo: popup search box</title>
 
-    <link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
-    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
+    <link rel="stylesheet" type="text/css" href="//code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
 
     <script type="text/javascript">
       mkws_config = {
      };
     </script>
 
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
-    <script type="text/javascript" src="http://jquery-json.googlecode.com/files/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="//code.jquery.com/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="//jquery-json.googlecode.com/files/jquery.json-2.4.js"></script>
     <!-- legacy libs for testing
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.4.4.min.js"></script>
-    <script src="http://code.jquery.com/ui/1.8.0/jquery-ui.min.js"></script>
+    <script type="text/javascript" src="//code.jquery.com/jquery-1.6.4.min.js"></script>
+    <script src="//code.jquery.com/ui/1.8.0/jquery-ui.min.js"></script>
     -->
 
-    <script type="text/javascript" src="http://mkws.indexdata.com/pazpar2/js/pz2.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/handlebars-v1.1.2.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/mkws.js"></script>
-
+    <script type="text/javascript" src="//mkws.indexdata.com/pazpar2/js/pz2.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws.js"></script>
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-jquery.js"></script>
   </head>
   <body>
     <script type="text/javascript">
@@ -76,6 +76,6 @@ whatever makes up the application itself:
 
 [...]
     </pre>
-    <script src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
+    <script src="//code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
   </body>
 </html>
diff --git a/examples/htdocs/reference-universe.html b/examples/htdocs/reference-universe.html
new file mode 100644 (file)
index 0000000..438407b
--- /dev/null
@@ -0,0 +1,15 @@
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>MKWS demo: Reference Universe widget</title>
+    <link rel="stylesheet" type="text/css" href="mkws-widget-reference.css" />
+    <script type="text/javascript">
+      var mkws_config = { service_proxy_auth: "//mkws.indexdata.com/service-proxy-testauth" };
+    </script>
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
+    <script type="text/javascript" src="mkws-widget-reference.js"></script>
+  </head>
+  <body>
+    <div class='mkwsReference' autosearch='!param!q'>result will appear here</div>
+  </body>
+</html>
diff --git a/examples/htdocs/stateful.html b/examples/htdocs/stateful.html
new file mode 100644 (file)
index 0000000..875c056
--- /dev/null
@@ -0,0 +1,20 @@
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>MKWS demo client</title>
+    <link rel="stylesheet" type="text/css" href="//mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="//mkws.indexdata.com/mkws-complete.js"></script>
+    <style>.searchbox { float: right; }</style>
+  </head>
+  <body>
+    <div class="mkwsSwitch"></div>
+    <div class="mkwsLang"></div>
+    <form>
+      <input type="text" name="q" class="searchbox"/>
+    </form>
+    <div class="mkwsResults" autosearch="!param!q"></div>
+    <div class="mkwsTargets"></div>
+    <div class="mkwsStat"></div>
+    <div class="mkwsBuilder"></div>
+  </body>
+</html>
index 572d583..2f39dd2 100644 (file)
@@ -4,7 +4,7 @@
     <title>MKWS demo client</title>
     <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
     <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
-    <script id="mkwsTemplateSummary" type="text/x-handlebars-template">
+    <script class="mkwsTemplate_Summary" type="text/x-handlebars-template">
       <a href="#" id="{{_id}}" onclick="{{_onclick}}">
        <b>{{md-title}}</b>
       </a>
@@ -15,7 +15,7 @@
        <span><i>{{md-title-responsibility}}</i></span>
       {{/if}}
     </script>
-    <script id="mkwsTemplateRecord" type="text/x-handlebars-template">
+    <script class="mkwsTemplate_Record" type="text/x-handlebars-template">
       <table>
        <tr>
          <th>Title</th>
     </script>
   </head>
   <body>
-    <div id="mkwsSwitch"></div>
-    <div id="mkwsLang"></div>
-    <div id="mkwsSearch"></div>
-    <div id="mkwsResults"></div>
-    <div id="mkwsTargets"></div>
-    <div id="mkwsStat"></div>
+    <div class="mkwsSwitch"></div>
+    <div class="mkwsLang"></div>
+    <div class="mkwsSearch"></div>
+    <div class="mkwsResults"></div>
+    <div class="mkwsTargets"></div>
+    <div class="mkwsStat"></div>
   </body>
 </html>
diff --git a/examples/htdocs/two-teams.html b/examples/htdocs/two-teams.html
new file mode 100644 (file)
index 0000000..006f11b
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>MKWS demo: Mike's playground</title>
+    <link rel="stylesheet" type="text/css" href="http://mkws.indexdata.com/mkws.css" />
+    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="http://mkws.indexdata.com/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="http://mkws.indexdata.com/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="http://mkws.indexdata.com/pazpar2/js/pz2.js"></script>
+    <script type="text/javascript" src="http://mkws.indexdata.com/mkws.js"></script>
+  </head>
+  <body>
+    <table width="100%" border="1">
+      <tr>
+       <td valign="top" width="50%">
+         <div class="mkwsSwitch"></div>
+         <div class="mkwsLang"></div>
+         <div class="mkwsSearch"></div>
+         <div class="mkwsResults"></div>
+         <div class="mkwsTargets"></div>
+         <div class="mkwsStat"></div>
+       </td>
+       <td valign="top" width="50%">
+         <div class="mkwsSwitch mkwsTeam_2"></div>
+         <div class="mkwsLang mkwsTeam_2"></div>
+         <div class="mkwsSearch mkwsTeam_2"></div>
+         <div class="mkwsResults mkwsTeam_2"></div>
+         <div class="mkwsTargets mkwsTeam_2"></div>
+         <div class="mkwsStat mkwsTeam_2"></div>
+       </td>
+      </tr>
+    </table>
+    <div class="mkwsMOTD">This is the first MOTD</div>
+    <div class="mkwsMOTD mkwsTeam_2">This is the second MOTD</div>
+  </body>
+</html>
index d669525..2c57e7a 100644 (file)
@@ -4,27 +4,41 @@
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo: Wolfram's playground</title>
-    <link rel="stylesheet" type="text/css" href="../../tools/htdocs/mkws.css" />
+    <link rel="stylesheet" type="text/css" href="tools/htdocs/mkws.css" />
     <script type="text/javascript">
       var mkws_config = {
-       /*
          pazpar2_url : "/service-proxy/",
-         service_proxy_auth : "/service-proxy-auth/",
-         */
+         show_perpage: false,
+         show_sort: false,
+         perpage_default: 10,
+         sort_default: "title:1",
+         lang: "de",
+         service_proxy_auth : "/service-proxy-testauth"
       };
     </script>
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.4.4.min.js"></script>
-    <script type="text/javascript" src="http://mkws.indexdata.com/pazpar2/js/pz2.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/handlebars-v1.1.2.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/jquery.json-2.4.js"></script>
-    <script type="text/javascript" src="../../tools/htdocs/mkws.js"></script>
+    <script type="text/javascript" src="src/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="src/pz2.js"></script>
+    <script type="text/javascript" src="src/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="src/jquery.json-2.4.js"></script>
+   
+    <!-- <script type="text/javascript" src="src/mkws.js"></script> -->
+    <script type="text/javascript" src="src/mkws-handlebars.js"></script>
+    <script type="text/javascript" src="src/mkws-core.js"></script>
+    <script type="text/javascript" src="src/mkws-team.js"></script>
+    <script type="text/javascript" src="src/mkws-filter.js"></script>
+    <script type="text/javascript" src="src/mkws-widgets.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-termlists.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-authname.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-log.js"></script>
+    <script type="text/javascript" src="src/mkws-widget-record.js"></script>
+
     <style type="text/css">
-      #mkwsTermlists div.facet {
+      .mkwsTermlists div.facet {
       float:left;
       width: 30%;
       margin: 0.3em;
       }
-      #mkwsStat {
+      .mkwsStat {
       text-align: right;
       }
     </style>
     <table width="100%" border="0">
       <tr>
         <td>
-         <div id="mkwsSwitch"></div>
-          <div id="mkwsLang"></div>
-          <div id="mkwsSearch"></div>
+         <div class="mkwsSwitch"></div>
+          <div class="mkwsLang"></div>
+          <div class="mkwsSearch"></div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:500px; overflow: auto">
-            <div id="mkwsPager"></div>
-            <div id="mkwsNavi"></div>
-            <div id="mkwsRecords"></div>
-            <div id="mkwsTargets"></div>
-            <div id="mkwsRanking"></div>
+            <div class="mkwsPager"></div>
+            <div class="mkwsNavi"></div>
+            <div class="mkwsRecords"></div>
+            <div class="mkwsTargets"></div>
+            <div class="mkwsRanking"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:300px; overflow: hidden">
-            <div id="mkwsTermlists"></div>
+            <div class="mkwsTermlists"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
-          <div id="mkwsStat"></div>
+          <div class="mkwsStat"></div>
         </td>
       </tr>
     </table>
index 84efba2..277342e 100644 (file)
@@ -4,23 +4,14 @@
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <title>MKWS demo: Wolfram's playground</title>
-    <link rel="stylesheet" type="text/css" href="../../tools/htdocs/mkws.css" />
-    <script type="text/javascript">
-      var mkws_config = {
-       /*
-         pazpar2_url : "/service-proxy/",
-         service_proxy_auth : "/service-proxy-auth/",
-         */
-      };
-    </script>
-    <script type="text/javascript" src="../../tools/htdocs/mkws-complete.js"></script>
+    <link rel="stylesheet" type="text/css" href="tools/htdocs/mkws.css" />
     <style type="text/css">
-      #mkwsTermlists div.facet {
+      .mkwsTermlists div.facet {
       float:left;
       width: 30%;
       margin: 0.3em;
       }
-      #mkwsStat {
+      .mkwsStat {
       text-align: right;
       }
     </style>
     <table width="100%" border="0">
       <tr>
         <td>
-         <div id="mkwsSwitch"></div>
-          <div id="mkwsLang"></div>
-          <div id="mkwsSearch"></div>
+         <div class="mkwsSwitch"></div>
+          <div class="mkwsLang"></div>
+          <div class="mkwsSearch"></div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:500px; overflow: auto">
-            <div id="mkwsPager"></div>
-            <div id="mkwsNavi"></div>
-            <div id="mkwsRecords"></div>
-            <div id="mkwsTargets"></div>
-            <div id="mkwsRanking"></div>
+            <div class="mkwsPager"></div>
+            <div class="mkwsNavi"></div>
+            <div class="mkwsRecords"></div>
+            <div class="mkwsTargets"></div>
+            <div class="mkwsRanking"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
           <div style="height:300px; overflow: hidden">
-            <div id="mkwsTermlists"></div>
+            <div class="mkwsTermlists"></div>
           </div>
         </td>
       </tr>
       <tr>
         <td>
-          <div id="mkwsStat"></div>
+          <div class="mkwsStat"></div>
         </td>
       </tr>
     </table>
+
+    <script type="text/javascript" src="src/mkws-complete.js"></script>
+    <!-- <script type="text/javascript" src="https://mkws.indexdata.com/mkws-complete.js"></script> -->
+
+    <script type="text/javascript">
+      var mkws_config = {
+          pazpar2_url : "/service-proxy/",
+          service_proxy_auth: "/service-proxy-testauth",
+          perpage_default: 10
+      };
+    </script>
+
   </body>
 </html>
diff --git a/examples/jasmine/SpecRunner.html b/examples/jasmine/SpecRunner.html
new file mode 100644 (file)
index 0000000..64e472e
--- /dev/null
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+  "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+  <title>Jasmine Spec Runner</title>
+
+  <link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.1/jasmine_favicon.png">
+  <link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
+  <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
+  <script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>
+
+  <!-- include source files here... -->
+  <script type="text/javascript" src="src/Player.js"></script>
+  <script type="text/javascript" src="src/Song.js"></script>
+
+  <!-- include spec files here... -->
+  <script type="text/javascript" src="spec/SpecHelper.js"></script>
+  <script type="text/javascript" src="spec/PlayerSpec.js"></script>
+
+  <script type="text/javascript">
+    (function() {
+      var jasmineEnv = jasmine.getEnv();
+      jasmineEnv.updateInterval = 1000;
+
+      var htmlReporter = new jasmine.HtmlReporter();
+
+      jasmineEnv.addReporter(htmlReporter);
+
+      jasmineEnv.specFilter = function(spec) {
+        return htmlReporter.specFilter(spec);
+      };
+
+      var currentWindowOnload = window.onload;
+
+      window.onload = function() {
+        if (currentWindowOnload) {
+          currentWindowOnload();
+        }
+        execJasmine();
+      };
+
+      function execJasmine() {
+        jasmineEnv.execute();
+      }
+
+    })();
+  </script>
+
+</head>
+
+<body>
+</body>
+</html>
diff --git a/examples/jasmine/lib/jasmine-1.3.1/MIT.LICENSE b/examples/jasmine/lib/jasmine-1.3.1/MIT.LICENSE
new file mode 100644 (file)
index 0000000..7c435ba
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright (c) 2008-2011 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/examples/jasmine/lib/jasmine-1.3.1/jasmine-html.js b/examples/jasmine/lib/jasmine-1.3.1/jasmine-html.js
new file mode 100644 (file)
index 0000000..543d569
--- /dev/null
@@ -0,0 +1,681 @@
+jasmine.HtmlReporterHelpers = {};
+
+jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) {
+  var el = document.createElement(type);
+
+  for (var i = 2; i < arguments.length; i++) {
+    var child = arguments[i];
+
+    if (typeof child === 'string') {
+      el.appendChild(document.createTextNode(child));
+    } else {
+      if (child) {
+        el.appendChild(child);
+      }
+    }
+  }
+
+  for (var attr in attrs) {
+    if (attr == "className") {
+      el[attr] = attrs[attr];
+    } else {
+      el.setAttribute(attr, attrs[attr]);
+    }
+  }
+
+  return el;
+};
+
+jasmine.HtmlReporterHelpers.getSpecStatus = function(child) {
+  var results = child.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.skipped) {
+    status = 'skipped';
+  }
+
+  return status;
+};
+
+jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) {
+  var parentDiv = this.dom.summary;
+  var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite';
+  var parent = child[parentSuite];
+
+  if (parent) {
+    if (typeof this.views.suites[parent.id] == 'undefined') {
+      this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views);
+    }
+    parentDiv = this.views.suites[parent.id].element;
+  }
+
+  parentDiv.appendChild(childElement);
+};
+
+
+jasmine.HtmlReporterHelpers.addHelpers = function(ctor) {
+  for(var fn in jasmine.HtmlReporterHelpers) {
+    ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn];
+  }
+};
+
+jasmine.HtmlReporter = function(_doc) {
+  var self = this;
+  var doc = _doc || window.document;
+
+  var reporterView;
+
+  var dom = {};
+
+  // Jasmine Reporter Public Interface
+  self.logRunningSpecs = false;
+
+  self.reportRunnerStarting = function(runner) {
+    var specs = runner.specs() || [];
+
+    if (specs.length == 0) {
+      return;
+    }
+
+    createReporterDom(runner.env.versionString());
+    doc.body.appendChild(dom.reporter);
+    setExceptionHandling();
+
+    reporterView = new jasmine.HtmlReporter.ReporterView(dom);
+    reporterView.addSpecs(specs, self.specFilter);
+  };
+
+  self.reportRunnerResults = function(runner) {
+    reporterView && reporterView.complete();
+  };
+
+  self.reportSuiteResults = function(suite) {
+    reporterView.suiteComplete(suite);
+  };
+
+  self.reportSpecStarting = function(spec) {
+    if (self.logRunningSpecs) {
+      self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
+    }
+  };
+
+  self.reportSpecResults = function(spec) {
+    reporterView.specComplete(spec);
+  };
+
+  self.log = function() {
+    var console = jasmine.getGlobal().console;
+    if (console && console.log) {
+      if (console.log.apply) {
+        console.log.apply(console, arguments);
+      } else {
+        console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
+      }
+    }
+  };
+
+  self.specFilter = function(spec) {
+    if (!focusedSpecName()) {
+      return true;
+    }
+
+    return spec.getFullName().indexOf(focusedSpecName()) === 0;
+  };
+
+  return self;
+
+  function focusedSpecName() {
+    var specName;
+
+    (function memoizeFocusedSpec() {
+      if (specName) {
+        return;
+      }
+
+      var paramMap = [];
+      var params = jasmine.HtmlReporter.parameters(doc);
+
+      for (var i = 0; i < params.length; i++) {
+        var p = params[i].split('=');
+        paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
+      }
+
+      specName = paramMap.spec;
+    })();
+
+    return specName;
+  }
+
+  function createReporterDom(version) {
+    dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' },
+      dom.banner = self.createDom('div', { className: 'banner' },
+        self.createDom('span', { className: 'title' }, "Jasmine "),
+        self.createDom('span', { className: 'version' }, version)),
+
+      dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}),
+      dom.alert = self.createDom('div', {className: 'alert'},
+        self.createDom('span', { className: 'exceptions' },
+          self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'),
+          self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))),
+      dom.results = self.createDom('div', {className: 'results'},
+        dom.summary = self.createDom('div', { className: 'summary' }),
+        dom.details = self.createDom('div', { id: 'details' }))
+    );
+  }
+
+  function noTryCatch() {
+    return window.location.search.match(/catch=false/);
+  }
+
+  function searchWithCatch() {
+    var params = jasmine.HtmlReporter.parameters(window.document);
+    var removed = false;
+    var i = 0;
+
+    while (!removed && i < params.length) {
+      if (params[i].match(/catch=/)) {
+        params.splice(i, 1);
+        removed = true;
+      }
+      i++;
+    }
+    if (jasmine.CATCH_EXCEPTIONS) {
+      params.push("catch=false");
+    }
+
+    return params.join("&");
+  }
+
+  function setExceptionHandling() {
+    var chxCatch = document.getElementById('no_try_catch');
+
+    if (noTryCatch()) {
+      chxCatch.setAttribute('checked', true);
+      jasmine.CATCH_EXCEPTIONS = false;
+    }
+    chxCatch.onclick = function() {
+      window.location.search = searchWithCatch();
+    };
+  }
+};
+jasmine.HtmlReporter.parameters = function(doc) {
+  var paramStr = doc.location.search.substring(1);
+  var params = [];
+
+  if (paramStr.length > 0) {
+    params = paramStr.split('&');
+  }
+  return params;
+}
+jasmine.HtmlReporter.sectionLink = function(sectionName) {
+  var link = '?';
+  var params = [];
+
+  if (sectionName) {
+    params.push('spec=' + encodeURIComponent(sectionName));
+  }
+  if (!jasmine.CATCH_EXCEPTIONS) {
+    params.push("catch=false");
+  }
+  if (params.length > 0) {
+    link += params.join("&");
+  }
+
+  return link;
+};
+jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter);
+jasmine.HtmlReporter.ReporterView = function(dom) {
+  this.startedAt = new Date();
+  this.runningSpecCount = 0;
+  this.completeSpecCount = 0;
+  this.passedCount = 0;
+  this.failedCount = 0;
+  this.skippedCount = 0;
+
+  this.createResultsMenu = function() {
+    this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'},
+      this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'),
+      ' | ',
+      this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing'));
+
+    this.summaryMenuItem.onclick = function() {
+      dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, '');
+    };
+
+    this.detailsMenuItem.onclick = function() {
+      showDetails();
+    };
+  };
+
+  this.addSpecs = function(specs, specFilter) {
+    this.totalSpecCount = specs.length;
+
+    this.views = {
+      specs: {},
+      suites: {}
+    };
+
+    for (var i = 0; i < specs.length; i++) {
+      var spec = specs[i];
+      this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views);
+      if (specFilter(spec)) {
+        this.runningSpecCount++;
+      }
+    }
+  };
+
+  this.specComplete = function(spec) {
+    this.completeSpecCount++;
+
+    if (isUndefined(this.views.specs[spec.id])) {
+      this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom);
+    }
+
+    var specView = this.views.specs[spec.id];
+
+    switch (specView.status()) {
+      case 'passed':
+        this.passedCount++;
+        break;
+
+      case 'failed':
+        this.failedCount++;
+        break;
+
+      case 'skipped':
+        this.skippedCount++;
+        break;
+    }
+
+    specView.refresh();
+    this.refresh();
+  };
+
+  this.suiteComplete = function(suite) {
+    var suiteView = this.views.suites[suite.id];
+    if (isUndefined(suiteView)) {
+      return;
+    }
+    suiteView.refresh();
+  };
+
+  this.refresh = function() {
+
+    if (isUndefined(this.resultsMenu)) {
+      this.createResultsMenu();
+    }
+
+    // currently running UI
+    if (isUndefined(this.runningAlert)) {
+      this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" });
+      dom.alert.appendChild(this.runningAlert);
+    }
+    this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount);
+
+    // skipped specs UI
+    if (isUndefined(this.skippedAlert)) {
+      this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" });
+    }
+
+    this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
+
+    if (this.skippedCount === 1 && isDefined(dom.alert)) {
+      dom.alert.appendChild(this.skippedAlert);
+    }
+
+    // passing specs UI
+    if (isUndefined(this.passedAlert)) {
+      this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" });
+    }
+    this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount);
+
+    // failing specs UI
+    if (isUndefined(this.failedAlert)) {
+      this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"});
+    }
+    this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount);
+
+    if (this.failedCount === 1 && isDefined(dom.alert)) {
+      dom.alert.appendChild(this.failedAlert);
+      dom.alert.appendChild(this.resultsMenu);
+    }
+
+    // summary info
+    this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount);
+    this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing";
+  };
+
+  this.complete = function() {
+    dom.alert.removeChild(this.runningAlert);
+
+    this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all";
+
+    if (this.failedCount === 0) {
+      dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount)));
+    } else {
+      showDetails();
+    }
+
+    dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"));
+  };
+
+  return this;
+
+  function showDetails() {
+    if (dom.reporter.className.search(/showDetails/) === -1) {
+      dom.reporter.className += " showDetails";
+    }
+  }
+
+  function isUndefined(obj) {
+    return typeof obj === 'undefined';
+  }
+
+  function isDefined(obj) {
+    return !isUndefined(obj);
+  }
+
+  function specPluralizedFor(count) {
+    var str = count + " spec";
+    if (count > 1) {
+      str += "s"
+    }
+    return str;
+  }
+
+};
+
+jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView);
+
+
+jasmine.HtmlReporter.SpecView = function(spec, dom, views) {
+  this.spec = spec;
+  this.dom = dom;
+  this.views = views;
+
+  this.symbol = this.createDom('li', { className: 'pending' });
+  this.dom.symbolSummary.appendChild(this.symbol);
+
+  this.summary = this.createDom('div', { className: 'specSummary' },
+    this.createDom('a', {
+      className: 'description',
+      href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()),
+      title: this.spec.getFullName()
+    }, this.spec.description)
+  );
+
+  this.detail = this.createDom('div', { className: 'specDetail' },
+      this.createDom('a', {
+        className: 'description',
+        href: '?spec=' + encodeURIComponent(this.spec.getFullName()),
+        title: this.spec.getFullName()
+      }, this.spec.getFullName())
+  );
+};
+
+jasmine.HtmlReporter.SpecView.prototype.status = function() {
+  return this.getSpecStatus(this.spec);
+};
+
+jasmine.HtmlReporter.SpecView.prototype.refresh = function() {
+  this.symbol.className = this.status();
+
+  switch (this.status()) {
+    case 'skipped':
+      break;
+
+    case 'passed':
+      this.appendSummaryToSuiteDiv();
+      break;
+
+    case 'failed':
+      this.appendSummaryToSuiteDiv();
+      this.appendFailureDetail();
+      break;
+  }
+};
+
+jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() {
+  this.summary.className += ' ' + this.status();
+  this.appendToSummary(this.spec, this.summary);
+};
+
+jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() {
+  this.detail.className += ' ' + this.status();
+
+  var resultItems = this.spec.results().getItems();
+  var messagesDiv = this.createDom('div', { className: 'messages' });
+
+  for (var i = 0; i < resultItems.length; i++) {
+    var result = resultItems[i];
+
+    if (result.type == 'log') {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
+    } else if (result.type == 'expect' && result.passed && !result.passed()) {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
+
+      if (result.trace.stack) {
+        messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
+      }
+    }
+  }
+
+  if (messagesDiv.childNodes.length > 0) {
+    this.detail.appendChild(messagesDiv);
+    this.dom.details.appendChild(this.detail);
+  }
+};
+
+jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) {
+  this.suite = suite;
+  this.dom = dom;
+  this.views = views;
+
+  this.element = this.createDom('div', { className: 'suite' },
+    this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description)
+  );
+
+  this.appendToSummary(this.suite, this.element);
+};
+
+jasmine.HtmlReporter.SuiteView.prototype.status = function() {
+  return this.getSpecStatus(this.suite);
+};
+
+jasmine.HtmlReporter.SuiteView.prototype.refresh = function() {
+  this.element.className += " " + this.status();
+};
+
+jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView);
+
+/* @deprecated Use jasmine.HtmlReporter instead
+ */
+jasmine.TrivialReporter = function(doc) {
+  this.document = doc || document;
+  this.suiteDivs = {};
+  this.logRunningSpecs = false;
+};
+
+jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
+  var el = document.createElement(type);
+
+  for (var i = 2; i < arguments.length; i++) {
+    var child = arguments[i];
+
+    if (typeof child === 'string') {
+      el.appendChild(document.createTextNode(child));
+    } else {
+      if (child) { el.appendChild(child); }
+    }
+  }
+
+  for (var attr in attrs) {
+    if (attr == "className") {
+      el[attr] = attrs[attr];
+    } else {
+      el.setAttribute(attr, attrs[attr]);
+    }
+  }
+
+  return el;
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
+  var showPassed, showSkipped;
+
+  this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' },
+      this.createDom('div', { className: 'banner' },
+        this.createDom('div', { className: 'logo' },
+            this.createDom('span', { className: 'title' }, "Jasmine"),
+            this.createDom('span', { className: 'version' }, runner.env.versionString())),
+        this.createDom('div', { className: 'options' },
+            "Show ",
+            showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
+            this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
+            showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
+            this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
+            )
+          ),
+
+      this.runnerDiv = this.createDom('div', { className: 'runner running' },
+          this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
+          this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
+          this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
+      );
+
+  this.document.body.appendChild(this.outerDiv);
+
+  var suites = runner.suites();
+  for (var i = 0; i < suites.length; i++) {
+    var suite = suites[i];
+    var suiteDiv = this.createDom('div', { className: 'suite' },
+        this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
+        this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
+    this.suiteDivs[suite.id] = suiteDiv;
+    var parentDiv = this.outerDiv;
+    if (suite.parentSuite) {
+      parentDiv = this.suiteDivs[suite.parentSuite.id];
+    }
+    parentDiv.appendChild(suiteDiv);
+  }
+
+  this.startedAt = new Date();
+
+  var self = this;
+  showPassed.onclick = function(evt) {
+    if (showPassed.checked) {
+      self.outerDiv.className += ' show-passed';
+    } else {
+      self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
+    }
+  };
+
+  showSkipped.onclick = function(evt) {
+    if (showSkipped.checked) {
+      self.outerDiv.className += ' show-skipped';
+    } else {
+      self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
+    }
+  };
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
+  var results = runner.results();
+  var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
+  this.runnerDiv.setAttribute("class", className);
+  //do it twice for IE
+  this.runnerDiv.setAttribute("className", className);
+  var specs = runner.specs();
+  var specCount = 0;
+  for (var i = 0; i < specs.length; i++) {
+    if (this.specFilter(specs[i])) {
+      specCount++;
+    }
+  }
+  var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
+  message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
+  this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
+
+  this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
+};
+
+jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
+  var results = suite.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.totalCount === 0) { // todo: change this to check results.skipped
+    status = 'skipped';
+  }
+  this.suiteDivs[suite.id].className += " " + status;
+};
+
+jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
+  if (this.logRunningSpecs) {
+    this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
+  }
+};
+
+jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
+  var results = spec.results();
+  var status = results.passed() ? 'passed' : 'failed';
+  if (results.skipped) {
+    status = 'skipped';
+  }
+  var specDiv = this.createDom('div', { className: 'spec '  + status },
+      this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
+      this.createDom('a', {
+        className: 'description',
+        href: '?spec=' + encodeURIComponent(spec.getFullName()),
+        title: spec.getFullName()
+      }, spec.description));
+
+
+  var resultItems = results.getItems();
+  var messagesDiv = this.createDom('div', { className: 'messages' });
+  for (var i = 0; i < resultItems.length; i++) {
+    var result = resultItems[i];
+
+    if (result.type == 'log') {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
+    } else if (result.type == 'expect' && result.passed && !result.passed()) {
+      messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
+
+      if (result.trace.stack) {
+        messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
+      }
+    }
+  }
+
+  if (messagesDiv.childNodes.length > 0) {
+    specDiv.appendChild(messagesDiv);
+  }
+
+  this.suiteDivs[spec.suite.id].appendChild(specDiv);
+};
+
+jasmine.TrivialReporter.prototype.log = function() {
+  var console = jasmine.getGlobal().console;
+  if (console && console.log) {
+    if (console.log.apply) {
+      console.log.apply(console, arguments);
+    } else {
+      console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
+    }
+  }
+};
+
+jasmine.TrivialReporter.prototype.getLocation = function() {
+  return this.document.location;
+};
+
+jasmine.TrivialReporter.prototype.specFilter = function(spec) {
+  var paramMap = {};
+  var params = this.getLocation().search.substring(1).split('&');
+  for (var i = 0; i < params.length; i++) {
+    var p = params[i].split('=');
+    paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
+  }
+
+  if (!paramMap.spec) {
+    return true;
+  }
+  return spec.getFullName().indexOf(paramMap.spec) === 0;
+};
diff --git a/examples/jasmine/lib/jasmine-1.3.1/jasmine.css b/examples/jasmine/lib/jasmine-1.3.1/jasmine.css
new file mode 100644 (file)
index 0000000..8c008dc
--- /dev/null
@@ -0,0 +1,82 @@
+body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; }
+
+#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; }
+#HTMLReporter a { text-decoration: none; }
+#HTMLReporter a:hover { text-decoration: underline; }
+#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; }
+#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; }
+#HTMLReporter #jasmine_content { position: fixed; right: 100%; }
+#HTMLReporter .version { color: #aaaaaa; }
+#HTMLReporter .banner { margin-top: 14px; }
+#HTMLReporter .duration { color: #aaaaaa; float: right; }
+#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; }
+#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; }
+#HTMLReporter .symbolSummary li.passed { font-size: 14px; }
+#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; }
+#HTMLReporter .symbolSummary li.failed { line-height: 9px; }
+#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; }
+#HTMLReporter .symbolSummary li.skipped { font-size: 14px; }
+#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; }
+#HTMLReporter .symbolSummary li.pending { line-height: 11px; }
+#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; }
+#HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; }
+#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; }
+#HTMLReporter .runningAlert { background-color: #666666; }
+#HTMLReporter .skippedAlert { background-color: #aaaaaa; }
+#HTMLReporter .skippedAlert:first-child { background-color: #333333; }
+#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; }
+#HTMLReporter .passingAlert { background-color: #a6b779; }
+#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; }
+#HTMLReporter .failingAlert { background-color: #cf867e; }
+#HTMLReporter .failingAlert:first-child { background-color: #b03911; }
+#HTMLReporter .results { margin-top: 14px; }
+#HTMLReporter #details { display: none; }
+#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; }
+#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; }
+#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; }
+#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; }
+#HTMLReporter.showDetails .summary { display: none; }
+#HTMLReporter.showDetails #details { display: block; }
+#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; }
+#HTMLReporter .summary { margin-top: 14px; }
+#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; }
+#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; }
+#HTMLReporter .summary .specSummary.failed a { color: #b03911; }
+#HTMLReporter .description + .suite { margin-top: 0; }
+#HTMLReporter .suite { margin-top: 14px; }
+#HTMLReporter .suite a { color: #333333; }
+#HTMLReporter #details .specDetail { margin-bottom: 28px; }
+#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; }
+#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; }
+#HTMLReporter .resultMessage span.result { display: block; }
+#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; }
+
+#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ }
+#TrivialReporter a:visited, #TrivialReporter a { color: #303; }
+#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; }
+#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; }
+#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; }
+#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; }
+#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; }
+#TrivialReporter .runner.running { background-color: yellow; }
+#TrivialReporter .options { text-align: right; font-size: .8em; }
+#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; }
+#TrivialReporter .suite .suite { margin: 5px; }
+#TrivialReporter .suite.passed { background-color: #dfd; }
+#TrivialReporter .suite.failed { background-color: #fdd; }
+#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; }
+#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; }
+#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; }
+#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; }
+#TrivialReporter .spec.skipped { background-color: #bbb; }
+#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; }
+#TrivialReporter .passed { background-color: #cfc; display: none; }
+#TrivialReporter .failed { background-color: #fbb; }
+#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; }
+#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; }
+#TrivialReporter .resultMessage .mismatch { color: black; }
+#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; }
+#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; }
+#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; }
+#TrivialReporter #jasmine_content { position: fixed; right: 100%; }
+#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; }
diff --git a/examples/jasmine/lib/jasmine-1.3.1/jasmine.js b/examples/jasmine/lib/jasmine-1.3.1/jasmine.js
new file mode 100644 (file)
index 0000000..6b3459b
--- /dev/null
@@ -0,0 +1,2600 @@
+var isCommonJS = typeof window == "undefined" && typeof exports == "object";
+
+/**
+ * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework.
+ *
+ * @namespace
+ */
+var jasmine = {};
+if (isCommonJS) exports.jasmine = jasmine;
+/**
+ * @private
+ */
+jasmine.unimplementedMethod_ = function() {
+  throw new Error("unimplemented method");
+};
+
+/**
+ * Use <code>jasmine.undefined</code> instead of <code>undefined</code>, since <code>undefined</code> is just
+ * a plain old variable and may be redefined by somebody else.
+ *
+ * @private
+ */
+jasmine.undefined = jasmine.___undefined___;
+
+/**
+ * Show diagnostic messages in the console if set to true
+ *
+ */
+jasmine.VERBOSE = false;
+
+/**
+ * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed.
+ *
+ */
+jasmine.DEFAULT_UPDATE_INTERVAL = 250;
+
+/**
+ * Maximum levels of nesting that will be included when an object is pretty-printed
+ */
+jasmine.MAX_PRETTY_PRINT_DEPTH = 40;
+
+/**
+ * Default timeout interval in milliseconds for waitsFor() blocks.
+ */
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
+
+/**
+ * By default exceptions thrown in the context of a test are caught by jasmine so that it can run the remaining tests in the suite.
+ * Set to false to let the exception bubble up in the browser.
+ *
+ */
+jasmine.CATCH_EXCEPTIONS = true;
+
+jasmine.getGlobal = function() {
+  function getGlobal() {
+    return this;
+  }
+
+  return getGlobal();
+};
+
+/**
+ * Allows for bound functions to be compared.  Internal use only.
+ *
+ * @ignore
+ * @private
+ * @param base {Object} bound 'this' for the function
+ * @param name {Function} function to find
+ */
+jasmine.bindOriginal_ = function(base, name) {
+  var original = base[name];
+  if (original.apply) {
+    return function() {
+      return original.apply(base, arguments);
+    };
+  } else {
+    // IE support
+    return jasmine.getGlobal()[name];
+  }
+};
+
+jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout');
+jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout');
+jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval');
+jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval');
+
+jasmine.MessageResult = function(values) {
+  this.type = 'log';
+  this.values = values;
+  this.trace = new Error(); // todo: test better
+};
+
+jasmine.MessageResult.prototype.toString = function() {
+  var text = "";
+  for (var i = 0; i < this.values.length; i++) {
+    if (i > 0) text += " ";
+    if (jasmine.isString_(this.values[i])) {
+      text += this.values[i];
+    } else {
+      text += jasmine.pp(this.values[i]);
+    }
+  }
+  return text;
+};
+
+jasmine.ExpectationResult = function(params) {
+  this.type = 'expect';
+  this.matcherName = params.matcherName;
+  this.passed_ = params.passed;
+  this.expected = params.expected;
+  this.actual = params.actual;
+  this.message = this.passed_ ? 'Passed.' : params.message;
+
+  var trace = (params.trace || new Error(this.message));
+  this.trace = this.passed_ ? '' : trace;
+};
+
+jasmine.ExpectationResult.prototype.toString = function () {
+  return this.message;
+};
+
+jasmine.ExpectationResult.prototype.passed = function () {
+  return this.passed_;
+};
+
+/**
+ * Getter for the Jasmine environment. Ensures one gets created
+ */
+jasmine.getEnv = function() {
+  var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env();
+  return env;
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isArray_ = function(value) {
+  return jasmine.isA_("Array", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isString_ = function(value) {
+  return jasmine.isA_("String", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isNumber_ = function(value) {
+  return jasmine.isA_("Number", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param {String} typeName
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isA_ = function(typeName, value) {
+  return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
+};
+
+/**
+ * Pretty printer for expecations.  Takes any object and turns it into a human-readable string.
+ *
+ * @param value {Object} an object to be outputted
+ * @returns {String}
+ */
+jasmine.pp = function(value) {
+  var stringPrettyPrinter = new jasmine.StringPrettyPrinter();
+  stringPrettyPrinter.format(value);
+  return stringPrettyPrinter.string;
+};
+
+/**
+ * Returns true if the object is a DOM Node.
+ *
+ * @param {Object} obj object to check
+ * @returns {Boolean}
+ */
+jasmine.isDomNode = function(obj) {
+  return obj.nodeType > 0;
+};
+
+/**
+ * Returns a matchable 'generic' object of the class type.  For use in expecations of type when values don't matter.
+ *
+ * @example
+ * // don't care about which function is passed in, as long as it's a function
+ * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function));
+ *
+ * @param {Class} clazz
+ * @returns matchable object of the type clazz
+ */
+jasmine.any = function(clazz) {
+  return new jasmine.Matchers.Any(clazz);
+};
+
+/**
+ * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the
+ * attributes on the object.
+ *
+ * @example
+ * // don't care about any other attributes than foo.
+ * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: "bar"});
+ *
+ * @param sample {Object} sample
+ * @returns matchable object for the sample
+ */
+jasmine.objectContaining = function (sample) {
+    return new jasmine.Matchers.ObjectContaining(sample);
+};
+
+/**
+ * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks.
+ *
+ * Spies should be created in test setup, before expectations.  They can then be checked, using the standard Jasmine
+ * expectation syntax. Spies can be checked if they were called or not and what the calling params were.
+ *
+ * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs).
+ *
+ * Spies are torn down at the end of every spec.
+ *
+ * Note: Do <b>not</b> call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj.
+ *
+ * @example
+ * // a stub
+ * var myStub = jasmine.createSpy('myStub');  // can be used anywhere
+ *
+ * // spy example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ *
+ * // actual foo.not will not be called, execution stops
+ * spyOn(foo, 'not');
+
+ // foo.not spied upon, execution will continue to implementation
+ * spyOn(foo, 'not').andCallThrough();
+ *
+ * // fake example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ *
+ * // foo.not(val) will return val
+ * spyOn(foo, 'not').andCallFake(function(value) {return value;});
+ *
+ * // mock example
+ * foo.not(7 == 7);
+ * expect(foo.not).toHaveBeenCalled();
+ * expect(foo.not).toHaveBeenCalledWith(true);
+ *
+ * @constructor
+ * @see spyOn, jasmine.createSpy, jasmine.createSpyObj
+ * @param {String} name
+ */
+jasmine.Spy = function(name) {
+  /**
+   * The name of the spy, if provided.
+   */
+  this.identity = name || 'unknown';
+  /**
+   *  Is this Object a spy?
+   */
+  this.isSpy = true;
+  /**
+   * The actual function this spy stubs.
+   */
+  this.plan = function() {
+  };
+  /**
+   * Tracking of the most recent call to the spy.
+   * @example
+   * var mySpy = jasmine.createSpy('foo');
+   * mySpy(1, 2);
+   * mySpy.mostRecentCall.args = [1, 2];
+   */
+  this.mostRecentCall = {};
+
+  /**
+   * Holds arguments for each call to the spy, indexed by call count
+   * @example
+   * var mySpy = jasmine.createSpy('foo');
+   * mySpy(1, 2);
+   * mySpy(7, 8);
+   * mySpy.mostRecentCall.args = [7, 8];
+   * mySpy.argsForCall[0] = [1, 2];
+   * mySpy.argsForCall[1] = [7, 8];
+   */
+  this.argsForCall = [];
+  this.calls = [];
+};
+
+/**
+ * Tells a spy to call through to the actual implemenatation.
+ *
+ * @example
+ * var foo = {
+ *   bar: function() { // do some stuff }
+ * }
+ *
+ * // defining a spy on an existing property: foo.bar
+ * spyOn(foo, 'bar').andCallThrough();
+ */
+jasmine.Spy.prototype.andCallThrough = function() {
+  this.plan = this.originalValue;
+  return this;
+};
+
+/**
+ * For setting the return value of a spy.
+ *
+ * @example
+ * // defining a spy from scratch: foo() returns 'baz'
+ * var foo = jasmine.createSpy('spy on foo').andReturn('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() returns 'baz'
+ * spyOn(foo, 'bar').andReturn('baz');
+ *
+ * @param {Object} value
+ */
+jasmine.Spy.prototype.andReturn = function(value) {
+  this.plan = function() {
+    return value;
+  };
+  return this;
+};
+
+/**
+ * For throwing an exception when a spy is called.
+ *
+ * @example
+ * // defining a spy from scratch: foo() throws an exception w/ message 'ouch'
+ * var foo = jasmine.createSpy('spy on foo').andThrow('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch'
+ * spyOn(foo, 'bar').andThrow('baz');
+ *
+ * @param {String} exceptionMsg
+ */
+jasmine.Spy.prototype.andThrow = function(exceptionMsg) {
+  this.plan = function() {
+    throw exceptionMsg;
+  };
+  return this;
+};
+
+/**
+ * Calls an alternate implementation when a spy is called.
+ *
+ * @example
+ * var baz = function() {
+ *   // do some stuff, return something
+ * }
+ * // defining a spy from scratch: foo() calls the function baz
+ * var foo = jasmine.createSpy('spy on foo').andCall(baz);
+ *
+ * // defining a spy on an existing property: foo.bar() calls an anonymnous function
+ * spyOn(foo, 'bar').andCall(function() { return 'baz';} );
+ *
+ * @param {Function} fakeFunc
+ */
+jasmine.Spy.prototype.andCallFake = function(fakeFunc) {
+  this.plan = fakeFunc;
+  return this;
+};
+
+/**
+ * Resets all of a spy's the tracking variables so that it can be used again.
+ *
+ * @example
+ * spyOn(foo, 'bar');
+ *
+ * foo.bar();
+ *
+ * expect(foo.bar.callCount).toEqual(1);
+ *
+ * foo.bar.reset();
+ *
+ * expect(foo.bar.callCount).toEqual(0);
+ */
+jasmine.Spy.prototype.reset = function() {
+  this.wasCalled = false;
+  this.callCount = 0;
+  this.argsForCall = [];
+  this.calls = [];
+  this.mostRecentCall = {};
+};
+
+jasmine.createSpy = function(name) {
+
+  var spyObj = function() {
+    spyObj.wasCalled = true;
+    spyObj.callCount++;
+    var args = jasmine.util.argsToArray(arguments);
+    spyObj.mostRecentCall.object = this;
+    spyObj.mostRecentCall.args = args;
+    spyObj.argsForCall.push(args);
+    spyObj.calls.push({object: this, args: args});
+    return spyObj.plan.apply(this, arguments);
+  };
+
+  var spy = new jasmine.Spy(name);
+
+  for (var prop in spy) {
+    spyObj[prop] = spy[prop];
+  }
+
+  spyObj.reset();
+
+  return spyObj;
+};
+
+/**
+ * Determines whether an object is a spy.
+ *
+ * @param {jasmine.Spy|Object} putativeSpy
+ * @returns {Boolean}
+ */
+jasmine.isSpy = function(putativeSpy) {
+  return putativeSpy && putativeSpy.isSpy;
+};
+
+/**
+ * Creates a more complicated spy: an Object that has every property a function that is a spy.  Used for stubbing something
+ * large in one call.
+ *
+ * @param {String} baseName name of spy class
+ * @param {Array} methodNames array of names of methods to make spies
+ */
+jasmine.createSpyObj = function(baseName, methodNames) {
+  if (!jasmine.isArray_(methodNames) || methodNames.length === 0) {
+    throw new Error('createSpyObj requires a non-empty array of method names to create spies for');
+  }
+  var obj = {};
+  for (var i = 0; i < methodNames.length; i++) {
+    obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]);
+  }
+  return obj;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the current spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.log = function() {
+  var spec = jasmine.getEnv().currentSpec;
+  spec.log.apply(spec, arguments);
+};
+
+/**
+ * Function that installs a spy on an existing object's method name.  Used within a Spec to create a spy.
+ *
+ * @example
+ * // spy example
+ * var foo = {
+ *   not: function(bool) { return !bool; }
+ * }
+ * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops
+ *
+ * @see jasmine.createSpy
+ * @param obj
+ * @param methodName
+ * @return {jasmine.Spy} a Jasmine spy that can be chained with all spy methods
+ */
+var spyOn = function(obj, methodName) {
+  return jasmine.getEnv().currentSpec.spyOn(obj, methodName);
+};
+if (isCommonJS) exports.spyOn = spyOn;
+
+/**
+ * Creates a Jasmine spec that will be added to the current suite.
+ *
+ * // TODO: pending tests
+ *
+ * @example
+ * it('should be true', function() {
+ *   expect(true).toEqual(true);
+ * });
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var it = function(desc, func) {
+  return jasmine.getEnv().it(desc, func);
+};
+if (isCommonJS) exports.it = it;
+
+/**
+ * Creates a <em>disabled</em> Jasmine spec.
+ *
+ * A convenience method that allows existing specs to be disabled temporarily during development.
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var xit = function(desc, func) {
+  return jasmine.getEnv().xit(desc, func);
+};
+if (isCommonJS) exports.xit = xit;
+
+/**
+ * Starts a chain for a Jasmine expectation.
+ *
+ * It is passed an Object that is the actual value and should chain to one of the many
+ * jasmine.Matchers functions.
+ *
+ * @param {Object} actual Actual value to test against and expected value
+ * @return {jasmine.Matchers}
+ */
+var expect = function(actual) {
+  return jasmine.getEnv().currentSpec.expect(actual);
+};
+if (isCommonJS) exports.expect = expect;
+
+/**
+ * Defines part of a jasmine spec.  Used in cominbination with waits or waitsFor in asynchrnous specs.
+ *
+ * @param {Function} func Function that defines part of a jasmine spec.
+ */
+var runs = function(func) {
+  jasmine.getEnv().currentSpec.runs(func);
+};
+if (isCommonJS) exports.runs = runs;
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+var waits = function(timeout) {
+  jasmine.getEnv().currentSpec.waits(timeout);
+};
+if (isCommonJS) exports.waits = waits;
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+  jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments);
+};
+if (isCommonJS) exports.waitsFor = waitsFor;
+
+/**
+ * A function that is called before each spec in a suite.
+ *
+ * Used for spec setup, including validating assumptions.
+ *
+ * @param {Function} beforeEachFunction
+ */
+var beforeEach = function(beforeEachFunction) {
+  jasmine.getEnv().beforeEach(beforeEachFunction);
+};
+if (isCommonJS) exports.beforeEach = beforeEach;
+
+/**
+ * A function that is called after each spec in a suite.
+ *
+ * Used for restoring any state that is hijacked during spec execution.
+ *
+ * @param {Function} afterEachFunction
+ */
+var afterEach = function(afterEachFunction) {
+  jasmine.getEnv().afterEach(afterEachFunction);
+};
+if (isCommonJS) exports.afterEach = afterEach;
+
+/**
+ * Defines a suite of specifications.
+ *
+ * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared
+ * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization
+ * of setup in some tests.
+ *
+ * @example
+ * // TODO: a simple suite
+ *
+ * // TODO: a simple suite with a nested describe block
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var describe = function(description, specDefinitions) {
+  return jasmine.getEnv().describe(description, specDefinitions);
+};
+if (isCommonJS) exports.describe = describe;
+
+/**
+ * Disables a suite of specifications.  Used to disable some suites in a file, or files, temporarily during development.
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var xdescribe = function(description, specDefinitions) {
+  return jasmine.getEnv().xdescribe(description, specDefinitions);
+};
+if (isCommonJS) exports.xdescribe = xdescribe;
+
+
+// Provide the XMLHttpRequest class for IE 5.x-6.x:
+jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() {
+  function tryIt(f) {
+    try {
+      return f();
+    } catch(e) {
+    }
+    return null;
+  }
+
+  var xhr = tryIt(function() {
+    return new ActiveXObject("Msxml2.XMLHTTP.6.0");
+  }) ||
+    tryIt(function() {
+      return new ActiveXObject("Msxml2.XMLHTTP.3.0");
+    }) ||
+    tryIt(function() {
+      return new ActiveXObject("Msxml2.XMLHTTP");
+    }) ||
+    tryIt(function() {
+      return new ActiveXObject("Microsoft.XMLHTTP");
+    });
+
+  if (!xhr) throw new Error("This browser does not support XMLHttpRequest.");
+
+  return xhr;
+} : XMLHttpRequest;
+/**
+ * @namespace
+ */
+jasmine.util = {};
+
+/**
+ * Declare that a child class inherit it's prototype from the parent class.
+ *
+ * @private
+ * @param {Function} childClass
+ * @param {Function} parentClass
+ */
+jasmine.util.inherit = function(childClass, parentClass) {
+  /**
+   * @private
+   */
+  var subclass = function() {
+  };
+  subclass.prototype = parentClass.prototype;
+  childClass.prototype = new subclass();
+};
+
+jasmine.util.formatException = function(e) {
+  var lineNumber;
+  if (e.line) {
+    lineNumber = e.line;
+  }
+  else if (e.lineNumber) {
+    lineNumber = e.lineNumber;
+  }
+
+  var file;
+
+  if (e.sourceURL) {
+    file = e.sourceURL;
+  }
+  else if (e.fileName) {
+    file = e.fileName;
+  }
+
+  var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString();
+
+  if (file && lineNumber) {
+    message += ' in ' + file + ' (line ' + lineNumber + ')';
+  }
+
+  return message;
+};
+
+jasmine.util.htmlEscape = function(str) {
+  if (!str) return str;
+  return str.replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;');
+};
+
+jasmine.util.argsToArray = function(args) {
+  var arrayOfArgs = [];
+  for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]);
+  return arrayOfArgs;
+};
+
+jasmine.util.extend = function(destination, source) {
+  for (var property in source) destination[property] = source[property];
+  return destination;
+};
+
+/**
+ * Environment for Jasmine
+ *
+ * @constructor
+ */
+jasmine.Env = function() {
+  this.currentSpec = null;
+  this.currentSuite = null;
+  this.currentRunner_ = new jasmine.Runner(this);
+
+  this.reporter = new jasmine.MultiReporter();
+
+  this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL;
+  this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+  this.lastUpdate = 0;
+  this.specFilter = function() {
+    return true;
+  };
+
+  this.nextSpecId_ = 0;
+  this.nextSuiteId_ = 0;
+  this.equalityTesters_ = [];
+
+  // wrap matchers
+  this.matchersClass = function() {
+    jasmine.Matchers.apply(this, arguments);
+  };
+  jasmine.util.inherit(this.matchersClass, jasmine.Matchers);
+
+  jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass);
+};
+
+
+jasmine.Env.prototype.setTimeout = jasmine.setTimeout;
+jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout;
+jasmine.Env.prototype.setInterval = jasmine.setInterval;
+jasmine.Env.prototype.clearInterval = jasmine.clearInterval;
+
+/**
+ * @returns an object containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.version = function () {
+  if (jasmine.version_) {
+    return jasmine.version_;
+  } else {
+    throw new Error('Version not set');
+  }
+};
+
+/**
+ * @returns string containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.versionString = function() {
+  if (!jasmine.version_) {
+    return "version unknown";
+  }
+
+  var version = this.version();
+  var versionString = version.major + "." + version.minor + "." + version.build;
+  if (version.release_candidate) {
+    versionString += ".rc" + version.release_candidate;
+  }
+  versionString += " revision " + version.revision;
+  return versionString;
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSpecId = function () {
+  return this.nextSpecId_++;
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSuiteId = function () {
+  return this.nextSuiteId_++;
+};
+
+/**
+ * Register a reporter to receive status updates from Jasmine.
+ * @param {jasmine.Reporter} reporter An object which will receive status updates.
+ */
+jasmine.Env.prototype.addReporter = function(reporter) {
+  this.reporter.addReporter(reporter);
+};
+
+jasmine.Env.prototype.execute = function() {
+  this.currentRunner_.execute();
+};
+
+jasmine.Env.prototype.describe = function(description, specDefinitions) {
+  var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite);
+
+  var parentSuite = this.currentSuite;
+  if (parentSuite) {
+    parentSuite.add(suite);
+  } else {
+    this.currentRunner_.add(suite);
+  }
+
+  this.currentSuite = suite;
+
+  var declarationError = null;
+  try {
+    specDefinitions.call(suite);
+  } catch(e) {
+    declarationError = e;
+  }
+
+  if (declarationError) {
+    this.it("encountered a declaration exception", function() {
+      throw declarationError;
+    });
+  }
+
+  this.currentSuite = parentSuite;
+
+  return suite;
+};
+
+jasmine.Env.prototype.beforeEach = function(beforeEachFunction) {
+  if (this.currentSuite) {
+    this.currentSuite.beforeEach(beforeEachFunction);
+  } else {
+    this.currentRunner_.beforeEach(beforeEachFunction);
+  }
+};
+
+jasmine.Env.prototype.currentRunner = function () {
+  return this.currentRunner_;
+};
+
+jasmine.Env.prototype.afterEach = function(afterEachFunction) {
+  if (this.currentSuite) {
+    this.currentSuite.afterEach(afterEachFunction);
+  } else {
+    this.currentRunner_.afterEach(afterEachFunction);
+  }
+
+};
+
+jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) {
+  return {
+    execute: function() {
+    }
+  };
+};
+
+jasmine.Env.prototype.it = function(description, func) {
+  var spec = new jasmine.Spec(this, this.currentSuite, description);
+  this.currentSuite.add(spec);
+  this.currentSpec = spec;
+
+  if (func) {
+    spec.runs(func);
+  }
+
+  return spec;
+};
+
+jasmine.Env.prototype.xit = function(desc, func) {
+  return {
+    id: this.nextSpecId(),
+    runs: function() {
+    }
+  };
+};
+
+jasmine.Env.prototype.compareRegExps_ = function(a, b, mismatchKeys, mismatchValues) {
+  if (a.source != b.source)
+    mismatchValues.push("expected pattern /" + b.source + "/ is not equal to the pattern /" + a.source + "/");
+
+  if (a.ignoreCase != b.ignoreCase)
+    mismatchValues.push("expected modifier i was" + (b.ignoreCase ? " " : " not ") + "set and does not equal the origin modifier");
+
+  if (a.global != b.global)
+    mismatchValues.push("expected modifier g was" + (b.global ? " " : " not ") + "set and does not equal the origin modifier");
+
+  if (a.multiline != b.multiline)
+    mismatchValues.push("expected modifier m was" + (b.multiline ? " " : " not ") + "set and does not equal the origin modifier");
+
+  if (a.sticky != b.sticky)
+    mismatchValues.push("expected modifier y was" + (b.sticky ? " " : " not ") + "set and does not equal the origin modifier");
+
+  return (mismatchValues.length === 0);
+};
+
+jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) {
+  if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) {
+    return true;
+  }
+
+  a.__Jasmine_been_here_before__ = b;
+  b.__Jasmine_been_here_before__ = a;
+
+  var hasKey = function(obj, keyName) {
+    return obj !== null && obj[keyName] !== jasmine.undefined;
+  };
+
+  for (var property in b) {
+    if (!hasKey(a, property) && hasKey(b, property)) {
+      mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
+    }
+  }
+  for (property in a) {
+    if (!hasKey(b, property) && hasKey(a, property)) {
+      mismatchKeys.push("expected missing key '" + property + "', but present in actual.");
+    }
+  }
+  for (property in b) {
+    if (property == '__Jasmine_been_here_before__') continue;
+    if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) {
+      mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual.");
+    }
+  }
+
+  if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) {
+    mismatchValues.push("arrays were not the same length");
+  }
+
+  delete a.__Jasmine_been_here_before__;
+  delete b.__Jasmine_been_here_before__;
+  return (mismatchKeys.length === 0 && mismatchValues.length === 0);
+};
+
+jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) {
+  mismatchKeys = mismatchKeys || [];
+  mismatchValues = mismatchValues || [];
+
+  for (var i = 0; i < this.equalityTesters_.length; i++) {
+    var equalityTester = this.equalityTesters_[i];
+    var result = equalityTester(a, b, this, mismatchKeys, mismatchValues);
+    if (result !== jasmine.undefined) return result;
+  }
+
+  if (a === b) return true;
+
+  if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) {
+    return (a == jasmine.undefined && b == jasmine.undefined);
+  }
+
+  if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) {
+    return a === b;
+  }
+
+  if (a instanceof Date && b instanceof Date) {
+    return a.getTime() == b.getTime();
+  }
+
+  if (a.jasmineMatches) {
+    return a.jasmineMatches(b);
+  }
+
+  if (b.jasmineMatches) {
+    return b.jasmineMatches(a);
+  }
+
+  if (a instanceof jasmine.Matchers.ObjectContaining) {
+    return a.matches(b);
+  }
+
+  if (b instanceof jasmine.Matchers.ObjectContaining) {
+    return b.matches(a);
+  }
+
+  if (jasmine.isString_(a) && jasmine.isString_(b)) {
+    return (a == b);
+  }
+
+  if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) {
+    return (a == b);
+  }
+
+  if (a instanceof RegExp && b instanceof RegExp) {
+    return this.compareRegExps_(a, b, mismatchKeys, mismatchValues);
+  }
+
+  if (typeof a === "object" && typeof b === "object") {
+    return this.compareObjects_(a, b, mismatchKeys, mismatchValues);
+  }
+
+  //Straight check
+  return (a === b);
+};
+
+jasmine.Env.prototype.contains_ = function(haystack, needle) {
+  if (jasmine.isArray_(haystack)) {
+    for (var i = 0; i < haystack.length; i++) {
+      if (this.equals_(haystack[i], needle)) return true;
+    }
+    return false;
+  }
+  return haystack.indexOf(needle) >= 0;
+};
+
+jasmine.Env.prototype.addEqualityTester = function(equalityTester) {
+  this.equalityTesters_.push(equalityTester);
+};
+/** No-op base class for Jasmine reporters.
+ *
+ * @constructor
+ */
+jasmine.Reporter = function() {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerStarting = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerResults = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecStarting = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecResults = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.log = function(str) {
+};
+
+/**
+ * Blocks are functions with executable code that make up a spec.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {Function} func
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Block = function(env, func, spec) {
+  this.env = env;
+  this.func = func;
+  this.spec = spec;
+};
+
+jasmine.Block.prototype.execute = function(onComplete) {
+  if (!jasmine.CATCH_EXCEPTIONS) {
+    this.func.apply(this.spec);
+  }
+  else {
+    try {
+      this.func.apply(this.spec);
+    } catch (e) {
+      this.spec.fail(e);
+    }
+  }
+  onComplete();
+};
+/** JavaScript API reporter.
+ *
+ * @constructor
+ */
+jasmine.JsApiReporter = function() {
+  this.started = false;
+  this.finished = false;
+  this.suites_ = [];
+  this.results_ = {};
+};
+
+jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) {
+  this.started = true;
+  var suites = runner.topLevelSuites();
+  for (var i = 0; i < suites.length; i++) {
+    var suite = suites[i];
+    this.suites_.push(this.summarize_(suite));
+  }
+};
+
+jasmine.JsApiReporter.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) {
+  var isSuite = suiteOrSpec instanceof jasmine.Suite;
+  var summary = {
+    id: suiteOrSpec.id,
+    name: suiteOrSpec.description,
+    type: isSuite ? 'suite' : 'spec',
+    children: []
+  };
+  
+  if (isSuite) {
+    var children = suiteOrSpec.children();
+    for (var i = 0; i < children.length; i++) {
+      summary.children.push(this.summarize_(children[i]));
+    }
+  }
+  return summary;
+};
+
+jasmine.JsApiReporter.prototype.results = function() {
+  return this.results_;
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) {
+  return this.results_[specId];
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) {
+  this.finished = true;
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) {
+  this.results_[spec.id] = {
+    messages: spec.results().getItems(),
+    result: spec.results().failedCount > 0 ? "failed" : "passed"
+  };
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.log = function(str) {
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){
+  var results = {};
+  for (var i = 0; i < specIds.length; i++) {
+    var specId = specIds[i];
+    results[specId] = this.summarizeResult_(this.results_[specId]);
+  }
+  return results;
+};
+
+jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){
+  var summaryMessages = [];
+  var messagesLength = result.messages.length;
+  for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) {
+    var resultMessage = result.messages[messageIndex];
+    summaryMessages.push({
+      text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined,
+      passed: resultMessage.passed ? resultMessage.passed() : true,
+      type: resultMessage.type,
+      message: resultMessage.message,
+      trace: {
+        stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined
+      }
+    });
+  }
+
+  return {
+    result : result.result,
+    messages : summaryMessages
+  };
+};
+
+/**
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param actual
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Matchers = function(env, actual, spec, opt_isNot) {
+  this.env = env;
+  this.actual = actual;
+  this.spec = spec;
+  this.isNot = opt_isNot || false;
+  this.reportWasCalled_ = false;
+};
+
+// todo: @deprecated as of Jasmine 0.11, remove soon [xw]
+jasmine.Matchers.pp = function(str) {
+  throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!");
+};
+
+// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw]
+jasmine.Matchers.prototype.report = function(result, failing_message, details) {
+  throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs");
+};
+
+jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) {
+  for (var methodName in prototype) {
+    if (methodName == 'report') continue;
+    var orig = prototype[methodName];
+    matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig);
+  }
+};
+
+jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
+  return function() {
+    var matcherArgs = jasmine.util.argsToArray(arguments);
+    var result = matcherFunction.apply(this, arguments);
+
+    if (this.isNot) {
+      result = !result;
+    }
+
+    if (this.reportWasCalled_) return result;
+
+    var message;
+    if (!result) {
+      if (this.message) {
+        message = this.message.apply(this, arguments);
+        if (jasmine.isArray_(message)) {
+          message = message[this.isNot ? 1 : 0];
+        }
+      } else {
+        var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
+        message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate;
+        if (matcherArgs.length > 0) {
+          for (var i = 0; i < matcherArgs.length; i++) {
+            if (i > 0) message += ",";
+            message += " " + jasmine.pp(matcherArgs[i]);
+          }
+        }
+        message += ".";
+      }
+    }
+    var expectationResult = new jasmine.ExpectationResult({
+      matcherName: matcherName,
+      passed: result,
+      expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
+      actual: this.actual,
+      message: message
+    });
+    this.spec.addMatcherResult(expectationResult);
+    return jasmine.undefined;
+  };
+};
+
+
+
+
+/**
+ * toBe: compares the actual to the expected using ===
+ * @param expected
+ */
+jasmine.Matchers.prototype.toBe = function(expected) {
+  return this.actual === expected;
+};
+
+/**
+ * toNotBe: compares the actual to the expected using !==
+ * @param expected
+ * @deprecated as of 1.0. Use not.toBe() instead.
+ */
+jasmine.Matchers.prototype.toNotBe = function(expected) {
+  return this.actual !== expected;
+};
+
+/**
+ * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toEqual = function(expected) {
+  return this.env.equals_(this.actual, expected);
+};
+
+/**
+ * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual
+ * @param expected
+ * @deprecated as of 1.0. Use not.toEqual() instead.
+ */
+jasmine.Matchers.prototype.toNotEqual = function(expected) {
+  return !this.env.equals_(this.actual, expected);
+};
+
+/**
+ * Matcher that compares the actual to the expected using a regular expression.  Constructs a RegExp, so takes
+ * a pattern or a String.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toMatch = function(expected) {
+  return new RegExp(expected).test(this.actual);
+};
+
+/**
+ * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch
+ * @param expected
+ * @deprecated as of 1.0. Use not.toMatch() instead.
+ */
+jasmine.Matchers.prototype.toNotMatch = function(expected) {
+  return !(new RegExp(expected).test(this.actual));
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeDefined = function() {
+  return (this.actual !== jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeUndefined = function() {
+  return (this.actual === jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to null.
+ */
+jasmine.Matchers.prototype.toBeNull = function() {
+  return (this.actual === null);
+};
+
+/**
+ * Matcher that compares the actual to NaN.
+ */
+jasmine.Matchers.prototype.toBeNaN = function() {
+       this.message = function() {
+               return [ "Expected " + jasmine.pp(this.actual) + " to be NaN." ];
+       };
+
+       return (this.actual !== this.actual);
+};
+
+/**
+ * Matcher that boolean not-nots the actual.
+ */
+jasmine.Matchers.prototype.toBeTruthy = function() {
+  return !!this.actual;
+};
+
+
+/**
+ * Matcher that boolean nots the actual.
+ */
+jasmine.Matchers.prototype.toBeFalsy = function() {
+  return !this.actual;
+};
+
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called.
+ */
+jasmine.Matchers.prototype.toHaveBeenCalled = function() {
+  if (arguments.length > 0) {
+    throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
+  }
+
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy " + this.actual.identity + " to have been called.",
+      "Expected spy " + this.actual.identity + " not to have been called."
+    ];
+  };
+
+  return this.actual.wasCalled;
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */
+jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled;
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was not called.
+ *
+ * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead
+ */
+jasmine.Matchers.prototype.wasNotCalled = function() {
+  if (arguments.length > 0) {
+    throw new Error('wasNotCalled does not take arguments');
+  }
+
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy " + this.actual.identity + " to not have been called.",
+      "Expected spy " + this.actual.identity + " to have been called."
+    ];
+  };
+
+  return !this.actual.wasCalled;
+};
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters.
+ *
+ * @example
+ *
+ */
+jasmine.Matchers.prototype.toHaveBeenCalledWith = function() {
+  var expectedArgs = jasmine.util.argsToArray(arguments);
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+  this.message = function() {
+    var invertedMessage = "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was.";
+    var positiveMessage = "";
+    if (this.actual.callCount === 0) {
+      positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.";
+    } else {
+      positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but actual calls were " + jasmine.pp(this.actual.argsForCall).replace(/^\[ | \]$/g, '')
+    }
+    return [positiveMessage, invertedMessage];
+  };
+
+  return this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith;
+
+/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasNotCalledWith = function() {
+  var expectedArgs = jasmine.util.argsToArray(arguments);
+  if (!jasmine.isSpy(this.actual)) {
+    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+  }
+
+  this.message = function() {
+    return [
+      "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was",
+      "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was"
+    ];
+  };
+
+  return !this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/**
+ * Matcher that checks that the expected item is an element in the actual Array.
+ *
+ * @param {Object} expected
+ */
+jasmine.Matchers.prototype.toContain = function(expected) {
+  return this.env.contains_(this.actual, expected);
+};
+
+/**
+ * Matcher that checks that the expected item is NOT an element in the actual Array.
+ *
+ * @param {Object} expected
+ * @deprecated as of 1.0. Use not.toContain() instead.
+ */
+jasmine.Matchers.prototype.toNotContain = function(expected) {
+  return !this.env.contains_(this.actual, expected);
+};
+
+jasmine.Matchers.prototype.toBeLessThan = function(expected) {
+  return this.actual < expected;
+};
+
+jasmine.Matchers.prototype.toBeGreaterThan = function(expected) {
+  return this.actual > expected;
+};
+
+/**
+ * Matcher that checks that the expected item is equal to the actual item
+ * up to a given level of decimal precision (default 2).
+ *
+ * @param {Number} expected
+ * @param {Number} precision, as number of decimal places
+ */
+jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) {
+  if (!(precision === 0)) {
+    precision = precision || 2;
+  }
+  return Math.abs(expected - this.actual) < (Math.pow(10, -precision) / 2);
+};
+
+/**
+ * Matcher that checks that the expected exception was thrown by the actual.
+ *
+ * @param {String} [expected]
+ */
+jasmine.Matchers.prototype.toThrow = function(expected) {
+  var result = false;
+  var exception;
+  if (typeof this.actual != 'function') {
+    throw new Error('Actual is not a function');
+  }
+  try {
+    this.actual();
+  } catch (e) {
+    exception = e;
+  }
+  if (exception) {
+    result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected));
+  }
+
+  var not = this.isNot ? "not " : "";
+
+  this.message = function() {
+    if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) {
+      return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' ');
+    } else {
+      return "Expected function to throw an exception.";
+    }
+  };
+
+  return result;
+};
+
+jasmine.Matchers.Any = function(expectedClass) {
+  this.expectedClass = expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.jasmineMatches = function(other) {
+  if (this.expectedClass == String) {
+    return typeof other == 'string' || other instanceof String;
+  }
+
+  if (this.expectedClass == Number) {
+    return typeof other == 'number' || other instanceof Number;
+  }
+
+  if (this.expectedClass == Function) {
+    return typeof other == 'function' || other instanceof Function;
+  }
+
+  if (this.expectedClass == Object) {
+    return typeof other == 'object';
+  }
+
+  return other instanceof this.expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.jasmineToString = function() {
+  return '<jasmine.any(' + this.expectedClass + ')>';
+};
+
+jasmine.Matchers.ObjectContaining = function (sample) {
+  this.sample = sample;
+};
+
+jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) {
+  mismatchKeys = mismatchKeys || [];
+  mismatchValues = mismatchValues || [];
+
+  var env = jasmine.getEnv();
+
+  var hasKey = function(obj, keyName) {
+    return obj != null && obj[keyName] !== jasmine.undefined;
+  };
+
+  for (var property in this.sample) {
+    if (!hasKey(other, property) && hasKey(this.sample, property)) {
+      mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
+    }
+    else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) {
+      mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual.");
+    }
+  }
+
+  return (mismatchKeys.length === 0 && mismatchValues.length === 0);
+};
+
+jasmine.Matchers.ObjectContaining.prototype.jasmineToString = function () {
+  return "<jasmine.objectContaining(" + jasmine.pp(this.sample) + ")>";
+};
+// Mock setTimeout, clearTimeout
+// Contributed by Pivotal Computer Systems, www.pivotalsf.com
+
+jasmine.FakeTimer = function() {
+  this.reset();
+
+  var self = this;
+  self.setTimeout = function(funcToCall, millis) {
+    self.timeoutsMade++;
+    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false);
+    return self.timeoutsMade;
+  };
+
+  self.setInterval = function(funcToCall, millis) {
+    self.timeoutsMade++;
+    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true);
+    return self.timeoutsMade;
+  };
+
+  self.clearTimeout = function(timeoutKey) {
+    self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+  };
+
+  self.clearInterval = function(timeoutKey) {
+    self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+  };
+
+};
+
+jasmine.FakeTimer.prototype.reset = function() {
+  this.timeoutsMade = 0;
+  this.scheduledFunctions = {};
+  this.nowMillis = 0;
+};
+
+jasmine.FakeTimer.prototype.tick = function(millis) {
+  var oldMillis = this.nowMillis;
+  var newMillis = oldMillis + millis;
+  this.runFunctionsWithinRange(oldMillis, newMillis);
+  this.nowMillis = newMillis;
+};
+
+jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) {
+  var scheduledFunc;
+  var funcsToRun = [];
+  for (var timeoutKey in this.scheduledFunctions) {
+    scheduledFunc = this.scheduledFunctions[timeoutKey];
+    if (scheduledFunc != jasmine.undefined &&
+        scheduledFunc.runAtMillis >= oldMillis &&
+        scheduledFunc.runAtMillis <= nowMillis) {
+      funcsToRun.push(scheduledFunc);
+      this.scheduledFunctions[timeoutKey] = jasmine.undefined;
+    }
+  }
+
+  if (funcsToRun.length > 0) {
+    funcsToRun.sort(function(a, b) {
+      return a.runAtMillis - b.runAtMillis;
+    });
+    for (var i = 0; i < funcsToRun.length; ++i) {
+      try {
+        var funcToRun = funcsToRun[i];
+        this.nowMillis = funcToRun.runAtMillis;
+        funcToRun.funcToCall();
+        if (funcToRun.recurring) {
+          this.scheduleFunction(funcToRun.timeoutKey,
+              funcToRun.funcToCall,
+              funcToRun.millis,
+              true);
+        }
+      } catch(e) {
+      }
+    }
+    this.runFunctionsWithinRange(oldMillis, nowMillis);
+  }
+};
+
+jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) {
+  this.scheduledFunctions[timeoutKey] = {
+    runAtMillis: this.nowMillis + millis,
+    funcToCall: funcToCall,
+    recurring: recurring,
+    timeoutKey: timeoutKey,
+    millis: millis
+  };
+};
+
+/**
+ * @namespace
+ */
+jasmine.Clock = {
+  defaultFakeTimer: new jasmine.FakeTimer(),
+
+  reset: function() {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.defaultFakeTimer.reset();
+  },
+
+  tick: function(millis) {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.defaultFakeTimer.tick(millis);
+  },
+
+  runFunctionsWithinRange: function(oldMillis, nowMillis) {
+    jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis);
+  },
+
+  scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) {
+    jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring);
+  },
+
+  useMock: function() {
+    if (!jasmine.Clock.isInstalled()) {
+      var spec = jasmine.getEnv().currentSpec;
+      spec.after(jasmine.Clock.uninstallMock);
+
+      jasmine.Clock.installMock();
+    }
+  },
+
+  installMock: function() {
+    jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer;
+  },
+
+  uninstallMock: function() {
+    jasmine.Clock.assertInstalled();
+    jasmine.Clock.installed = jasmine.Clock.real;
+  },
+
+  real: {
+    setTimeout: jasmine.getGlobal().setTimeout,
+    clearTimeout: jasmine.getGlobal().clearTimeout,
+    setInterval: jasmine.getGlobal().setInterval,
+    clearInterval: jasmine.getGlobal().clearInterval
+  },
+
+  assertInstalled: function() {
+    if (!jasmine.Clock.isInstalled()) {
+      throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()");
+    }
+  },
+
+  isInstalled: function() {
+    return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer;
+  },
+
+  installed: null
+};
+jasmine.Clock.installed = jasmine.Clock.real;
+
+//else for IE support
+jasmine.getGlobal().setTimeout = function(funcToCall, millis) {
+  if (jasmine.Clock.installed.setTimeout.apply) {
+    return jasmine.Clock.installed.setTimeout.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.setTimeout(funcToCall, millis);
+  }
+};
+
+jasmine.getGlobal().setInterval = function(funcToCall, millis) {
+  if (jasmine.Clock.installed.setInterval.apply) {
+    return jasmine.Clock.installed.setInterval.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.setInterval(funcToCall, millis);
+  }
+};
+
+jasmine.getGlobal().clearTimeout = function(timeoutKey) {
+  if (jasmine.Clock.installed.clearTimeout.apply) {
+    return jasmine.Clock.installed.clearTimeout.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.clearTimeout(timeoutKey);
+  }
+};
+
+jasmine.getGlobal().clearInterval = function(timeoutKey) {
+  if (jasmine.Clock.installed.clearTimeout.apply) {
+    return jasmine.Clock.installed.clearInterval.apply(this, arguments);
+  } else {
+    return jasmine.Clock.installed.clearInterval(timeoutKey);
+  }
+};
+
+/**
+ * @constructor
+ */
+jasmine.MultiReporter = function() {
+  this.subReporters_ = [];
+};
+jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter);
+
+jasmine.MultiReporter.prototype.addReporter = function(reporter) {
+  this.subReporters_.push(reporter);
+};
+
+(function() {
+  var functionNames = [
+    "reportRunnerStarting",
+    "reportRunnerResults",
+    "reportSuiteResults",
+    "reportSpecStarting",
+    "reportSpecResults",
+    "log"
+  ];
+  for (var i = 0; i < functionNames.length; i++) {
+    var functionName = functionNames[i];
+    jasmine.MultiReporter.prototype[functionName] = (function(functionName) {
+      return function() {
+        for (var j = 0; j < this.subReporters_.length; j++) {
+          var subReporter = this.subReporters_[j];
+          if (subReporter[functionName]) {
+            subReporter[functionName].apply(subReporter, arguments);
+          }
+        }
+      };
+    })(functionName);
+  }
+})();
+/**
+ * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults
+ *
+ * @constructor
+ */
+jasmine.NestedResults = function() {
+  /**
+   * The total count of results
+   */
+  this.totalCount = 0;
+  /**
+   * Number of passed results
+   */
+  this.passedCount = 0;
+  /**
+   * Number of failed results
+   */
+  this.failedCount = 0;
+  /**
+   * Was this suite/spec skipped?
+   */
+  this.skipped = false;
+  /**
+   * @ignore
+   */
+  this.items_ = [];
+};
+
+/**
+ * Roll up the result counts.
+ *
+ * @param result
+ */
+jasmine.NestedResults.prototype.rollupCounts = function(result) {
+  this.totalCount += result.totalCount;
+  this.passedCount += result.passedCount;
+  this.failedCount += result.failedCount;
+};
+
+/**
+ * Adds a log message.
+ * @param values Array of message parts which will be concatenated later.
+ */
+jasmine.NestedResults.prototype.log = function(values) {
+  this.items_.push(new jasmine.MessageResult(values));
+};
+
+/**
+ * Getter for the results: message & results.
+ */
+jasmine.NestedResults.prototype.getItems = function() {
+  return this.items_;
+};
+
+/**
+ * Adds a result, tracking counts (total, passed, & failed)
+ * @param {jasmine.ExpectationResult|jasmine.NestedResults} result
+ */
+jasmine.NestedResults.prototype.addResult = function(result) {
+  if (result.type != 'log') {
+    if (result.items_) {
+      this.rollupCounts(result);
+    } else {
+      this.totalCount++;
+      if (result.passed()) {
+        this.passedCount++;
+      } else {
+        this.failedCount++;
+      }
+    }
+  }
+  this.items_.push(result);
+};
+
+/**
+ * @returns {Boolean} True if <b>everything</b> below passed
+ */
+jasmine.NestedResults.prototype.passed = function() {
+  return this.passedCount === this.totalCount;
+};
+/**
+ * Base class for pretty printing for expectation results.
+ */
+jasmine.PrettyPrinter = function() {
+  this.ppNestLevel_ = 0;
+};
+
+/**
+ * Formats a value in a nice, human-readable string.
+ *
+ * @param value
+ */
+jasmine.PrettyPrinter.prototype.format = function(value) {
+  this.ppNestLevel_++;
+  try {
+    if (value === jasmine.undefined) {
+      this.emitScalar('undefined');
+    } else if (value === null) {
+      this.emitScalar('null');
+    } else if (value === jasmine.getGlobal()) {
+      this.emitScalar('<global>');
+    } else if (value.jasmineToString) {
+      this.emitScalar(value.jasmineToString());
+    } else if (typeof value === 'string') {
+      this.emitString(value);
+    } else if (jasmine.isSpy(value)) {
+      this.emitScalar("spy on " + value.identity);
+    } else if (value instanceof RegExp) {
+      this.emitScalar(value.toString());
+    } else if (typeof value === 'function') {
+      this.emitScalar('Function');
+    } else if (typeof value.nodeType === 'number') {
+      this.emitScalar('HTMLNode');
+    } else if (value instanceof Date) {
+      this.emitScalar('Date(' + value + ')');
+    } else if (value.__Jasmine_been_here_before__) {
+      this.emitScalar('<circular reference: ' + (jasmine.isArray_(value) ? 'Array' : 'Object') + '>');
+    } else if (jasmine.isArray_(value) || typeof value == 'object') {
+      value.__Jasmine_been_here_before__ = true;
+      if (jasmine.isArray_(value)) {
+        this.emitArray(value);
+      } else {
+        this.emitObject(value);
+      }
+      delete value.__Jasmine_been_here_before__;
+    } else {
+      this.emitScalar(value.toString());
+    }
+  } finally {
+    this.ppNestLevel_--;
+  }
+};
+
+jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) {
+  for (var property in obj) {
+    if (!obj.hasOwnProperty(property)) continue;
+    if (property == '__Jasmine_been_here_before__') continue;
+    fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && 
+                                         obj.__lookupGetter__(property) !== null) : false);
+  }
+};
+
+jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_;
+
+jasmine.StringPrettyPrinter = function() {
+  jasmine.PrettyPrinter.call(this);
+
+  this.string = '';
+};
+jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter);
+
+jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) {
+  this.append(value);
+};
+
+jasmine.StringPrettyPrinter.prototype.emitString = function(value) {
+  this.append("'" + value + "'");
+};
+
+jasmine.StringPrettyPrinter.prototype.emitArray = function(array) {
+  if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) {
+    this.append("Array");
+    return;
+  }
+
+  this.append('[ ');
+  for (var i = 0; i < array.length; i++) {
+    if (i > 0) {
+      this.append(', ');
+    }
+    this.format(array[i]);
+  }
+  this.append(' ]');
+};
+
+jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) {
+  if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) {
+    this.append("Object");
+    return;
+  }
+
+  var self = this;
+  this.append('{ ');
+  var first = true;
+
+  this.iterateObject(obj, function(property, isGetter) {
+    if (first) {
+      first = false;
+    } else {
+      self.append(', ');
+    }
+
+    self.append(property);
+    self.append(' : ');
+    if (isGetter) {
+      self.append('<getter>');
+    } else {
+      self.format(obj[property]);
+    }
+  });
+
+  this.append(' }');
+};
+
+jasmine.StringPrettyPrinter.prototype.append = function(value) {
+  this.string += value;
+};
+jasmine.Queue = function(env) {
+  this.env = env;
+
+  // parallel to blocks. each true value in this array means the block will
+  // get executed even if we abort
+  this.ensured = [];
+  this.blocks = [];
+  this.running = false;
+  this.index = 0;
+  this.offset = 0;
+  this.abort = false;
+};
+
+jasmine.Queue.prototype.addBefore = function(block, ensure) {
+  if (ensure === jasmine.undefined) {
+    ensure = false;
+  }
+
+  this.blocks.unshift(block);
+  this.ensured.unshift(ensure);
+};
+
+jasmine.Queue.prototype.add = function(block, ensure) {
+  if (ensure === jasmine.undefined) {
+    ensure = false;
+  }
+
+  this.blocks.push(block);
+  this.ensured.push(ensure);
+};
+
+jasmine.Queue.prototype.insertNext = function(block, ensure) {
+  if (ensure === jasmine.undefined) {
+    ensure = false;
+  }
+
+  this.ensured.splice((this.index + this.offset + 1), 0, ensure);
+  this.blocks.splice((this.index + this.offset + 1), 0, block);
+  this.offset++;
+};
+
+jasmine.Queue.prototype.start = function(onComplete) {
+  this.running = true;
+  this.onComplete = onComplete;
+  this.next_();
+};
+
+jasmine.Queue.prototype.isRunning = function() {
+  return this.running;
+};
+
+jasmine.Queue.LOOP_DONT_RECURSE = true;
+
+jasmine.Queue.prototype.next_ = function() {
+  var self = this;
+  var goAgain = true;
+
+  while (goAgain) {
+    goAgain = false;
+    
+    if (self.index < self.blocks.length && !(this.abort && !this.ensured[self.index])) {
+      var calledSynchronously = true;
+      var completedSynchronously = false;
+
+      var onComplete = function () {
+        if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {
+          completedSynchronously = true;
+          return;
+        }
+
+        if (self.blocks[self.index].abort) {
+          self.abort = true;
+        }
+
+        self.offset = 0;
+        self.index++;
+
+        var now = new Date().getTime();
+        if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {
+          self.env.lastUpdate = now;
+          self.env.setTimeout(function() {
+            self.next_();
+          }, 0);
+        } else {
+          if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {
+            goAgain = true;
+          } else {
+            self.next_();
+          }
+        }
+      };
+      self.blocks[self.index].execute(onComplete);
+
+      calledSynchronously = false;
+      if (completedSynchronously) {
+        onComplete();
+      }
+      
+    } else {
+      self.running = false;
+      if (self.onComplete) {
+        self.onComplete();
+      }
+    }
+  }
+};
+
+jasmine.Queue.prototype.results = function() {
+  var results = new jasmine.NestedResults();
+  for (var i = 0; i < this.blocks.length; i++) {
+    if (this.blocks[i].results) {
+      results.addResult(this.blocks[i].results());
+    }
+  }
+  return results;
+};
+
+
+/**
+ * Runner
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ */
+jasmine.Runner = function(env) {
+  var self = this;
+  self.env = env;
+  self.queue = new jasmine.Queue(env);
+  self.before_ = [];
+  self.after_ = [];
+  self.suites_ = [];
+};
+
+jasmine.Runner.prototype.execute = function() {
+  var self = this;
+  if (self.env.reporter.reportRunnerStarting) {
+    self.env.reporter.reportRunnerStarting(this);
+  }
+  self.queue.start(function () {
+    self.finishCallback();
+  });
+};
+
+jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) {
+  beforeEachFunction.typeName = 'beforeEach';
+  this.before_.splice(0,0,beforeEachFunction);
+};
+
+jasmine.Runner.prototype.afterEach = function(afterEachFunction) {
+  afterEachFunction.typeName = 'afterEach';
+  this.after_.splice(0,0,afterEachFunction);
+};
+
+
+jasmine.Runner.prototype.finishCallback = function() {
+  this.env.reporter.reportRunnerResults(this);
+};
+
+jasmine.Runner.prototype.addSuite = function(suite) {
+  this.suites_.push(suite);
+};
+
+jasmine.Runner.prototype.add = function(block) {
+  if (block instanceof jasmine.Suite) {
+    this.addSuite(block);
+  }
+  this.queue.add(block);
+};
+
+jasmine.Runner.prototype.specs = function () {
+  var suites = this.suites();
+  var specs = [];
+  for (var i = 0; i < suites.length; i++) {
+    specs = specs.concat(suites[i].specs());
+  }
+  return specs;
+};
+
+jasmine.Runner.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.Runner.prototype.topLevelSuites = function() {
+  var topLevelSuites = [];
+  for (var i = 0; i < this.suites_.length; i++) {
+    if (!this.suites_[i].parentSuite) {
+      topLevelSuites.push(this.suites_[i]);
+    }
+  }
+  return topLevelSuites;
+};
+
+jasmine.Runner.prototype.results = function() {
+  return this.queue.results();
+};
+/**
+ * Internal representation of a Jasmine specification, or test.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {jasmine.Suite} suite
+ * @param {String} description
+ */
+jasmine.Spec = function(env, suite, description) {
+  if (!env) {
+    throw new Error('jasmine.Env() required');
+  }
+  if (!suite) {
+    throw new Error('jasmine.Suite() required');
+  }
+  var spec = this;
+  spec.id = env.nextSpecId ? env.nextSpecId() : null;
+  spec.env = env;
+  spec.suite = suite;
+  spec.description = description;
+  spec.queue = new jasmine.Queue(env);
+
+  spec.afterCallbacks = [];
+  spec.spies_ = [];
+
+  spec.results_ = new jasmine.NestedResults();
+  spec.results_.description = description;
+  spec.matchersClass = null;
+};
+
+jasmine.Spec.prototype.getFullName = function() {
+  return this.suite.getFullName() + ' ' + this.description + '.';
+};
+
+
+jasmine.Spec.prototype.results = function() {
+  return this.results_;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.Spec.prototype.log = function() {
+  return this.results_.log(arguments);
+};
+
+jasmine.Spec.prototype.runs = function (func) {
+  var block = new jasmine.Block(this.env, func, this);
+  this.addToQueue(block);
+  return this;
+};
+
+jasmine.Spec.prototype.addToQueue = function (block) {
+  if (this.queue.isRunning()) {
+    this.queue.insertNext(block);
+  } else {
+    this.queue.add(block);
+  }
+};
+
+/**
+ * @param {jasmine.ExpectationResult} result
+ */
+jasmine.Spec.prototype.addMatcherResult = function(result) {
+  this.results_.addResult(result);
+};
+
+jasmine.Spec.prototype.expect = function(actual) {
+  var positive = new (this.getMatchersClass_())(this.env, actual, this);
+  positive.not = new (this.getMatchersClass_())(this.env, actual, this, true);
+  return positive;
+};
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+jasmine.Spec.prototype.waits = function(timeout) {
+  var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this);
+  this.addToQueue(waitsFunc);
+  return this;
+};
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+  var latchFunction_ = null;
+  var optional_timeoutMessage_ = null;
+  var optional_timeout_ = null;
+
+  for (var i = 0; i < arguments.length; i++) {
+    var arg = arguments[i];
+    switch (typeof arg) {
+      case 'function':
+        latchFunction_ = arg;
+        break;
+      case 'string':
+        optional_timeoutMessage_ = arg;
+        break;
+      case 'number':
+        optional_timeout_ = arg;
+        break;
+    }
+  }
+
+  var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this);
+  this.addToQueue(waitsForFunc);
+  return this;
+};
+
+jasmine.Spec.prototype.fail = function (e) {
+  var expectationResult = new jasmine.ExpectationResult({
+    passed: false,
+    message: e ? jasmine.util.formatException(e) : 'Exception',
+    trace: { stack: e.stack }
+  });
+  this.results_.addResult(expectationResult);
+};
+
+jasmine.Spec.prototype.getMatchersClass_ = function() {
+  return this.matchersClass || this.env.matchersClass;
+};
+
+jasmine.Spec.prototype.addMatchers = function(matchersPrototype) {
+  var parent = this.getMatchersClass_();
+  var newMatchersClass = function() {
+    parent.apply(this, arguments);
+  };
+  jasmine.util.inherit(newMatchersClass, parent);
+  jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass);
+  this.matchersClass = newMatchersClass;
+};
+
+jasmine.Spec.prototype.finishCallback = function() {
+  this.env.reporter.reportSpecResults(this);
+};
+
+jasmine.Spec.prototype.finish = function(onComplete) {
+  this.removeAllSpies();
+  this.finishCallback();
+  if (onComplete) {
+    onComplete();
+  }
+};
+
+jasmine.Spec.prototype.after = function(doAfter) {
+  if (this.queue.isRunning()) {
+    this.queue.add(new jasmine.Block(this.env, doAfter, this), true);
+  } else {
+    this.afterCallbacks.unshift(doAfter);
+  }
+};
+
+jasmine.Spec.prototype.execute = function(onComplete) {
+  var spec = this;
+  if (!spec.env.specFilter(spec)) {
+    spec.results_.skipped = true;
+    spec.finish(onComplete);
+    return;
+  }
+
+  this.env.reporter.reportSpecStarting(this);
+
+  spec.env.currentSpec = spec;
+
+  spec.addBeforesAndAftersToQueue();
+
+  spec.queue.start(function () {
+    spec.finish(onComplete);
+  });
+};
+
+jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() {
+  var runner = this.env.currentRunner();
+  var i;
+
+  for (var suite = this.suite; suite; suite = suite.parentSuite) {
+    for (i = 0; i < suite.before_.length; i++) {
+      this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this));
+    }
+  }
+  for (i = 0; i < runner.before_.length; i++) {
+    this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this));
+  }
+  for (i = 0; i < this.afterCallbacks.length; i++) {
+    this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this), true);
+  }
+  for (suite = this.suite; suite; suite = suite.parentSuite) {
+    for (i = 0; i < suite.after_.length; i++) {
+      this.queue.add(new jasmine.Block(this.env, suite.after_[i], this), true);
+    }
+  }
+  for (i = 0; i < runner.after_.length; i++) {
+    this.queue.add(new jasmine.Block(this.env, runner.after_[i], this), true);
+  }
+};
+
+jasmine.Spec.prototype.explodes = function() {
+  throw 'explodes function should not have been called';
+};
+
+jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) {
+  if (obj == jasmine.undefined) {
+    throw "spyOn could not find an object to spy upon for " + methodName + "()";
+  }
+
+  if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) {
+    throw methodName + '() method does not exist';
+  }
+
+  if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) {
+    throw new Error(methodName + ' has already been spied upon');
+  }
+
+  var spyObj = jasmine.createSpy(methodName);
+
+  this.spies_.push(spyObj);
+  spyObj.baseObj = obj;
+  spyObj.methodName = methodName;
+  spyObj.originalValue = obj[methodName];
+
+  obj[methodName] = spyObj;
+
+  return spyObj;
+};
+
+jasmine.Spec.prototype.removeAllSpies = function() {
+  for (var i = 0; i < this.spies_.length; i++) {
+    var spy = this.spies_[i];
+    spy.baseObj[spy.methodName] = spy.originalValue;
+  }
+  this.spies_ = [];
+};
+
+/**
+ * Internal representation of a Jasmine suite.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {String} description
+ * @param {Function} specDefinitions
+ * @param {jasmine.Suite} parentSuite
+ */
+jasmine.Suite = function(env, description, specDefinitions, parentSuite) {
+  var self = this;
+  self.id = env.nextSuiteId ? env.nextSuiteId() : null;
+  self.description = description;
+  self.queue = new jasmine.Queue(env);
+  self.parentSuite = parentSuite;
+  self.env = env;
+  self.before_ = [];
+  self.after_ = [];
+  self.children_ = [];
+  self.suites_ = [];
+  self.specs_ = [];
+};
+
+jasmine.Suite.prototype.getFullName = function() {
+  var fullName = this.description;
+  for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {
+    fullName = parentSuite.description + ' ' + fullName;
+  }
+  return fullName;
+};
+
+jasmine.Suite.prototype.finish = function(onComplete) {
+  this.env.reporter.reportSuiteResults(this);
+  this.finished = true;
+  if (typeof(onComplete) == 'function') {
+    onComplete();
+  }
+};
+
+jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) {
+  beforeEachFunction.typeName = 'beforeEach';
+  this.before_.unshift(beforeEachFunction);
+};
+
+jasmine.Suite.prototype.afterEach = function(afterEachFunction) {
+  afterEachFunction.typeName = 'afterEach';
+  this.after_.unshift(afterEachFunction);
+};
+
+jasmine.Suite.prototype.results = function() {
+  return this.queue.results();
+};
+
+jasmine.Suite.prototype.add = function(suiteOrSpec) {
+  this.children_.push(suiteOrSpec);
+  if (suiteOrSpec instanceof jasmine.Suite) {
+    this.suites_.push(suiteOrSpec);
+    this.env.currentRunner().addSuite(suiteOrSpec);
+  } else {
+    this.specs_.push(suiteOrSpec);
+  }
+  this.queue.add(suiteOrSpec);
+};
+
+jasmine.Suite.prototype.specs = function() {
+  return this.specs_;
+};
+
+jasmine.Suite.prototype.suites = function() {
+  return this.suites_;
+};
+
+jasmine.Suite.prototype.children = function() {
+  return this.children_;
+};
+
+jasmine.Suite.prototype.execute = function(onComplete) {
+  var self = this;
+  this.queue.start(function () {
+    self.finish(onComplete);
+  });
+};
+jasmine.WaitsBlock = function(env, timeout, spec) {
+  this.timeout = timeout;
+  jasmine.Block.call(this, env, null, spec);
+};
+
+jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block);
+
+jasmine.WaitsBlock.prototype.execute = function (onComplete) {
+  if (jasmine.VERBOSE) {
+    this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...');
+  }
+  this.env.setTimeout(function () {
+    onComplete();
+  }, this.timeout);
+};
+/**
+ * A block which waits for some condition to become true, with timeout.
+ *
+ * @constructor
+ * @extends jasmine.Block
+ * @param {jasmine.Env} env The Jasmine environment.
+ * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true.
+ * @param {Function} latchFunction A function which returns true when the desired condition has been met.
+ * @param {String} message The message to display if the desired condition hasn't been met within the given time period.
+ * @param {jasmine.Spec} spec The Jasmine spec.
+ */
+jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) {
+  this.timeout = timeout || env.defaultTimeoutInterval;
+  this.latchFunction = latchFunction;
+  this.message = message;
+  this.totalTimeSpentWaitingForLatch = 0;
+  jasmine.Block.call(this, env, null, spec);
+};
+jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block);
+
+jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10;
+
+jasmine.WaitsForBlock.prototype.execute = function(onComplete) {
+  if (jasmine.VERBOSE) {
+    this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen'));
+  }
+  var latchFunctionResult;
+  try {
+    latchFunctionResult = this.latchFunction.apply(this.spec);
+  } catch (e) {
+    this.spec.fail(e);
+    onComplete();
+    return;
+  }
+
+  if (latchFunctionResult) {
+    onComplete();
+  } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) {
+    var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen');
+    this.spec.fail({
+      name: 'timeout',
+      message: message
+    });
+
+    this.abort = true;
+    onComplete();
+  } else {
+    this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT;
+    var self = this;
+    this.env.setTimeout(function() {
+      self.execute(onComplete);
+    }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT);
+  }
+};
+
+jasmine.version_= {
+  "major": 1,
+  "minor": 3,
+  "build": 1,
+  "revision": 1354556913
+};
diff --git a/examples/jasmine/lib/jasmine-1.3.1/jasmine_favicon.png b/examples/jasmine/lib/jasmine-1.3.1/jasmine_favicon.png
new file mode 100644 (file)
index 0000000..3562e27
Binary files /dev/null and b/examples/jasmine/lib/jasmine-1.3.1/jasmine_favicon.png differ
diff --git a/examples/jasmine/spec/PlayerSpec.js b/examples/jasmine/spec/PlayerSpec.js
new file mode 100644 (file)
index 0000000..79f1022
--- /dev/null
@@ -0,0 +1,58 @@
+describe("Player", function() {
+  var player;
+  var song;
+
+  beforeEach(function() {
+    player = new Player();
+    song = new Song();
+  });
+
+  it("should be able to play a Song", function() {
+    player.play(song);
+    expect(player.currentlyPlayingSong).toEqual(song);
+
+    //demonstrates use of custom matcher
+    expect(player).toBePlaying(song);
+  });
+
+  describe("when song has been paused", function() {
+    beforeEach(function() {
+      player.play(song);
+      player.pause();
+    });
+
+    it("should indicate that the song is currently paused", function() {
+      expect(player.isPlaying).toBeFalsy();
+
+      // demonstrates use of 'not' with a custom matcher
+      expect(player).not.toBePlaying(song);
+    });
+
+    it("should be possible to resume", function() {
+      player.resume();
+      expect(player.isPlaying).toBeTruthy();
+      expect(player.currentlyPlayingSong).toEqual(song);
+    });
+  });
+
+  // demonstrates use of spies to intercept and test method calls
+  it("tells the current song if the user has made it a favorite", function() {
+    spyOn(song, 'persistFavoriteStatus');
+
+    player.play(song);
+    player.makeFavorite();
+
+    expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
+  });
+
+  //demonstrates use of expected exceptions
+  describe("#resume", function() {
+    it("should throw an exception if song is already playing", function() {
+      player.play(song);
+
+      expect(function() {
+        player.resume();
+      }).toThrow("song is already playing");
+    });
+  });
+});
\ No newline at end of file
diff --git a/examples/jasmine/spec/SpecHelper.js b/examples/jasmine/spec/SpecHelper.js
new file mode 100644 (file)
index 0000000..e9b8284
--- /dev/null
@@ -0,0 +1,9 @@
+beforeEach(function() {
+  this.addMatchers({
+    toBePlaying: function(expectedSong) {
+      var player = this.actual;
+      return player.currentlyPlayingSong === expectedSong && 
+             player.isPlaying;
+    }
+  });
+});
diff --git a/examples/jasmine/src/Player.js b/examples/jasmine/src/Player.js
new file mode 100644 (file)
index 0000000..fcce826
--- /dev/null
@@ -0,0 +1,22 @@
+function Player() {
+}
+Player.prototype.play = function(song) {
+  this.currentlyPlayingSong = song;
+  this.isPlaying = true;
+};
+
+Player.prototype.pause = function() {
+  this.isPlaying = false;
+};
+
+Player.prototype.resume = function() {
+  if (this.isPlaying) {
+    throw new Error("song is already playing");
+  }
+
+  this.isPlaying = true;
+};
+
+Player.prototype.makeFavorite = function() {
+  this.currentlyPlayingSong.persistFavoriteStatus(true);
+};
\ No newline at end of file
diff --git a/examples/jasmine/src/Song.js b/examples/jasmine/src/Song.js
new file mode 100644 (file)
index 0000000..a8a3f2d
--- /dev/null
@@ -0,0 +1,7 @@
+function Song() {
+}
+
+Song.prototype.persistFavoriteStatus = function(value) {
+  // something complicated
+  throw new Error("not yet implemented");
+};
\ No newline at end of file
diff --git a/notes/2013-06-24--todo b/notes/2013-06-24--todo
deleted file mode 100644 (file)
index 6bdc1be..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-Adam, Wolfram, Jakub and I just had a ninety-minute meeting to look at
-MKWS (the MasterKey Widget Set) and discuss how to progress it.
-
-I won't bother trying to summarise the discussion, just the
-conclusions.
-
-1. We really like the concept, though Jakub is wary about the danger
-   of reinventing the wheel.
-
-2. We're happy with the distinction between two levels of MK2
-   integration: MKWS for entry-level HTML programmers; and more
-   sophisticated customers who want more control can roll their own
-   MasterKey Client based on MK2-UI-core.
-
-3. To avoid that wheel-reinventing, we want MKWS to itself be built on
-   the UI core. To pick a concrete benefit: Wolfram has invested some
-   effort to make MKWS multi-lingual; but had it been based on the UI
-   core, he could have put that work into the core, and all other MK2
-   UIs would have benefitted from it.
-
-4. We plan therefore to re-engineer MKWS to use the UI core -- or,
-   which might be a faster route to the same destination, to redo the
-   MKWS work but starting from a core-based UI instead of from
-   JSDemo. (This is in accordance with the experimental nature of the
-   work we've been doing -- what we're currenty referring to as "the"
-   MKWS code is actually one of several threads within the
-   "experiments" directory.)
-
-5. Because we want to enable smooth demos at ALA this coming weekend,
-   we are NOT going to begin the re-engineering until after ALA,
-   instead concentrating on getting the current MKWS experiment to
-   work as cleanly as possible.
-
-6. We want to establishing some demo sites that use MKWS, ready to be
-   shown to prospective customers. We think the best approach is to
-   pick three sites that currently have no searching (for example,
-   Dennis's site and one of my own) and add minimal MKWS code to
-   each. Then Seb will be able to demo the small HTML/JS changes in
-   those sites.
-
-7. To make these demos work, we will need to solve the problem of
-   cross-site scripting, allowing those sites to make Service Proxy
-   requests. This is the top programming priority (and what I will
-   start working on as soon as I've sent this message!)
-
-8. After some back-and-forth, we decided that we would eliminate the
-   current use of jQuery. (We're not doing anything at all clever with
-   it -- basically just node-selection and HTML-building). Life is
-   simpler without the dependency, and the danger of colliding with a
-   different jQuery version in use on the customer's site.
-
-9. We'd like to have mkws.js include pz2api.1.js on its own, so that
-   the customer's HTML only has to do a single JS include. Not a big
-   deal, but it just makes the demos look more lightweight.
-
-10. We really like the explicitness of the configuration hash that
-   Wolfram's introduced, and the way it defaults intelligently when
-   elements of it are omitted (or indeed when the whole thing is
-   omitted). We'd like to add a config element that tells the results
-   page to pop up in front of the main website, like the Google CSE
-   results do on http://www.miketaylor.org.uk/
-
diff --git a/notes/developers.txt b/notes/developers.txt
new file mode 100644 (file)
index 0000000..02fb7bd
--- /dev/null
@@ -0,0 +1,365 @@
+Notes for developers
+
+These notes are collected by Heikki, mostly from skype chats with Wolfram
+and Mike. I collected them for my own use, but I hope they will turn out
+to be helpful to anyone who needs to get started with mkws.
+
+Environment
+-----------
+
+apt-get install yui-compressor
+get nodejs, sudo make install, ln -s /usr/local/bin/npm ~/bin/npm if needed
+
+cd .../mkws; make check
+
+
+Apache
+------
+You need to set up a local apache. 
+  * add 'mkws' in /etc/hosts to point to 127.0.0.2
+  * symlinked .../mkws/tools/apache2/mkws-heikki to /etc/apache/sites-available
+  * a2ensite mkws-heikki
+  * a2enmod rewrite
+  * a2enmod headers
+  * service apache2 reload
+  * Check that your browser sees somethig in http://mkws/ and 
+    http://mkws/jasmine-popup.html. If need be, enable javascript etc. 
+
+Libraries
+---------
+
+* We are using jquery as a browser indepent layer to access the dom, so we
+don't have to worry about IE bugs.  Wolfram looked why we are using
+jquery.json-2.4.js ... it turns out we needed it because the standard functions
+in IE8 are broken.
+
+* jasmine is a test framework (mkws dev). you will not use jasmine in a
+production site.  the nice thing with the jasmine test framework is that it
+will work with any browser. I can start a virtual machine with IE8, open the
+test page, wait 3 seconds for success and shutdown windows.
+
+* handlebar is a template engine
+
+
+
+Include files
+-------------
+
+The whitepaper says to include mkws-complete.js. This file is made by concatenating
+a number of files (see Makefile). For us developers, it is easier to include the
+raw files, as in
+
+    <script type="text/javascript" src="tools/htdocs/jquery-1.10.0.min.js"></script>
+    <script type="text/javascript" src="tools/htdocs/pz2.js"></script>
+    <script type="text/javascript" src="tools/htdocs/handlebars-v1.1.2.js"></script>
+    <script type="text/javascript" src="tools/htdocs/jquery.json-2.4.js"></script>
+    <script type="text/javascript" src="tools/htdocs/mkws.js"></script>
+
+You can also include the css directly in your test page:
+    <style type="text/css">
+      #mkwsTermlists div.facet {
+      float:left;
+      width: 30%;
+      margin: 0.3em;
+      }
+      #mkwsStat {
+      text-align: right;
+      }
+    </style>
+
+
+Most (all?) code work happens in mkws.js.
+
+
+Unit tests
+----------
+
+Tests are based on jasmine. a general description of jasmine is on
+http://jasmine.github.io/1.3/introduction.html
+
+If you want understand the test than you can look at mkws/test/spec/mkws-config.js
+and mkws/test/spec/mkws-pazpar2.js . See also mkws/test/README.txt
+
+The test scripts are included from the test page, for example
+mkws/examples/htdocs/jasmine-popup.html has 
+<script type="text/javascript" src="test/spec/mkws-pazpar2.js"></script>
+
+
+
+
+Structure of mkws.js
+--------------------
+(This will soon be out of date, but should provide some kind of starting
+point even then. This is taken directly from a Skype chat with Mike, where
+he explained the whole thing.)
+
+First page is just helper functions for the Handlebars template library, which we
+use to generate some of the HTML output. (Down the line, we will use this more
+heavilty -- right now it's only used for records).
+
+Then we define the mkws object, which contains all global state -- which hopefully
+there is not much of. It is one of only two objects we place in the global namespace:
+the other is mkws_config, which is a hash supplied by the application if it wants
+to override default configs.
+
+Next is a very short function defined to allow us to publish and subscribe events.
+That is not yet used: shifting much of the code to do so is a big part of what I
+am working on right now.
+
+Next, a very short stanza of code that just makes sure mkws_config is defined:
+simple applications won't bother to define it at all since they override none
+of the defaults.
+
+Next, a factory method for making widget objects. At present this is trivial
+because we are only now starting to need a representation of individual widgets
+as JS objects. More of the functionality will get moved into these objects over
+the next week.
+
+Next, a factory method for making widget-team objects. This is where all the
+awesomeness is at the moment. A team is a bunch of widgets that work together
+to achieve a common goal, e.g. the search-box, search-button and results-pane
+widgets.
+
+HTML elements are defined as belonging to the same team if they have an
+mkwsTeam_NAME class for the same NAME. You can have multiple teams (as in
+two-teams.html that I linked to earlier) which are completely independent of
+each other.
+
+I guess you're familiar with the JS idiom where the factory function for a kind
+of object also acts as a namespace where all the object's member-variables live,
+invisible to the outside world? That's what we do here. All the member variables
+have names of the form m_NAME.
+
+Now I sugges you skip over all the team-object code for now -- we'll return to it
+later. For now, page down to "// wrapper to call team() after page load" which is
+the next thing after the end of that function (or class, if you like).
+
+You're familiar with this JS idiom?
+  (function() { code ... })();
+Runs the code immediately, but within its own namespace. That's what we do for
+all the remaining code in mkws.js. In this case, we pass the jQuery object into
+that namespace under the name `j' for reasons that are frankly opaque to me.
+
+There's still a few places in the code where oddities live on, either from jsdemo
+or from work Wolfram's done, where I don't really understand why it's that way
+but I'm scared to change it. In this case, IIRC, it's something to do with
+protecting our copy of the jQuery object, or something.
+
+Aaanyway, within that namespaced area, where's what we do.
+
+First, we set up the mkws.debug() function, which just logs to the console in a
+form that doesn't explode IE. I have plans for this function, make it understand
+debugging levels a bit like log4j or maybe more like yaz-log where there are
+named logging types that are not in a sequence.
+
+(You will notice that the teams have a debug() function which delegates to this
+but adds some other useful team-specific stuff.)
+
+Next up: the utility function mkws.handle_node_with_team(). We use a LOT of nodes
+that have their team-name in a class (as in "mkwsTeam_NAME" outlined above).
+All the utility does is parse out that team-name, and the widget-type, from the
+classes, and pass them through to the callback.
+
+mkws.resize_page() does what it says. Gets called when window-size changes, and
+allows us to move the facers to the side or the bottom as the screen is wide or
+narrow (e.g. when you turn your iPad 90 degrees)
+
+(Aside: I thought we'd have to iterate over all teams to move their facet lists
+but it turns out we don't: jQuery just Does The Right Thing if you call
+   $(".mkwsTermlistContainer1").hide();
+or similar and there are multiple hits.)
+
+Next come a bunch of JS functions that are invoked from the MKWS UIs --
+swithching between target and record views, stepping through pages of results,
+etc. All of these are team-specific, but the global code in the HTML can't
+invoke a team's member function directly. So these stub functions just invoke
+the relevant member of the appropriate team.
+
+default_mkws_config() fills in the mkws_config structure from hardwired defaults.
+This is the wrong way round: instead, whenever we want to find a config value, we
+should default our way up a tree, starting with the individual widget's config,
+falling back to the team's config if the widget doesn't define that value, then
+the global config, and finally the default. I'll make that change once widget
+objects are fully real.
+
+authenticate_session() authenticates onto the SP when we're using it (rather
+than raw pp2). It's a bit sellotape-and-string, to be honest, just does a wget.
+It would be better if this was supported by pz2.js
+
+run_auto_searches() is what makes pages like
+  http://example.indexdata.com/auto3.html
+work. THere are two places it's invoked from. Either directly when all the HTML
+has been set up if we're using raw pp2; or when SP authentication has been
+completed if we're using that. As with the UI functions, it just delegates down
+into the teams.
+
+Finally, code that runs when the page has finished loading -- this is really
+the main() function
+
+The first thing it does is patch up the HTML so that old-style MKWS apps work
+using new-style elements. This is the code you just fixed.
+
+Straight after that, more fixup: elements that have an mkws* class but no
+team are given an extra class mkwsTeam_AUTO. This is the ONLY thing that's special
+about the team "AUTO" -- it has no other privileges.
+
+Very near the end now: we walk through all nodes with an mkws* class, and create
+the team and widget objects using the factories we described earlier. Jason is
+worried this will be slow, hence the instrumentation. It's not :-)
+
+Last of all: start things off! SP-auth if used, otherwise straight to the
+auto-searches.
+
+
+OK, want to plough into the team object?
+
+... bearing in mind that some of this should be moved out of the team into the
+top-level code, and some other parts will be moved down into individual widgets
+once we have them.
+
+OK. So we start with a bunch of member variables for state. Many of them will be
+hauntingly familiar to anyone who's worked on jsdemo :-)
+
+A new one is m_debug_time, which is a structure used for keeping track of elapsed
+time. It's nice: it lets debug messages for each time note how long it's been
+since the last message in that same team, which means you can see how slow-ass
+our network operations can be. That's implemented in debug(), which is the very
+next thing in the file.
+
+Then a bunch of code to do with setting initial values from defaults.
+
+The stuff about languages is a great example of code that should be at the top
+level, not in the team: it deals only with global objects, yet gets run n times
+if there are n teams. (I am adding a ###-comment to this effect right now.)
+
+Then we make the pz2 object, which is our only channel of communication with the
+server outside of the HTTP GET hack in authenticate_session(). The callback
+functions are all closures with access to this team's member variables. Are you
+somewhat familiar with pz2.js already?
+
+Well, it's all driven by callbacks. You register them at object-creation time;
+then later when you call (say) m_paz.search(), it will invoke your my_onsearch()
+function when the search-response arrives.
+
+There are some oddities in the order things get done. For example, is m_perpage
+set AFTER this object is created, rather than at the same time as its stablemate
+m_sort up above? No reason. So plenty of tedious, error-prone, cleaning up of
+this sort to be done.
+
+Then come the pz2 callbacks. my_onshow() is an interesting representative. The
+team-name is passed back by pz2 (which has used it as the SP window-ID to keep
+sessions separate). I used to think it needed to be passed back like this so
+the invoked callback functions could know what team they're being called for,
+but now I realise they don't need to, because they're closures that can see
+m_teamName. Oh well.
+
+Indeed, you can see that my_onshow() doesn't even USE the teamName parameter.
+
+Anyway, the point is to find specific elements that belong to the right team,
+so they can be populated with relevant data. We do that for the pager, and of
+course the record-list.
+
+The meat of the record-list is filled in by invocations of renderSummary(),
+which loads a Handlebars template and uses that to generate the HTML. Needless
+to say, loadTemplate() caches compiled templates.
+
+my_onstat() and my_onterm() do similar things -- you can figure out the details.
+
+add_single_facet(), target_filtered() and others are uninteresting utility
+functions.
+
+my_onrecord() and my_onbytarget() are more of the same. There is some nastiness
+in my_onrecord() to handle poping up a full-record div in the right place and
+getting rid of any old ones. It doesn't work quite right.
+
+onFormSubmitEventHandler() is more interesting. This is a JS event handle (for
+form-submit, duh) but how does it know what team to work for? It checks `this'
+to see that classes the HTML element has, and so finds the relevant mkwsTeam_NAME
+class. Then it can fire newSearch() on the relevant team. But it now occurs to
+me that this too is a closure so it doesn't need to do any of that. It can just
+start the search in its own team.
+
+[Why can that simplifying change be made here but not in, say, switchView()?
+Because the former is assigned to a DOM object from within the JS code, so acts
+as a closure; but the latter is invoked by a fragment of JS text which the user
+clicks on, when there is no context.]
+
+An oddity of newSearch(): it's not defined as
+    function newSearch()
+like most of the other member functions, but
+    that.newSearch = function()
+That's because, unlike most other member function, it gets explicitly invoked
+on a team object:
+    mkws.teams[tname].newSearch(val);
+
+But in fact that won't be necessary once I fix the invoker
+(onFormSubmitEventHandler) to be aware of its own context, so that can simplify,
+too.
+
+The next interesting method is triggerSearch(). You can see that it assembles
+the pp2filter and pp2limit values for the search invocation by walking the
+array m_filters[], which is built by click on facets.
+
+That's done in a slightly clumsy way. I might make a Filters object at some
+point with some nice clean methods.
+
+BTW., more unnecessary context-jockying with the windowid parameter. I don't
+need it, I have m_teamName.
+
+loadSelect() is another fine example of a method that appears in a random
+place. It's just one of the HTML-generation helpers.
+
+Now we come to a bunch of externally invoked functions, that.limitTarget() etc.
+These are the meat that are called by the stubs in the top-level code -- remember
+those one-liners?
+
+They change state in various ways based on the user's clicks. The first four
+({un,}limit{Query,Target}()) do so by tweaking the m_filters[] array.
+
+More HTML-generation helpers: redraw_navi(), drawPager() -- note the inconsistent
+multi-word naming scheme
+
+We are *completely* schizophrenic over whether to use camelCase or
+underscore_separated
+
+Then more UI functions (that.X, that.Y)
+
+Anyway, onwards ... loadTemplate() you already know about.
+
+defaultTemplate() is the hardwired defaults, used for applications that don't
+define their own templates. For an application that does, see
+http://example.indexdata.com/lolcat.html
+
+As you can imagine, Lynn was WAY impressed by lolcat.html
+
+mkws_html_all() is a big ugly function that generates a buttload of HTML.
+Basically, it spots where you've used a magic name like <class="mkwsSearch">
+and expands it to contain the relevant HTML.
+
+Then it's just utility functions: parseQueryString() to read URL params,
+mkws_set_lang() and friends generate more HTML.
+
+All these HTML-generation functions should of course be together. Many of them
+should also use Handlebars templates, so that clever applications can redefine
+them. In places, too, they still need fixing to use CSS classes instead of inline
+markup, <b> and suchlike.
+
+that.run_auto_search() is interesting. It gets the term to search for from the
+"autosearch" attribute on the relevant element, but there are special forms for
+that string. !param!NAME gets the query from the URL parameter called NAME;
+!path!NUM gets it from the NUM'th last component of the URL path. There may be
+more of these in future.
+
+Once it's got that it's a pretty straightforward invocation of newSearch()
+
+M(string) yields the translation of string into the currently selected language.
+We use it a lot in the HTML generation, and it's one part of that process that's
+more cumbersome in Handlebars. The problem is that M() is a closure so it could
+in principle know what the language of THIS team is, and it could be different
+for different teams, even though that's not the case at the moment. But Handlebars
+helpers are set once for all time, so they can't be team-specific, which means
+they can only refer to the globally selected lan
+
+And that, really, is the end of the team object (and so of mkws.js). TA-DAH!
+
+
index d8df4e2..0d78b94 100644 (file)
@@ -1,4 +1,4 @@
 The set of targets provided by MKWS by default can be maintained using
 MKAdmin.  Go to
-       http://mk2.indexdata.com/console/
-and act as the "MK Demo" library administrator.
+       http://mkc-admin.indexdata.com/console/
+and act as the "DEMO MKWS: the MasterKey Widget Set" library administrator.
diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644 (file)
index 0000000..bb36c2d
--- /dev/null
@@ -0,0 +1,14 @@
+README.html
+README.odt
+README.pdf
+handlebars-v1.1.2.js
+jquery-1.10.0.min.js
+jquery.json-2.4.js
+mkws-complete.js
+mkws-complete.min.js
+mkws.js
+mkws.min.js
+pz2.js
+whitepaper.html
+whitepaper.odt
+whitepaper.pdf
diff --git a/src/Makefile b/src/Makefile
new file mode 100644 (file)
index 0000000..c6fb23a
--- /dev/null
@@ -0,0 +1,101 @@
+HANDLEBARS_FILE = handlebars-v1.1.2.js
+JQUERY_FILE = jquery-1.10.0.min.js
+JQUERY_JSON_FILE = jquery.json-2.4.js
+PP2_FILE = pz2.js
+
+HANDLEBARS_URL = http://builds.handlebarsjs.com.s3.amazonaws.com/${HANDLEBARS_FILE}
+JQUERY_URL = http://code.jquery.com/${JQUERY_FILE}
+JQUERY_JSON_URL = https://jquery-json.googlecode.com/files/${JQUERY_JSON_FILE}
+PP2_URL = http://git.indexdata.com/?p=pazpar2.git;a=blob_plain;f=js/${PP2_FILE};hb=HEAD
+
+JQUERY_UI_URL =        http://code.jquery.com/ui/1.10.3/jquery-ui.js
+VERSION = $(shell tr -d '\012' < VERSION)
+
+COMPONENTS = mkws-handlebars.js \
+       mkws-core.js mkws-team.js mkws-filter.js \
+       mkws-widgets.js mkws-widget-termlists.js \
+       mkws-widget-authname.js mkws-widget-categories.js mkws-widget-log.js \
+       mkws-widget-record.js mkws-widget-builder.js
+
+GENERATED = ${HANDLEBARS_FILE} ${JQUERY_FILE} ${JQUERY_JSON_FILE} ${PP2_FILE} \
+       mkws.js mkws.min.js mkws-complete.js mkws-complete.min.js
+
+INSTALLABLE = mkws-jquery.js VERSION NEWS $(GENERATED)
+
+INSTALLED = $(INSTALLABLE:%=../tools/htdocs/%)
+
+**make-default**: ../tools/htdocs/mkws.js
+
+install: $(INSTALLED)
+
+uninstall:
+       rm -f $(INSTALLED)
+
+../tools/htdocs/%: %
+       rm -f $@ && cp -p $? $@ && chmod 444 $@
+
+all: mkws.min.js mkws-complete.min.js
+
+mkws-js mkws-complete.js: Makefile mkws.js mkws-jquery.js ${HANDLEBARS_FILE} ${JQUERY_FILE} ${JQUERY_JSON_FILE} ${PP2_FILE}
+       ( set -e; \
+         echo "/*! Copyright (c) 2013-2014 IndexData ApS. http://indexdata.com"; \
+         echo "   Licence: GPL, http://www.indexdata.com/licences/gpl"; \
+         echo "   created at: $$(date)"; \
+         echo "   mkws.js GIT id: $$(git log mkws.js | head -n 1 | perl -npe 's,\S+\s+,,')"; \
+         echo "   pz2.js GIT id: $$(curl -sSf 'http://git.indexdata.com/?p=pazpar2.git;a=rss' | egrep '<guid' | head -1 | perl -ne 'print "$$1\n" if m,.*=([0-9a-f]+)</guid>,')"; \
+         echo "*/"; \
+         cat ${JQUERY_FILE}; \
+         cat ${JQUERY_JSON_FILE}; \
+         cat ${HANDLEBARS_FILE}; \
+         cat ${PP2_FILE}; \
+         cat  mkws.js; \
+         cat  mkws-jquery.js; \
+       ) > mkws-complete.js.new
+       mv -f mkws-complete.js.new mkws-complete.js
+
+%.min.js: %.js
+       yui-compressor $? > $@.new
+       mv -f $@.new $@
+
+mkws-syntax-check:
+       yui-compressor mkws.js >/dev/null
+
+${HANDLEBARS_FILE}:
+       curl -sSf ${HANDLEBARS_URL} -o $@.tmp
+       mv -f $@.tmp $@
+
+${JQUERY_FILE}:
+       curl -sSf ${JQUERY_URL} -o $@.new
+       perl -npe 's,sourceMappingURL=jquery.*map,,' $@.new > $@
+       rm -f $@.new
+
+${JQUERY_JSON_FILE}:
+       curl -sSf ${JQUERY_JSON_URL} -o $@.tmp
+       mv -f $@.tmp $@
+
+${PP2_FILE}:
+       curl -sSf "${PP2_URL}" -o $@.tmp
+       mv -f $@.tmp $@
+
+release: mkws.js mkws-complete.js mkws.min.js mkws-complete.min.js
+       @if [ -f releases/mkws-$(VERSION).js ]; then \
+               echo "*** There is already a release $(VERSION)"; \
+       else \
+               cp -p mkws.js releases/mkws-$(VERSION).js; \
+               cp -p mkws.min.js releases/mkws-$(VERSION).min.js; \
+               cp -p mkws-complete.js releases/mkws-complete-$(VERSION).js; \
+               cp -p mkws-complete.min.js releases/mkws-complete-$(VERSION).min.js; \
+               echo "Made release $(VERSION)"; \
+       fi
+
+mkws.js: $(COMPONENTS) Makefile
+       rm -f $@
+       cat $(COMPONENTS) > $@
+       chmod 444 $@
+
+distclean: clean
+       rm -f *.orig *.bak *.rej
+
+clean:
+       rm -f ${GENERATED}
+
diff --git a/src/NEWS b/src/NEWS
new file mode 100644 (file)
index 0000000..5bbb6ad
--- /dev/null
+++ b/src/NEWS
@@ -0,0 +1,15 @@
+Version history for the MasterKey Widget Set (MKWS)
+
+0.9 series
+----------
+
+The 0.9.x series of releases are beta quality. They are functional but
+have not yet been extensively battle-tested.
+
+0.9.2 [IN PROGRESS]
+       - Full-record template invokes {{translate}} on fieldnames.
+         Fixes MKWS-84 Translate fieldnames in full-record popup.
+
+0.9.1 Thu Dec 19 15:33:13 GMT 2013
+       - First public release.
+
diff --git a/src/VERSION b/src/VERSION
new file mode 100644 (file)
index 0000000..f374f66
--- /dev/null
@@ -0,0 +1 @@
+0.9.1
diff --git a/src/mkws-core.js b/src/mkws-core.js
new file mode 100644 (file)
index 0000000..0bfc174
--- /dev/null
@@ -0,0 +1,434 @@
+/*! MKWS, the MasterKey Widget Set.
+ *  Copyright (C) 2013-2014 Index Data
+ *  See the file LICENSE for details
+ */
+
+"use strict"; // HTML5: disable for log_level >= 2
+
+
+// Set up global mkws object. Contains truly global state such as SP
+// authentication, and a hash of team objects, indexed by team-name.
+//
+var mkws = {
+    authenticated: false,
+    log_level: 1, // Will be overridden from mkws.config, but
+                  // initial value allows jQuery popup to use logging.
+    teams: {},
+    widgetType2function: {},
+
+    locale_lang: {
+       "de": {
+           "Authors": "Autoren",
+           "Subjects": "Schlagw&ouml;rter",
+           "Sources": "Daten und Quellen",
+           "source": "datenquelle",
+           "Termlists": "Termlisten",
+           "Next": "Weiter",
+           "Prev": "Zur&uuml;ck",
+           "Search": "Suche",
+           "Sort by": "Sortieren nach",
+           "and show": "und zeige",
+           "per page": "pro Seite",
+           "Displaying": "Zeige",
+           "to": "von",
+           "of": "aus",
+           "found": "gefunden",
+           "Title": "Titel",
+           "Author": "Autor",
+           "author": "autor",
+           "Date": "Datum",
+           "Subject": "Schlagwort",
+           "subject": "schlagwort",
+           "Location": "Ort",
+           "Records": "Datens&auml;tze",
+           "Targets": "Datenbanken",
+
+           "dummy": "dummy"
+       },
+
+       "da": {
+           "Authors": "Forfattere",
+           "Subjects": "Emner",
+           "Sources": "Kilder",
+           "source": "kilder",
+           "Termlists": "Termlists",
+           "Next": "N&aelig;ste",
+           "Prev": "Forrige",
+           "Search": "S&oslash;g",
+           "Sort by": "Sorter efter",
+           "and show": "og vis",
+           "per page": "per side",
+           "Displaying": "Viser",
+           "to": "til",
+           "of": "ud af",
+           "found": "fandt",
+           "Title": "Title",
+           "Author": "Forfatter",
+           "author": "forfatter",
+           "Date": "Dato",
+           "Subject": "Emneord",
+           "subject": "emneord",
+           "Location": "Lokation",
+           "Records": "Poster",
+           "Targets": "Baser",
+
+           "dummy": "dummy"
+       }
+    }
+};
+
+
+mkws.log = function(string) {
+    if (!mkws.log_level)
+       return;
+
+    if (typeof console === "undefined" || typeof console.log === "undefined") { /* ARGH!!! old IE */
+       return;
+    }
+
+    // you need to disable use strict at the top of the file!!!
+    if (mkws.log_level >= 3) {
+       console.log(arguments.callee.caller);
+    } else if (mkws.log_level >= 2) {
+       console.log(">>> called from function " + arguments.callee.caller.name + ' <<<');
+    }
+    console.log(string);
+};
+
+
+// This function is taken from a StackOverflow answer
+// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript/901144#901144
+mkws.getParameterByName = function(name) {
+    name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
+    var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
+       results = regex.exec(location.search);
+    return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
+}
+
+
+mkws.registerWidgetType = function(name, fn) {
+    mkws.widgetType2function[name] = fn;
+    mkws.log("registered widget-type '" + name + "'");
+};
+
+mkws.promotionFunction = function(name) {
+    return mkws.widgetType2function[name];
+};
+
+
+mkws.setMkwsConfig = function(overrides) {
+    // Set global log_level flag early so that mkws.log() works
+    // Fall back to old "debug_level" setting for backwards compatibility
+    var tmp = overrides.log_level;
+    if (typeof(tmp) === 'undefined') tmp = overrides.debug_level;
+    if (typeof(tmp) !== 'undefined') mkws.log_level = tmp;
+
+    var config_default = {
+       use_service_proxy: true,
+       pazpar2_url: "//mkws.indexdata.com/service-proxy/",
+       service_proxy_auth: "//mkws.indexdata.com/service-proxy-auth",
+       lang: "",
+       sort_options: [["relevance"], ["title:1", "title"], ["date:0", "newest"], ["date:1", "oldest"]],
+       perpage_options: [10, 20, 30, 50],
+       sort_default: "relevance",
+       perpage_default: 20,
+       query_width: 50,
+       show_lang: true,        /* show/hide language menu */
+       show_sort: true,        /* show/hide sort menu */
+       show_perpage: true,     /* show/hide perpage menu */
+       lang_options: [],       /* display languages links for given languages, [] for all */
+       facets: ["xtargets", "subject", "author"], /* display facets, in this order, [] for none */
+       responsive_design_width: undefined, /* a page with less pixel width considered as narrow */
+       log_level: 1,     /* log level for development: 0..2 */
+
+       dummy: "dummy"
+    };
+
+    mkws.config = mkws.objectInheritingFrom(config_default);
+    for (var k in overrides) {
+       mkws.config[k] = overrides[k];
+    }
+};
+
+
+// This code is from Douglas Crockford's article "Prototypal Inheritance in JavaScript"
+// http://javascript.crockford.com/prototypal.html
+// mkws.objectInheritingFrom behaves the same as Object.create,
+// but since the latter is not available in IE8 we can't use it.
+//
+mkws.objectInheritingFrom = function(o) {
+    function F() {}
+    F.prototype = o;
+    return new F();
+}
+
+
+// The following functions are dispatchers for team methods that
+// are called from the UI using a team-name rather than implicit
+// context.
+mkws.switchView = function(tname, view) {
+    mkws.teams[tname].switchView(view);
+};
+
+mkws.showDetails = function(tname, prefixRecId) {
+    mkws.teams[tname].showDetails(prefixRecId);
+};
+
+mkws.limitTarget  = function(tname, id, name) {
+    mkws.teams[tname].limitTarget(id, name);
+};
+
+mkws.limitQuery  = function(tname, field, value) {
+    mkws.teams[tname].limitQuery(field, value);
+};
+
+mkws.limitCategory  = function(tname, id) {
+    mkws.teams[tname].limitCategory(id);
+};
+
+mkws.delimitTarget = function(tname, id) {
+    mkws.teams[tname].delimitTarget(id);
+};
+
+mkws.delimitQuery = function(tname, field, value) {
+    mkws.teams[tname].delimitQuery(field, value);
+};
+
+mkws.showPage = function(tname, pageNum) {
+    mkws.teams[tname].showPage(pageNum);
+};
+
+mkws.pagerPrev = function(tname) {
+    mkws.teams[tname].pagerPrev();
+};
+
+mkws.pagerNext = function(tname) {
+    mkws.teams[tname].pagerNext();
+};
+
+
+// wrapper to call team() after page load
+(function(j) {
+    var log = mkws.log;
+
+    function handleNodeWithTeam(node, callback) {
+       // First branch for DOM objects; second branch for jQuery objects
+       var classes = node.className || node.attr('class');
+       if (!classes) {
+           // For some reason, if we try to proceed when classes is
+           // undefined, we don't get an error message, but this
+           // function and its callers, up several stack level,
+           // silently return. What a crock.
+           log("handleNodeWithTeam() called on node with no classes");
+           return;
+       }
+       var list = classes.split(/\s+/)
+       var teamName, type;
+
+       for (var i = 0; i < list.length; i++) {
+           var cname = list[i];
+           if (cname.match(/^mkwsTeam_/)) {
+               teamName = cname.replace(/^mkwsTeam_/, '');
+           } else if (cname.match(/^mkws/)) {
+               type = cname.replace(/^mkws/, '');
+           }
+       }
+
+        if (!teamName) teamName = "AUTO";
+       callback.call(node, teamName, type);
+    }
+
+
+    function resizePage() {
+       var list = ["mkwsSwitch", "mkwsLang"];
+
+       var width = mkws.config.responsive_design_width;
+       var parent = $(".mkwsTermlists").parent();
+
+       if ($(window).width() <= width &&
+           parent.hasClass("mkwsTermlistContainer1")) {
+           log("changing from wide to narrow: " + $(window).width());
+           $(".mkwsTermlistContainer1").hide();
+           $(".mkwsTermlistContainer2").show();
+           for (var tname in mkws.teams) {
+               $(".mkwsTermlists.mkwsTeam_" + tname).appendTo($(".mkwsTermlistContainer2.mkwsTeam_" + tname));
+               for(var i = 0; i < list.length; i++) {
+                   $("." + list[i] + ".mkwsTeam_" + tname).hide();
+               }
+           }
+       } else if ($(window).width() > width &&
+                  parent.hasClass("mkwsTermlistContainer2")) {
+           log("changing from narrow to wide: " + $(window).width());
+           $(".mkwsTermlistContainer1").show();
+           $(".mkwsTermlistContainer2").hide();
+           for (var tname in mkws.teams) {
+               $(".mkwsTermlists.mkwsTeam_" + tname).appendTo($(".mkwsTermlistContainer1.mkwsTeam_" + tname));
+               for(var i = 0; i < list.length; i++) {
+                   $("." + list[i] + ".mkwsTeam_" + tname).show();
+               }
+           }
+       }
+    };
+
+
+    /*
+     * Run service-proxy authentication in background (after page load).
+     * The username/password is configured in the apache config file
+     * for the site.
+     */
+    function authenticateSession(auth_url, auth_domain, pp2_url) {
+       log("Run service proxy auth URL: " + auth_url);
+
+       if (!auth_domain) {
+           auth_domain = pp2_url.replace(/^(https?:)?\/\/(.*?)\/.*/, '$2');
+           log("guessed auth_domain '" + auth_domain + "' from pp2_url '" + pp2_url + "'");
+       }
+
+       var request = new pzHttpRequest(auth_url, function(err) {
+           alert("HTTP call for authentication failed: " + err)
+           return;
+       }, auth_domain);
+
+       request.get(null, function(data) {
+           if (!$.isXMLDoc(data)) {
+               alert("service proxy auth response document is not valid XML document, give up!");
+               return;
+           }
+           var status = $(data).find("status");
+           if (status.text() != "OK") {
+               alert("service proxy auth response status: " + status.text() + ", give up!");
+               return;
+           }
+
+           log("Service proxy auth successfully done");
+           mkws.authenticated = true;
+           var authName = $(data).find("displayName").text();
+           // You'd think there would be a better way to do this:
+           var realm = $(data).find("realm:not(realmAttributes realm)").text();
+           for (var teamName in mkws.teams) {
+               mkws.teams[teamName].queue("authenticated").publish(authName, realm);
+           }
+
+           runAutoSearches();
+       });
+    }
+
+
+    function runAutoSearches() {
+       log("running auto searches");
+
+       for (var teamName in mkws.teams) {
+           mkws.teams[teamName].queue("ready").publish();
+       }
+    }
+
+
+    function makeWidgetsWithin(level, node) {
+        node.find('[class^="mkws"],[class*=" mkws"]').each(function() {
+            handleNodeWithTeam(this, function(tname, type) {
+                var oldHTML = this.innerHTML;
+                var myTeam = mkws.teams[tname];
+                var myWidget = widget(j, myTeam, type, this);
+                myTeam.addWidget(myWidget);
+                var newHTML = this.innerHTML;
+                if (newHTML !== oldHTML) {
+                    log("widget " + tname + ":" + type + " HTML changed from '" + oldHTML + "' to '" + newHTML + "': reparse!");
+                    makeWidgetsWithin(level+1, $(this));
+                }
+            });
+        });
+    }
+
+
+    $(document).ready(function() {
+       var saved_config;
+       if (typeof mkws_config === 'undefined') {
+           log("setting empty config");
+           saved_config = {};
+       } else {
+           log("using config: " + $.toJSON(mkws_config));
+           saved_config = mkws_config;
+       }
+       mkws.setMkwsConfig(saved_config);
+
+       for (var key in mkws.config) {
+           if (mkws.config.hasOwnProperty(key)) {
+               if (key.match(/^language_/)) {
+                   var lang = key.replace(/^language_/, "");
+                   // Copy custom languages into list
+                   mkws.locale_lang[lang] = mkws.config[key];
+                   log("Added locally configured language '" + lang + "'");
+               }
+           }
+       }
+
+       if (mkws.config.query_width < 5 || mkws.config.query_width > 150) {
+           log("Reset query width: " + mkws.config.query_width);
+           mkws.config.query_width = 50;
+       }
+
+       // protocol independent link for pazpar2: "//mkws/sp" -> "https://mkws/sp"
+       if (mkws.config.pazpar2_url.match(/^\/\//)) {
+           mkws.config.pazpar2_url = document.location.protocol + mkws.config.pazpar2_url;
+           log("adjust protocol independent links: " + mkws.config.pazpar2_url);
+       }
+
+       if (mkws.config.responsive_design_width) {
+           // Responsive web design - change layout on the fly based on
+           // current screen width. Required for mobile devices.
+           $(window).resize(resizePage);
+           // initial check after page load
+           $(document).ready(resizePage);
+       }
+
+       // Backwards compatibility: set new magic class names on any
+       // elements that have the old magic IDs.
+       var ids = [ "Switch", "Lang", "Search", "Pager", "Navi",
+                   "Results", "Records", "Targets", "Ranking",
+                   "Termlists", "Stat", "MOTD" ];
+       for (var i = 0; i < ids.length; i++) {
+           var id = 'mkws' + ids[i];
+           var node = $('#' + id);
+           if (node.attr('id')) {
+               node.addClass(id);
+               log("added magic class to '" + node.attr('id') + "'");
+           }
+       }
+
+       // Find all nodes with an MKWS class, and determine their team from
+       // the mkwsTeam_* class. Make all team objects.
+       var then = $.now();
+       $('[class^="mkws"],[class*=" mkws"]').each(function() {
+           handleNodeWithTeam(this, function(tname, type) {
+               if (!mkws.teams[tname]) {
+                   mkws.teams[tname] = team(j, tname);
+                   log("Made MKWS team '" + tname + "'");
+               }
+           });
+       });
+
+        makeWidgetsWithin(1, $(':root'));
+        
+       var now = $.now();
+       log("Walking MKWS nodes took " + (now-then) + " ms");
+
+//        for (var tName in mkws.teams) {
+//            var myTeam = mkws.teams[tName]
+//            var types = myTeam.widgetTypes();
+//            log("TEAM '" + tName + "' = " + myTeam + " has widget types " + types);
+//            for (var i = 0; i < types.length; i++) {
+//                var type = types[i];
+//                log("  has widget of type '" + type + "': " + myTeam.widget(type));
+//            }
+//        }
+
+       if (mkws.config.use_service_proxy) {
+           authenticateSession(mkws.config.service_proxy_auth,
+                               mkws.config.service_proxy_auth_domain,
+                               mkws.config.pazpar2_url);
+       } else {
+           // raw pp2
+           runAutoSearches();
+       }
+    });
+})(jQuery);
diff --git a/src/mkws-filter.js b/src/mkws-filter.js
new file mode 100644 (file)
index 0000000..2266993
--- /dev/null
@@ -0,0 +1,129 @@
+// Factory function for sets of filters.
+function filterSet(team) {
+    var m_team = team;
+    var m_list = [];
+
+    var that = {};
+
+    that.toJSON = function() {
+       return $.toJSON(m_list);
+    };
+
+    that.add = function(filter) {
+       m_list.push(filter);
+    };
+
+    that.visitTargets = function(callback) {
+       for (var i in m_list) {
+           var filter = m_list[i];
+           if (filter.type === 'target') {
+               callback(filter.id, filter.name);
+           }
+       }
+    };
+
+    that.visitFields = function(callback) {
+       for (var i in m_list) {
+           var filter = m_list[i];
+           if (filter.type === 'field') {
+               callback(filter.field, filter.value);
+           }
+       }
+    };
+
+    that.visitCategories = function(callback) {
+       for (var i in m_list) {
+           var filter = m_list[i];
+           if (filter.type === 'category') {
+               callback(filter.id);
+           }
+       }
+    };
+
+    that.removeMatching = function(matchFn) {
+       var newList = [];
+       for (var i in m_list) {
+           var filter = m_list[i];
+           if (matchFn(filter)) {
+               m_team.log("removeMatching() removing filter " + $.toJSON(filter));
+           } else {
+               m_team.log("removeMatching() keeping filter " + $.toJSON(filter));
+               newList.push(filter);
+           }
+       }
+       m_list = newList;
+    };
+
+    that.targetFiltered = function(id) {
+       for (var i = 0; i < m_list.length; i++) {
+           if (m_list[i].type === 'target' ||
+               m_list[i].id === 'pz:id=' + id) {
+               return true;
+           }
+       }
+       return false;
+    };
+
+    that.pp2filter = function() {
+       var res = "";
+
+       that.visitTargets(function(id, name) {
+           if (res) res += ",";
+           if (id.match(/^[a-z:]+[=~]/)) {
+               m_team.log("filter '" + id + "' already begins with SETTING OP");
+           } else {
+               id = 'pz:id=' + id;
+           }
+           res += id;
+       });
+
+       return res;
+    };
+
+    that.pp2limit = function(initial) {
+       var res = initial || "";
+
+       that.visitFields(function(field, value) {
+           if (res) res += ",";
+           res += field + "=" + value.replace(/[\\|,]/g, '\\$&');
+       });
+       return res;
+    }
+
+    that.pp2catLimit = function() {
+       var res = "";
+
+       that.visitCategories(function(id) {
+           if (res) res += ",";
+           res += "category~" + id.replace(/[\\|,]/g, '\\$&');
+       });
+       return res;
+    }
+
+    return that;
+}
+
+
+// Factory functions for filters. These can be of several types.
+function targetFilter(id, name) {
+    return {
+        type: 'target',
+        id: id,
+        name: name
+    };
+}
+
+function fieldFilter(field, value) {
+    return {
+        type: 'field',
+        field: field,
+        value: value
+    };
+}
+
+function categoryFilter(id) {
+    return {
+        type: 'category',
+        id: id,
+    };
+}
diff --git a/src/mkws-handlebars.js b/src/mkws-handlebars.js
new file mode 100644 (file)
index 0000000..1fabf3c
--- /dev/null
@@ -0,0 +1,62 @@
+// Handlebars helpers
+Handlebars.registerHelper('json', function(obj) {
+    return $.toJSON(obj);
+});
+
+
+Handlebars.registerHelper('translate', function(s) {
+    return mkws.M(s);
+});
+
+
+// We need {{attr '@name'}} because Handlebars can't parse {{@name}}
+Handlebars.registerHelper('attr', function(attrName) {
+    return this[attrName];
+});
+
+
+/*
+ * Use as follows: {{#if-any NAME1 having="NAME2"}}
+ * Applicable when NAME1 is the name of an array
+ * The guarded code runs only if at least one element of the NAME1
+ * array has a subelement called NAME2.
+ */
+Handlebars.registerHelper('if-any', function(items, options) {
+    var having = options.hash.having;
+    for (var i in items) {
+       var item = items[i]
+       if (!having || item[having]) {
+           return options.fn(this);
+       }
+    }
+    return "";
+});
+
+
+Handlebars.registerHelper('first', function(items, options) {
+    var having = options.hash.having;
+    for (var i in items) {
+       var item = items[i]
+       if (!having || item[having]) {
+           return options.fn(item);
+       }
+    }
+    return "";
+});
+
+
+Handlebars.registerHelper('commaList', function(items, options) {
+    var out = "";
+
+    for (var i in items) {
+       if (i > 0) out += ", ";
+       out += options.fn(items[i])
+    }
+
+    return out;
+});
+
+
+Handlebars.registerHelper('index1', function(obj) {
+    return obj.data.index + 1;
+});
diff --git a/src/mkws-jquery.js b/src/mkws-jquery.js
new file mode 100644 (file)
index 0000000..2f79db4
--- /dev/null
@@ -0,0 +1,148 @@
+/*! jQuery plugin for MKWS, the MasterKey Widget Set.
+ *  Copyright (C) 2013-2014 Index Data
+ *  See the file LICENSE for details
+ */
+
+"use strict";
+
+
+/*
+ * implement jQuery plugin $.pazpar2({})
+ */
+function _mkws_jquery_plugin($) {
+    function debug (string) {
+       mkws.log("jquery.pazpar2: " + string);
+    }
+
+    function init_popup(obj) {
+       var config = obj ? obj : {};
+
+       var height = config.height || 760;
+       var width = config.width || 880;
+       var id_button = config.id_button || "input.mkwsButton";
+       var id_popup = config.id_popup || ".mkwsPopup";
+
+       debug("popup height: " + height + ", width: " + width);
+
+       // make sure that jquery-ui was loaded afte jQuery core lib, e.g.:
+       // <script src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
+       if (!$.ui) {
+           debug("Error: jquery-ui.js is missing, did you include it after jQuery core in the HTML file?");
+           return;
+       }
+
+       $(id_popup).dialog({
+           closeOnEscape: true,
+           autoOpen: false,
+           height: height,
+           width: width,
+           modal: true,
+           resizable: true,
+           buttons: {
+               Cancel: function() {
+                   $(this).dialog("close");
+               }
+           },
+           close: function() { }
+       });
+
+       $(id_button)
+           .button()
+           .click(function() {
+               $(id_popup).dialog("open");
+           });
+    };
+
+    $.extend({
+
+       // service-proxy or pazpar2
+       pazpar2: function(config) {
+           if (config == null || typeof config != 'object') {
+               config = {};
+           }
+           var id_popup = config.id_popup || ".mkwsPopup";
+           id_popup = id_popup.replace(/^[#\.]/, "");
+
+           // simple layout
+           var div = '\
+<div class="mkwsSwitch"></div>\
+<div class="mkwsLang"></div>\
+<div class="mkwsSearch"></div>\
+<div class="mkwsResults"></div>\
+<div class="mkwsTargets"></div>\
+<div class="mkwsStat"></div>';
+
+           // new table layout
+           var table = '\
+<style type="text/css">\
+  .mkwsTermlists div.facet {\
+  float:left;\
+  width: 30%;\
+  margin: 0.3em;\
+  }\
+  .mkwsStat {\
+  text-align: right;\
+  }\
+</style>\
+    \
+<table width="100%" border="0">\
+  <tr>\
+    <td>\
+      <div class="mkwsSwitch"></div>\
+      <div class="mkwsLang"></div>\
+      <div class="mkwsSearch"></div>\
+    </td>\
+  </tr>\
+  <tr>\
+    <td>\
+      <div style="height:500px; overflow: auto">\
+       <div class="mkwsPager"></div>\
+       <div class="mkwsNavi"></div>\
+       <div class="mkwsRecords"></div>\
+       <div class="mkwsTargets"></div>\
+       <div class="mkwsRanking"></div>\
+      </div>\
+    </td>\
+  </tr>\
+  <tr>\
+    <td>\
+      <div style="height:300px; overflow: hidden">\
+       <div class="mkwsTermlists"></div>\
+      </div>\
+    </td>\
+  </tr>\
+  <tr>\
+    <td>\
+      <div class="mkwsStat"></div>\
+    </td>\
+  </tr>\
+</table>';
+
+           var popup = '\
+<div class="mkwsSearch"></div>\
+<div class="' + id_popup + '">\
+  <div class="mkwsSwitch"></div>\
+  <div class="mkwsLang"></div>\
+  <div class="mkwsResults"></div>\
+  <div class="mkwsTargets"></div>\
+  <div class="mkwsStat"></div>\
+</div>'
+
+           if (config && config.layout == 'div') {
+               debug("jquery plugin layout: div");
+               document.write(div);
+           } else if (config && config.layout == 'popup') {
+               debug("jquery plugin layout: popup with id: " + id_popup);
+               document.write(popup);
+               $(document).ready(function() { init_popup(config); });
+           } else {
+               debug("jquery plugin layout: table");
+               document.write(table);
+           }
+       }
+    });
+};
+
+
+// enable before page load, so we could call it before mkws() runs
+_mkws_jquery_plugin(jQuery);
diff --git a/src/mkws-team.js b/src/mkws-team.js
new file mode 100644 (file)
index 0000000..8f4235e
--- /dev/null
@@ -0,0 +1,737 @@
+// Factory function for team objects. As much as possible, this uses
+// only member variables (prefixed "m_") and inner functions with
+// private scope.
+//
+// Some functions are visible as member-functions to be called from
+// outside code -- specifically, from generated HTML. These functions
+// are that.switchView(), showDetails(), limitTarget(), limitQuery(),
+// limitCategory(), delimitTarget(), delimitQuery(), showPage(),
+// pagerPrev(), pagerNext().
+//
+function team($, teamName) {
+    var that = {};
+    var m_teamName = teamName;
+    var m_submitted = false;
+    var m_query; // initially undefined
+    var m_sortOrder; // will be set below
+    var m_perpage; // will be set below
+    var m_filterSet = filterSet(that);
+    var m_totalRecordCount = 0;
+    var m_currentPage = 1;
+    var m_currentRecordId = '';
+    var m_currentRecordData = null;
+    var m_logTime = {
+       // Timestamps for logging
+       "start": $.now(),
+       "last": $.now()
+    };
+    var m_paz; // will be initialised below
+    var m_tempateText = {}; // widgets can register tempates to be compiled
+    var m_template = {}; // compiled templates, from any source
+    var m_config = mkws.objectInheritingFrom(mkws.config);
+    var m_widgets = {}; // Maps widget-type to object
+
+    that.toString = function() { return '[Team ' + teamName + ']'; };
+
+    // Accessor methods for individual widgets: readers
+    that.name = function() { return m_teamName; };
+    that.submitted = function() { return m_submitted; };
+    that.perpage = function() { return m_perpage; };
+    that.totalRecordCount = function() { return m_totalRecordCount; };
+    that.currentPage = function() { return m_currentPage; };
+    that.currentRecordId = function() { return m_currentRecordId; };
+    that.currentRecordData = function() { return m_currentRecordData; };
+    that.filters = function() { return m_filterSet; };
+    that.config = function() { return m_config; };
+
+    // Accessor methods for individual widgets: writers
+    that.set_sortOrder = function(val) { m_sortOrder = val };
+    that.set_perpage = function(val) { m_perpage = val };
+
+
+    // The following PubSub code is modified from the jQuery manual:
+    // http://api.jquery.com/jQuery.Callbacks/
+    //
+    // Use as:
+    // team.queue("eventName").subscribe(function(param1, param2 ...) { ... });
+    // team.queue("eventName").publish(arg1, arg2, ...);
+    //
+    var queues = {};
+    function queue(id) {
+       if (!queues[id]) {
+           var callbacks = $.Callbacks();
+           queues[id] = {
+               publish: callbacks.fire,
+               subscribe: callbacks.add,
+               unsubscribe: callbacks.remove
+           };
+       }
+       return queues[id];
+    };
+    that.queue = queue;
+
+
+    function log(s) {
+       var now = $.now();
+       var timestamp = (((now - m_logTime.start)/1000).toFixed(3) + " (+" +
+                        ((now - m_logTime.last)/1000).toFixed(3) + ") ");
+       m_logTime.last = now;
+       mkws.log(m_teamName + ": " + timestamp + s);
+       that.queue("log").publish(m_teamName, timestamp, s);
+    }
+    that.log = log;
+
+
+    log("start running MKWS");
+
+    m_sortOrder = m_config.sort_default;
+    m_perpage = m_config.perpage_default;
+
+    log("Create main pz2 object");
+    // create a parameters array and pass it to the pz2's constructor
+    // then register the form submit event with the pz2.search function
+    // autoInit is set to true on default
+    m_paz = new pz2({ "windowid": teamName,
+                     "pazpar2path": m_config.pazpar2_url,
+                     "usesessions" : m_config.use_service_proxy ? false : true,
+                     "oninit": onInit,
+                     "onbytarget": onBytarget,
+                     "onstat": onStat,
+                     "onterm": (m_config.facets.length ? onTerm : undefined),
+                     "onshow": onShow,
+                     "onrecord": onRecord,
+                     "showtime": 500,            //each timer (show, stat, term, bytarget) can be specified this way
+                     "termlist": m_config.facets.join(',')
+                   });
+
+    // pz2.js event handlers:
+    function onInit() {
+       log("init");
+       m_paz.stat();
+       m_paz.bytarget();
+    }
+
+    function onBytarget(data) {
+       log("target");
+       queue("targets").publish(data);
+    }
+
+    function onStat(data) {
+       queue("stat").publish(data);
+       if (parseInt(data.activeclients[0], 10) === 0)
+           queue("complete").publish(parseInt(data.hits[0], 10));
+    }
+
+    function onTerm(data) {
+       log("term");
+       queue("termlists").publish(data);
+    }
+
+    function onShow(data, teamName) {
+       log("show");
+       m_totalRecordCount = data.merged;
+       log("found " + m_totalRecordCount + " records");
+       queue("pager").publish(data);
+       queue("records").publish(data);
+    }
+
+    function onRecord(data, args, teamName) {
+       log("record");
+       // FIXME: record is async!!
+       clearTimeout(m_paz.recordTimer);
+       var detRecordDiv = findnode(recordDetailsId(data.recid[0]));
+       if (detRecordDiv.length) {
+           // in case on_show was faster to redraw element
+           return;
+       }
+       m_currentRecordData = data;
+       var recordDiv = findnode('.' + recordElementId(m_currentRecordData.recid[0]));
+       var html = renderDetails(m_currentRecordData);
+       $(recordDiv).append(html);
+    }
+
+
+    // Used by the Records widget and onRecord()
+    function recordElementId(s) {
+       return 'mkwsRec_' + s.replace(/[^a-z0-9]/ig, '_');
+    }
+    that.recordElementId = recordElementId;
+
+    // Used by onRecord(), showDetails() and renderDetails()
+    function recordDetailsId(s) {
+       return 'mkwsDet_' + s.replace(/[^a-z0-9]/ig, '_');
+    }
+
+
+    that.targetFiltered = function(id) {
+       return m_filterSet.targetFiltered(id);
+    };
+
+
+    that.limitTarget = function(id, name) {
+       log("limitTarget(id=" + id + ", name=" + name + ")");
+       m_filterSet.add(targetFilter(id, name));
+       if (m_query) triggerSearch();
+       return false;
+    };
+
+
+    that.limitQuery = function(field, value) {
+       log("limitQuery(field=" + field + ", value=" + value + ")");
+       m_filterSet.add(fieldFilter(field, value));
+        if (m_query) triggerSearch();
+       return false;
+    };
+
+
+    that.limitCategory = function(id) {
+       log("limitCategory(id=" + id + ")");
+        // Only one category filter at a time
+       m_filterSet.removeMatching(function(f) { return f.type === 'category' });
+        if (id !== '') m_filterSet.add(categoryFilter(id));
+        if (m_query) triggerSearch();
+       return false;
+    };
+
+
+    that.delimitTarget = function(id) {
+       log("delimitTarget(id=" + id + ")");
+       m_filterSet.removeMatching(function(f) { return f.type === 'target' });
+        if (m_query) triggerSearch();
+       return false;
+    };
+
+
+    that.delimitQuery = function(field, value) {
+       log("delimitQuery(field=" + field + ", value=" + value + ")");
+       m_filterSet.removeMatching(function(f) { return f.type == 'field' &&
+                                                 field == f.field && value == f.value });
+        if (m_query) triggerSearch();
+       return false;
+    };
+
+
+    that.showPage = function(pageNum) {
+       m_currentPage = pageNum;
+       m_paz.showPage(m_currentPage - 1);
+    };
+
+
+    that.pagerNext = function() {
+       if (m_totalRecordCount - m_perpage*m_currentPage > 0) {
+            m_paz.showNext();
+            m_currentPage++;
+       }
+    };
+
+
+    that.pagerPrev = function() {
+       if (m_paz.showPrev() != false)
+            m_currentPage--;
+    };
+
+
+    that.reShow = function() {
+       resetPage();
+       m_paz.show(0, m_perpage, m_sortOrder);
+    };
+
+
+    function resetPage() {
+       m_currentPage = 1;
+       m_totalRecordCount = 0;
+    }
+    that.resetPage = resetPage;
+
+
+    function newSearch(query, sortOrder, maxrecs, perpage, limit, targets, torusquery) {
+       log("newSearch: " + query);
+
+       if (m_config.use_service_proxy && !mkws.authenticated) {
+           alert("searching before authentication");
+           return;
+       }
+
+       m_filterSet.removeMatching(function(f) { return f.type !== 'category' });
+       triggerSearch(query, sortOrder, maxrecs, perpage, limit, targets, torusquery);
+       switchView('records'); // In case it's configured to start off as hidden
+       m_submitted = true;
+    }
+    that.newSearch = newSearch;
+
+
+    function triggerSearch(query, sortOrder, maxrecs, perpage, limit, targets, torusquery) {
+       resetPage();
+       queue("navi").publish();
+
+       // Continue to use previous query/sort-order unless new ones are specified
+       if (query) m_query = query;
+       if (sortOrder) m_sortOrder = sortOrder;
+       if (perpage) m_perpage = perpage;
+       if (targets) m_filterSet.add(targetFilter(targets, targets));
+
+       var pp2filter = m_filterSet.pp2filter();
+       var pp2limit = m_filterSet.pp2limit(limit);
+        var pp2catLimit = m_filterSet.pp2catLimit();
+       if (pp2catLimit) {
+            pp2filter = pp2filter ? pp2filter + "," + pp2catLimit : pp2catLimit;
+        }
+
+       var params = {};
+       if (pp2limit) params.limit = pp2limit;
+       if (maxrecs) params.maxrecs = maxrecs;
+       if (torusquery) {
+           if (!mkws.config.use_service_proxy)
+               alert("can't narrow search by torusquery when Service Proxy is not in use");
+           params.torusquery = torusquery;
+       }
+
+       log("triggerSearch(" + m_query + "): filters = " + m_filterSet.toJSON() + ", " +
+           "pp2filter = " + pp2filter + ", params = " + $.toJSON(params));
+
+       m_paz.search(m_query, m_perpage, m_sortOrder, pp2filter, undefined, params);
+    }
+
+
+    // switching view between targets and records
+    function switchView(view) {
+       var targets = widgetNode('Targets');
+       var results = widgetNode('Results') || widgetNode('Records');
+       var blanket = widgetNode('Blanket');
+       var motd    = widgetNode('MOTD');
+
+       switch(view) {
+        case 'targets':
+            if (targets) targets.css('display', 'block');
+            if (results) results.css('display', 'none');
+            if (blanket) blanket.css('display', 'none');
+            if (motd) motd.css('display', 'none');
+            break;
+        case 'records':
+            if (targets) targets.css('display', 'none');
+            if (results) results.css('display', 'block');
+            if (blanket) blanket.css('display', 'block');
+            if (motd) motd.css('display', 'none');
+            break;
+       case 'none':
+           alert("mkws.switchView(" + m_teamName + ", 'none') shouldn't happen");
+            if (targets) targets.css('display', 'none');
+            if (results) results.css('display', 'none');
+            if (blanket) blanket.css('display', 'none');
+            if (motd) motd.css('display', 'none');
+            break;
+        default:
+            alert("Unknown view '" + view + "'");
+       }
+    }
+    that.switchView = switchView;
+
+
+    // detailed record drawing
+    that.showDetails = function(recId) {
+       var oldRecordId = m_currentRecordId;
+       m_currentRecordId = recId;
+
+       // remove current detailed view if any
+       findnode('#' + recordDetailsId(oldRecordId)).remove();
+
+       // if the same clicked, just hide
+       if (recId == oldRecordId) {
+            m_currentRecordId = '';
+            m_currentRecordData = null;
+            return;
+       }
+       // request the record
+       log("showDetails() requesting record '" + recId + "'");
+       m_paz.record(recId);
+    };
+
+
+    /*
+     * All the HTML stuff to render the search forms and
+     * result pages.
+     */
+    function mkwsHtmlAll() {
+       mkwsSetLang();
+       if (m_config.show_lang)
+           mkwsHtmlLang();
+
+       log("HTML records");
+       // If the team has a .mkwsResults, populate it in the usual
+       // way. If not, assume that it's a smarter application that
+       // defines its own subcomponents, some or all of the
+       // following:
+       //      .mkwsTermlists
+       //      .mkwsRanking
+       //      .mkwsPager
+       //      .mkwsNavi
+       //      .mkwsRecords
+       findnode(".mkwsResults").html('\
+<table width="100%" border="0" cellpadding="6" cellspacing="0">\
+  <tr>\
+    <td class="mkwsTermlistContainer1 mkwsTeam_' + m_teamName + '" width="250" valign="top">\
+      <div class="mkwsTermlists mkwsTeam_' + m_teamName + '"></div>\
+    </td>\
+    <td class="mkwsMOTDContainer mkwsTeam_' + m_teamName + '" valign="top">\
+      <div class="mkwsRanking mkwsTeam_' + m_teamName + '"></div>\
+      <div class="mkwsPager mkwsTeam_' + m_teamName + '"></div>\
+      <div class="mkwsNavi mkwsTeam_' + m_teamName + '"></div>\
+      <div class="mkwsRecords mkwsTeam_' + m_teamName + '"></div>\
+    </td>\
+  </tr>\
+  <tr>\
+    <td colspan="2">\
+      <div class="mkwsTermlistContainer2 mkwsTeam_' + m_teamName + '"></div>\
+    </td>\
+  </tr>\
+</table>');
+
+       var acc = [];
+       var facets = m_config.facets;
+       acc.push('<div class="title">' + M('Termlists') + '</div>');
+       for (var i = 0; i < facets.length; i++) {
+           acc.push('<div class="mkwsFacet mkwsTeam_' + m_teamName + '" data-mkws-facet="' + facets[i] + '">');
+           acc.push('</div>');
+       }
+       findnode(".mkwsTermlists").html(acc.join(''));
+
+       var ranking_data = '<form name="mkwsSelect" class="mkwsSelect mkwsTeam_' + m_teamName + '" action="" >';
+       if (m_config.show_sort) {
+           ranking_data +=  M('Sort by') + ' ' + mkwsHtmlSort() + ' ';
+       }
+       if (m_config.show_perpage) {
+           ranking_data += M('and show') + ' ' + mkwsHtmlPerpage() + ' ' + M('per page') + '.';
+       }
+        ranking_data += '</form>';
+       findnode(".mkwsRanking").html(ranking_data);
+
+       // on first page, hide the termlist
+       $(document).ready(function() {
+            var t = widgetNode("Termlists");
+            if (t) t.hide();
+        });
+        var container = findnode(".mkwsMOTDContainer");
+       if (container.length) {
+           // Move the MOTD from the provided element down into the container
+           findnode(".mkwsMOTD").appendTo(container);
+       }
+    }
+
+
+    function mkwsSetLang()  {
+       var lang = mkws.getParameterByName("lang") || m_config.lang;
+       if (!lang || !mkws.locale_lang[lang]) {
+           m_config.lang = ""
+       } else {
+           m_config.lang = lang;
+       }
+
+       log("Locale language: " + (m_config.lang ? m_config.lang : "none"));
+       return m_config.lang;
+    }
+
+    // set or re-set "lang" URL parameter
+    function lang_url(lang) {
+       var query = location.search;
+       // no query parameters? done
+       if (!query) {
+           return "?lang=" + lang;
+       }
+
+       // parameter does not exists
+       if (!query.match(/[\?&]lang=/)) {
+            return query + "&lang=" + lang;
+        }
+
+       // replace existing parameter
+       query = query.replace(/\?lang=([^&#;]*)/, "?lang=" + lang);
+       query = query.replace(/\&lang=([^&#;]*)/, "&lang=" + lang);
+
+       return query;
+    }
+   
+       // dynamic URL or static page? /path/foo?query=test
+    /* create locale language menu */
+    function mkwsHtmlLang() {
+       var lang_default = "en";
+       var lang = m_config.lang || lang_default;
+       var list = [];
+
+       /* display a list of configured languages, or all */
+       var lang_options = m_config.lang_options || [];
+       var toBeIncluded = {};
+       for (var i = 0; i < lang_options.length; i++) {
+           toBeIncluded[lang_options[i]] = true;
+       }
+
+       for (var k in mkws.locale_lang) {
+           if (toBeIncluded[k] || lang_options.length == 0)
+               list.push(k);
+       }
+
+       // add english link
+       if (lang_options.length == 0 || toBeIncluded[lang_default])
+            list.push(lang_default);
+
+       log("Language menu for: " + list.join(", "));
+
+       /* the HTML part */
+       var data = "";
+       for(var i = 0; i < list.length; i++) {
+           var l = list[i];
+
+           if (data)
+               data += ' | ';
+
+           if (lang == l) {
+               data += ' <span>' + l + '</span> ';
+           } else {
+               data += ' <a href="' + lang_url(l) + '">' + l + '</a> '
+           }
+       }
+
+       findnode(".mkwsLang").html(data);
+    }
+
+
+    function mkwsHtmlSort() {
+       log("HTML sort, m_sortOrder = '" + m_sortOrder + "'");
+       var sort_html = '<select class="mkwsSort mkwsTeam_' + m_teamName + '">';
+
+       for(var i = 0; i < m_config.sort_options.length; i++) {
+           var opt = m_config.sort_options[i];
+           var key = opt[0];
+           var val = opt.length == 1 ? opt[0] : opt[1];
+
+           sort_html += '<option value="' + key + '"';
+           if (m_sortOrder == key || m_sortOrder == val) {
+               sort_html += ' selected="selected"';
+           }
+           sort_html += '>' + M(val) + '</option>';
+       }
+       sort_html += '</select>';
+
+       return sort_html;
+    }
+
+
+    function mkwsHtmlPerpage() {
+       log("HTML perpage, m_perpage = " + m_perpage);
+       var perpage_html = '<select class="mkwsPerpage mkwsTeam_' + m_teamName + '">';
+
+       for(var i = 0; i < m_config.perpage_options.length; i++) {
+           var key = m_config.perpage_options[i];
+
+           perpage_html += '<option value="' + key + '"';
+           if (key == m_perpage) {
+               perpage_html += ' selected="selected"';
+           }
+           perpage_html += '>' + key + '</option>';
+       }
+       perpage_html += '</select>';
+
+       return perpage_html;
+    }
+
+
+    // Translation function. At present, this is properly a
+    // global-level function (hence the assignment to mkws.M) but we
+    // want to make it per-team so different teams can operate in
+    // different languages.
+    //
+    function M(word) {
+       var lang = m_config.lang;
+
+       if (!lang || !mkws.locale_lang[lang])
+           return word;
+
+       return mkws.locale_lang[lang][word] || word;
+    }
+    mkws.M = M; // so the Handlebars helper can use it
+
+
+    // Finds the node of the specified class within the current team
+    function findnode(selector, teamName) {
+       teamName = teamName || m_teamName;
+
+       if (teamName === 'AUTO') {
+           selector = (selector + '.mkwsTeam_' + teamName + ',' +
+                       selector + ':not([class^="mkwsTeam"],[class*=" mkwsTeam"])');
+       } else {
+           selector = selector + '.mkwsTeam_' + teamName;
+       }
+
+       var node = $(selector);
+       //log('findnode(' + selector + ') found ' + node.length + ' nodes');
+       return node;
+    }
+    that.findnode = findnode;
+
+
+    // This much simpler and more efficient function should be usable
+    // in place of most uses of findnode.
+    function widgetNode(type) {
+        var w = that.widget(type);
+        return w ? $(w.node) : undefined;
+    }
+
+    function renderDetails(data, marker) {
+       var template = loadTemplate("Record");
+       var details = template(data);
+       return '<div class="details mkwsTeam_' + m_teamName + '" ' +
+           'id="' + recordDetailsId(data.recid[0]) + '">' + details + '</div>';
+    }
+    that.renderDetails = renderDetails;
+
+
+    that.registerTemplate = function(name, text) {
+        m_tempateText[name] = text;
+    };
+
+
+    function loadTemplate(name) {
+       var template = m_template[name];
+
+       if (template === undefined) {
+           // Fall back to generic template if there is no team-specific one
+           var source;
+           var node = widgetNode("Template_" + name);
+           if (!node) {
+               node = widgetNode("Template_" + name, "ALL");
+           }
+            if (node) {
+               source = node.html();
+            }
+
+           if (!source) {
+                source = m_tempateText[name];
+            }
+           if (!source) {
+               source = defaultTemplate(name);
+           }
+
+           template = Handlebars.compile(source);
+           log("compiled template '" + name + "'");
+           m_template[name] = template;
+       }
+
+       return template;
+    }
+    that.loadTemplate = loadTemplate;
+
+
+    function defaultTemplate(name) {
+       if (name === 'Record') {
+           return '\
+<table>\
+  <tr>\
+    <th>{{translate "Title"}}</th>\
+    <td>\
+      {{md-title}}\
+      {{#if md-title-remainder}}\
+       ({{md-title-remainder}})\
+      {{/if}}\
+      {{#if md-title-responsibility}}\
+       <i>{{md-title-responsibility}}</i>\
+      {{/if}}\
+    </td>\
+  </tr>\
+  {{#if md-date}}\
+  <tr>\
+    <th>{{translate "Date"}}</th>\
+    <td>{{md-date}}</td>\
+  </tr>\
+  {{/if}}\
+  {{#if md-author}}\
+  <tr>\
+    <th>{{translate "Author"}}</th>\
+    <td>{{md-author}}</td>\
+  </tr>\
+  {{/if}}\
+  {{#if md-electronic-url}}\
+  <tr>\
+    <th>{{translate "Links"}}</th>\
+    <td>\
+      {{#each md-electronic-url}}\
+       <a href="{{this}}">Link{{index1}}</a>\
+      {{/each}}\
+    </td>\
+  </tr>\
+  {{/if}}\
+  {{#if-any location having="md-subject"}}\
+  <tr>\
+    <th>{{translate "Subject"}}</th>\
+    <td>\
+      {{#first location having="md-subject"}}\
+       {{#if md-subject}}\
+         {{#commaList md-subject}}\
+           {{this}}{{/commaList}}\
+       {{/if}}\
+      {{/first}}\
+    </td>\
+  </tr>\
+  {{/if-any}}\
+  <tr>\
+    <th>{{translate "Locations"}}</th>\
+    <td>\
+      {{#commaList location}}\
+       {{attr "@name"}}{{/commaList}}\
+    </td>\
+  </tr>\
+</table>\
+';
+       } else if (name === "Summary") {
+           return '\
+<a href="#" id="{{_id}}" onclick="{{_onclick}}">\
+  <b>{{md-title}}</b>\
+</a>\
+{{#if md-title-remainder}}\
+  <span>{{md-title-remainder}}</span>\
+{{/if}}\
+{{#if md-title-responsibility}}\
+  <span><i>{{md-title-responsibility}}</i></span>\
+{{/if}}\
+';
+       } else if (name === "Image") {
+           return '\
+      <a href="#" id="{{_id}}" onclick="{{_onclick}}">\
+        {{#first md-thumburl}}\
+         <img src="{{this}}" alt="{{../md-title}}"/>\
+        {{/first}}\
+       <br/>\
+      </a>\
+';
+       }
+
+       var s = "There is no default '" + name +"' template!";
+       alert(s);
+       return s;
+    }
+
+    that.addWidget = function(w) {
+        if (!m_widgets[w.type]) {
+            m_widgets[w.type] = w;
+            log("Registered '" + w.type + "' widget in team '" + m_teamName + "'");
+        } else if (typeof(m_widgets[w.type]) !== 'number') {
+            m_widgets[w.type] = 2;
+            log("Registered duplicate '" + w.type + "' widget in team '" + m_teamName + "'");
+        } else {
+            m_widgets[w.type] += 1;
+            log("Registered '" + w.type + "' widget #" + m_widgets[w.type] + "' in team '" + m_teamName + "'");
+        }
+    }
+
+    that.widgetTypes = function() {
+        var keys = [];
+        for (var k in m_widgets) keys.push(k);
+        return keys.sort();
+    }
+
+    that.widget = function(type) {
+        return m_widgets[type];
+    }
+
+    mkwsHtmlAll()
+
+    return that;
+};
diff --git a/src/mkws-widget-authname.js b/src/mkws-widget-authname.js
new file mode 100644 (file)
index 0000000..a517e80
--- /dev/null
@@ -0,0 +1,7 @@
+mkws.registerWidgetType('Authname', function() {
+    var that = this;
+
+    this.team.queue("authenticated").subscribe(function(authName) {
+       $(that.node).html(authName);
+    });
+});
diff --git a/src/mkws-widget-builder.js b/src/mkws-widget-builder.js
new file mode 100644 (file)
index 0000000..6460b61
--- /dev/null
@@ -0,0 +1,29 @@
+mkws.registerWidgetType('Builder', function() {
+    var that = this;
+    var team = this.team;
+
+    this.button = $('<button/>', {
+        type: 'button',
+        text: this.config.text || "Build!"
+    });
+    $(this.node).append(this.button);
+    this.button.click(function() {
+        var   query = team.widget('Query').value();
+        var    sort = team.widget('Sort').value();
+        var perpage = team.widget('Perpage').value();
+
+        var html = ('<div class="mkwsRecords" ' +
+                    'autosearch="' + query + '" ' +
+                    'sort="' + sort + '" ' +
+                    'perpage="' + perpage + '"></div>');
+        var fn = that.callback || alert;
+        fn(html);
+    });
+});
+
+mkws.registerWidgetType('ConsoleBuilder', function() {
+    mkws.promotionFunction('Builder').call(this);    
+    this.callback = function(s) {
+        console.log("Generated widget: " + s);
+    }
+});
diff --git a/src/mkws-widget-categories.js b/src/mkws-widget-categories.js
new file mode 100644 (file)
index 0000000..6ce09fe
--- /dev/null
@@ -0,0 +1,35 @@
+mkws.registerWidgetType('Categories', function() {
+    var that = this;
+
+    if (!this.config.use_service_proxy) {
+       alert("can't use categories widget without Service Proxy");
+       return;
+    }
+
+    this.team.queue("authenticated").subscribe(function(authName, realm) {
+       var req = new pzHttpRequest(that.config.pazpar2_url + "?command=categories", function(err) {
+           alert("HTTP call for categories failed: " + err)
+       });
+
+       req.get(null, function(data) {
+           if (!$.isXMLDoc(data)) {
+               alert("categories response document is not XML");
+               return;
+           }
+           that.log("got categories: " + data);
+
+            var text = [];
+            text.push("Select category: ");
+            text.push("<select name='mkwsCategory' " +
+                     "onchange='mkws.limitCategory(\"" + that.team.name() + "\", this.value)'>");
+            text.push("<option value=''>[All]</option>");
+            $(data).find('category').each(function() {
+                var name = $(this).find('categoryName').text();
+                var id = $(this).find('categoryId').text();
+                text.push("<option value='", id, "'>", name, "</option>");
+            });
+            text.push("</select>");
+           $(that.node).html(text.join(''));
+       });
+    });
+});
diff --git a/src/mkws-widget-log.js b/src/mkws-widget-log.js
new file mode 100644 (file)
index 0000000..05af4fa
--- /dev/null
@@ -0,0 +1,7 @@
+mkws.registerWidgetType('Log', function() {
+    var that = this;
+
+    this.team.queue("log").subscribe(function(teamName, timestamp, message) {
+       $(that.node).append(teamName + ": " + timestamp + message + "<br/>");
+    });
+});
diff --git a/src/mkws-widget-record.js b/src/mkws-widget-record.js
new file mode 100644 (file)
index 0000000..f1167de
--- /dev/null
@@ -0,0 +1,24 @@
+mkws.registerWidgetType('Record', function() {
+    mkws.promotionFunction('Records').call(this);
+    if (!this.config.maxrecs) this.config.maxrecs = 1;
+});
+
+mkws.registerWidgetType('Image', function() {
+    mkws.promotionFunction('Record').call(this);
+    if (!this.config.template) this.config.template = 'Image';
+});
+
+mkws.registerWidgetType('GoogleImage', function() {
+    mkws.promotionFunction('Image').call(this);
+    if (!this.config.target) this.config.target = 'Google_Images';
+});
+
+mkws.registerWidgetType('Lolcat', function() {
+    mkws.promotionFunction('GoogleImage').call(this);
+    if (!this.config.autosearch) this.config.autosearch = 'kitteh';
+});
+
+mkws.registerWidgetType('Coverart', function() {
+    mkws.promotionFunction('Image').call(this);
+    if (!this.config.target) this.config.target = 'AmazonBooks';
+});
diff --git a/src/mkws-widget-termlists.js b/src/mkws-widget-termlists.js
new file mode 100644 (file)
index 0000000..e29ede0
--- /dev/null
@@ -0,0 +1,54 @@
+mkws.registerWidgetType('Termlists', function() {
+    var that = this;
+    var facets = that.config.facets;
+
+    this.team.queue("termlists").subscribe(function(data) {
+       // display if we first got results
+       $(that.node).show();
+    });
+
+    widget.autosearch(that);
+});
+
+
+mkws.registerWidgetType('Facet', function() {
+    var facetConfig = {
+       xtargets: [ "Sources",  16, false ],
+       subject:  [ "Subjects", 10, true ],
+       author:   [ "Authors",  10, true ]
+    }
+
+    var that = this;
+    var name = that.config.facet;
+    var ref = facetConfig[name] || alert("no facet definition for '" + name + "'");
+    var caption = ref[0];
+    var max = ref[1];
+    var pzIndex = ref[2] ? name : null;
+
+    that.team.queue("termlists").subscribe(function(data) {
+       data = data[name];
+
+       var teamName = that.team.name();
+       var acc = [];
+       acc.push('<div class="termtitle">' + mkws.M(caption) + '</div>');
+       for (var i = 0; i < data.length && i < max; i++) {
+           acc.push('<div class="term">');
+           acc.push('<a href="#" ');
+           var action = '';
+           if (!pzIndex) {
+               // Special case: target selection
+               acc.push('target_id='+data[i].id+' ');
+               if (!that.team.targetFiltered(data[i].id)) {
+                   action = 'mkws.limitTarget(\'' + teamName + '\', this.getAttribute(\'target_id\'),this.firstChild.nodeValue)';
+               }
+           } else {
+               action = 'mkws.limitQuery(\'' + teamName + '\', \'' + pzIndex + '\', this.firstChild.nodeValue)';
+           }
+           acc.push('onclick="' + action + ';return false;">' + data[i].name + '</a>'
+                    + ' <span>' + data[i].freq + '</span>');
+           acc.push('</div>');
+       }
+
+       $(that.node).html(acc.join(''));
+    });
+});
diff --git a/src/mkws-widgets.js b/src/mkws-widgets.js
new file mode 100644 (file)
index 0000000..90f1794
--- /dev/null
@@ -0,0 +1,373 @@
+// Factory function for widget objects.
+function widget($, team, type, node) {
+    // Static register of attributes that do not contribute to config
+    var ignoreAttrs = {
+       id:1, 'class':1, style:1, name:1, action:1, type:1, size:1,
+       value:1, width:1, valign:1
+    };
+
+    var that = {
+       team: team,
+       type: type,
+       node: node,
+       config: mkws.objectInheritingFrom(team.config())
+    };
+
+    function log(s) {
+       team.log(s);
+    }
+    that.log = log;
+
+    that.toString = function() {
+       return '[Widget ' + team.name() + ':' + type + ']';
+    };
+
+    that.value = function() {
+        return node.value;
+    }
+
+    for (var i = 0; i < node.attributes.length; i++) {
+       var a = node.attributes[i];
+       if (a.name === 'data-mkws-config') {
+           // Treat as a JSON fragment configuring just this widget
+           log(node + ": parsing config fragment '" + a.value + "'");
+           var data;
+           try {
+               data = $.parseJSON(a.value);
+               for (var key in data) {
+                   log(node + ": adding config element " + key + "='" + data[key] + "'");
+                   that.config[key] = data[key];
+               }
+           } catch (err) {
+               alert("Can't parse " + node + " data-mkws-config as JSON: " + a.value);
+           }
+       } else if (a.name.match (/^data-mkws-/)) {
+           var name = a.name.replace(/^data-mkws-/, '')
+           that.config[name] = a.value;
+           log(node + ": set data-mkws attribute " + name + "='" + a.value + "'");
+       } else if (!ignoreAttrs[a.name]) {
+           that.config[a.name] = a.value;
+           log(node + ": set regular attribute " + a.name + "='" + a.value + "'");
+       }
+    }
+
+    var fn = mkws.promotionFunction(type);
+    if (fn) {
+       fn.call(that);
+       log("made " + type + " widget(node=" + node + ")");
+    } else {
+       log("made UNPROMOTED widget(type=" + type + ", node=" + node + ")");
+    }
+
+    return that;
+}
+
+
+// Utility function for use by all widgets that can invoke autosearch.
+widget.autosearch = function(widget) {
+    widget.team.queue("ready").subscribe(function() {
+       var query = widget.config.autosearch;
+       if (query) {
+           if (query.match(/^!param!/)) {
+               var param = query.replace(/^!param!/, '');
+               query = mkws.getParameterByName(param);
+               widget.log("obtained query '" + query + "' from param '" + param + "'");
+               if (!query) {
+                   alert("This page has a MasterKey widget that needs a query specified by the '" + param + "' parameter");
+               }
+           } else if (query.match(/^!path!/)) {
+               var index = query.replace(/^!path!/, '');
+               var path = window.location.pathname.split('/');
+               query = path[path.length - index];
+               widget.log("obtained query '" + query + "' from path-component '" + index + "'");
+               if (!query) {
+                   alert("This page has a MasterKey widget that needs a query specified by the path-component " + index);
+               }
+            } else if (query.match(/^!var!/)) {
+               var name = query.replace(/^!var!/, '');
+               query = window[name]; // It's ridiculous that this works
+               widget.log("obtained query '" + query + "' from variable '" + name + "'");
+               if (!query) {
+                   alert("This page has a MasterKey widget that needs a query specified by the '" + name + "' variable");
+               }
+           }
+
+           var sortOrder = widget.config.sort;
+           var maxrecs = widget.config.maxrecs;
+           var perpage = widget.config.perpage;
+           var limit = widget.config.limit;
+           var targets = widget.config.targets;
+           var targetfilter = widget.config.targetfilter;
+           var target = widget.config.target;
+           if (target) targetfilter = 'udb=="' + target + '"';
+
+           var s = "running auto search: '" + query + "'";
+           if (sortOrder) s += " sorted by '" + sortOrder + "'";
+           if (maxrecs) s += " restricted to " + maxrecs + " records";
+           if (perpage) s += " with " + perpage + " per page";
+           if (limit) s += " limited by '" + limit + "'";
+           if (targets) s += " in targets '" + targets + "'";
+           if (targetfilter) s += " constrained by targetfilter '" + targetfilter + "'";
+           widget.log(s);
+
+           widget.team.newSearch(query, sortOrder, maxrecs, perpage, limit, targets, targetfilter);
+       }
+    });
+};
+
+
+// Functions follow for promoting the regular widget object into
+// widgets of specific types. These could be moved into their own
+// source files.
+
+
+mkws.registerWidgetType('Targets', function() {
+    var that = this;
+    var M = mkws.M;
+
+    $(this.node).html('\
+<div class="mkwsBytarget mkwsTeam_' + this.team.name() + '">\
+No information available yet.\
+</div>');
+    $(this.node).css("display", "none");
+
+    this.team.queue("targets").subscribe(function(data) {
+       var table ='<table><thead><tr>' +
+           '<td>' + M('Target ID') + '</td>' +
+           '<td>' + M('Hits') + '</td>' +
+           '<td>' + M('Diags') + '</td>' +
+           '<td>' + M('Records') + '</td>' +
+           '<td>' + M('State') + '</td>' +
+           '</tr></thead><tbody>';
+
+       for (var i = 0; i < data.length; i++) {
+           table += "<tr><td>" + data[i].id +
+               "</td><td>" + data[i].hits +
+               "</td><td>" + data[i].diagnostic +
+               "</td><td>" + data[i].records +
+               "</td><td>" + data[i].state + "</td></tr>";
+       }
+
+       table += '</tbody></table>';
+       var subnode = $(that.node).children('.mkwsBytarget');
+       subnode.html(table);
+    });
+});
+
+
+mkws.registerWidgetType('Stat', function() {
+    var that = this;
+    var M = mkws.M;
+
+    this.team.queue("stat").subscribe(function(data) {
+       if (that.node.length === 0)  alert("huh?!");
+
+       $(that.node).html('<span class="head">' + M('Status info') + '</span>' +
+           ' -- ' +
+           '<span class="clients">' + M('Active clients') + ': ' + data.activeclients + '/' + data.clients + '</span>' +
+           ' -- ' +
+           '<span class="records">' + M('Retrieved records') + ': ' + data.records + '/' + data.hits + '</span>');
+    });
+});
+
+
+mkws.registerWidgetType('Pager', function() {
+    var that = this;
+    var M = mkws.M;
+
+    this.team.queue("pager").subscribe(function(data) {
+       $(that.node).html(drawPager(data))
+
+       function drawPager(data) {
+           var teamName = that.team.name();
+           var s = '<div style="float: right">' + M('Displaying') + ': '
+               + (data.start + 1) + ' ' + M('to') + ' ' + (data.start + data.num) +
+               ' ' + M('of') + ' ' + data.merged + ' (' + M('found') + ': '
+               + data.total + ')</div>';
+
+           //client indexes pages from 1 but pz2 from 0
+           var onsides = 6;
+           var pages = Math.ceil(that.team.totalRecordCount() / that.team.perpage());
+           var currentPage = that.team.currentPage();
+
+           var firstClkbl = (currentPage - onsides > 0)
+               ? currentPage - onsides
+               : 1;
+
+           var lastClkbl = firstClkbl + 2*onsides < pages
+               ? firstClkbl + 2*onsides
+               : pages;
+
+           var prev = '<span class="mkwsPrev">&#60;&#60; ' + M('Prev') + '</span> | ';
+           if (currentPage > 1)
+               prev = '<a href="#" class="mkwsPrev" onclick="mkws.pagerPrev(\'' + teamName + '\');">'
+               +'&#60;&#60; ' + M('Prev') + '</a> | ';
+
+           var middle = '';
+           for(var i = firstClkbl; i <= lastClkbl; i++) {
+               var numLabel = i;
+               if(i == currentPage)
+                   numLabel = '<span class="mkwsSelected">' + i + '</span>';
+
+               middle += '<a href="#" onclick="mkws.showPage(\'' + teamName + '\', ' + i + ')"> '
+                   + numLabel + ' </a>';
+           }
+
+           var next = ' | <span class="mkwsNext">' + M('Next') + ' &#62;&#62;</span>';
+           if (pages - currentPage > 0)
+               next = ' | <a href="#" class="mkwsNext" onclick="mkws.pagerNext(\'' + teamName + '\')">'
+               + M('Next') + ' &#62;&#62;</a>';
+
+           var predots = '';
+           if (firstClkbl > 1)
+               predots = '...';
+
+           var postdots = '';
+           if (lastClkbl < pages)
+               postdots = '...';
+
+           s += '<div style="float: clear">'
+               + prev + predots + middle + postdots + next + '</div>';
+
+           return s;
+       }
+    });
+});
+
+
+mkws.registerWidgetType('Results', function() {
+    // Nothing to do apart from act as an autosearch trigger
+    // Contained elements do all the real work
+    widget.autosearch(this);
+});
+
+
+mkws.registerWidgetType('Records', function() {
+    var that = this;
+    var team = this.team;
+
+    this.team.queue("records").subscribe(function(data) {
+       var html = [];
+       for (var i = 0; i < data.hits.length; i++) {
+           var hit = data.hits[i];
+            that.team.queue("record").publish(hit);
+           var divId = team.recordElementId(hit.recid[0]);
+           html.push('<div class="record mkwsTeam_' + team.name() + ' ' + divId + '">', renderSummary(hit), '</div>');
+           // ### At some point, we may be able to move the
+           // m_currentRecordId and m_currentRecordData members
+           // from the team object into this widget.
+           if (hit.recid == team.currentRecordId()) {
+               if (team.currentRecordData())
+                   html.push(team.renderDetails(team.currentRecordData()));
+           }
+       }
+       $(that.node).html(html.join(''));
+
+       function renderSummary(hit) {
+           var template = team.loadTemplate(that.config.template || "Summary");
+           hit._id = team.recordElementId(hit.recid[0]);
+           hit._onclick = "mkws.showDetails('" + team.name() + "', '" + hit.recid[0] + "');return false;"
+           return template(hit);
+       }
+    });
+
+    widget.autosearch(that);
+});
+
+
+mkws.registerWidgetType('Navi', function() {
+    var that = this;
+    var teamName = this.team.name();
+    var M = mkws.M;
+
+    this.team.queue("navi").subscribe(function() {
+       var filters = that.team.filters();
+       var text = "";
+
+       filters.visitTargets(function(id, name) {
+           if (text) text += " | ";
+           text += M('source') + ': <a class="crossout" href="#" onclick="mkws.delimitTarget(\'' + teamName +
+               "', '" + id + "'" + ');return false;">' + name + '</a>';
+       });
+
+       filters.visitFields(function(field, value) {
+           if (text) text += " | ";
+           text += M(field) + ': <a class="crossout" href="#" onclick="mkws.delimitQuery(\'' + teamName +
+               "', '" + field + "', '" + value + "'" +
+               ');return false;">' + value + '</a>';
+       });
+
+       $(that.node).html(text);
+    });
+});
+
+
+// It seems this and the Perpage widget doen't need to subscribe to
+// anything, since they produce events rather than consuming them.
+//
+mkws.registerWidgetType('Sort', function() {
+    var that = this;
+
+    $(this.node).change(function() {
+       that.team.set_sortOrder($(that.node).val());
+       if (that.team.submitted()) {
+           that.team.reShow();
+       }
+       return false;
+    });
+});
+
+
+mkws.registerWidgetType('Perpage', function() {
+    var that = this;
+
+    $(this.node).change(function() {
+       that.team.set_perpage($(that.node).val());
+       if (that.team.submitted()) {
+           that.team.reShow();
+       }
+       return false;
+    });
+});
+
+
+mkws.registerWidgetType('Done', function() {
+    var that = this;
+
+    this.team.queue("complete").subscribe(function(n) {
+       $(that.node).html("Search complete: found " + n + " records");
+    });
+});
+
+
+mkws.registerWidgetType('Switch', function() {
+    var tname = this.team.name();
+    $(this.node).html('\
+<a href="#" onclick="mkws.switchView(\'' + tname + '\', \'records\')">Records</a><span> \
+| \
+</span><a href="#" onclick="mkws.switchView(\'' + tname + '\', \'targets\')">Targets</a>');
+});
+
+
+mkws.registerWidgetType('Search', function() {
+    var tname = this.team.name();
+    var M = mkws.M;
+
+    $(this.node).html('\
+<form name="mkwsSearchForm" class="mkwsSearchForm mkwsTeam_' + tname + '" action="" >\
+  <input class="mkwsQuery mkwsTeam_' + tname + '" type="text" size="' + this.config.query_width + '" />\
+  <input class="mkwsButton mkwsTeam_' + tname + '" type="submit" value="' + M('Search') + '" />\
+</form>');
+});
+
+
+mkws.registerWidgetType('SearchForm', function() {
+    var team = this.team;    
+    $(this.node).submit(function() {
+       var val = team.widget('Query').value();
+       team.newSearch(val);
+       return false;
+    });
+});
+
+
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644 (file)
index 0000000..1e39bc5
--- /dev/null
@@ -0,0 +1,6 @@
+node_modules
+logs/error_log
+logs/jasmine-dev
+logs/mkws-jasmine-access.log
+logs/mkws-jasmine-error.log
+logs/mkws-jasmine-rewrite.log
index e434e05..8547d4e 100644 (file)
-# Copyright (c) 2013 IndexData ApS. http://indexdata.com
+# Copyright (c) 2013-2014 IndexData ApS. http://indexdata.com
+
+# For running on Mike's local install of node.js
+MIKE = PATH=$$PATH:/usr/local/lib/node-v0.10.24-linux-x64/bin
+
+#PHANTOMJS_URL=https://mkws-dev.indexdata.com/jasmine-popup.html       
+PHANTOMJS_URL=http://localhost:4040/jasmine-local-popup.html
+PHANTOMJS_TIMEOUT=16
+
+NPM_INSTALL_FLAGS=-q
+JASMINE_NODE=  ./node_modules/jasmine-node/bin/jasmine-node
+PHANTOMJS=     ./node_modules/phantomjs/bin/phantomjs
+IMAGES=        ./images
+SCREENSHOT_WIDTH=      360 480 640 768 1024 1200 1440 2048
+PERL_SCRIPTS=  bin/bomb.pl
+TMP_DIR=       ./logs
+APACHE_HTTPD=  /usr/sbin/apache2
 
 all: check
 
 clean:
+       rm -f mkws-error.png mkws-error.html 
+       rm -f images/*.png
 
-distclean:
+distclean: clean clean-tmp clean-error
        rm -rf node_modules
-       
+       rm -f ${TMP_DIR}/jasmine-dev
 
-check:
-       for i in ./spec/*.js; do \
-         echo "$$i"; \
-         jasmine-node --noColor --captureExceptions --forceexit $$i; \
-       done
+clean-error:
+       rm -f mkws-error.png.* mkws-error.html.*
+
+clean-tmp:
+       rm -rf ${TMP_DIR}
+       mkdir -p ${TMP_DIR}
+       touch ${TMP_DIR}/.gitkeep
+
+mkws-complete-syntax-check:
+       ${MAKE} -C../src mkws-complete.min.js
+
+check: mkws-complete-syntax-check
+       @if [ ! -e node_modules ]; then echo "please run first: make node-modules"; exit 1; fi 
+       ${JASMINE_NODE} --noColor --captureExceptions --forceexit ./spec
 
 test: check
 
+terse:
+       $(MIKE) jasmine-node --noColor --captureExceptions --forceexit spec
+
+phantomjs p: apache-stop apache-start _phantomjs 
+       ${MAKE} apache-stop
+
+_phantomjs:
+       ./bin/bomb.pl --timeout="${PHANTOMJS_TIMEOUT}.5" ${PHANTOMJS} phantom/run-jasmine.js ${PHANTOMJS_URL} ${PHANTOMJS_TIMEOUT}
+
+mike-test:
+       $(MAKE) _phantomjs PHANTOMJS_URL=http://x.example.indexdata.com/jasmine-popup.html
+
+screenshot:
+       ${PHANTOMJS} phantom/screenshot.js ${PHANTOMJS_URL} ${IMAGES}/screenshot.png 1200 1000
+
+screenshot-mkws:
+       for i in ${SCREENSHOT_WIDTH}; do \
+           ${PHANTOMJS} phantom/screenshot.js http://mkws.indexdata.com ${IMAGES}/mkws-$$i.png $$i 1000 &  \
+       done; wait
+       ls -l ${IMAGES}
+
+screenshot-indexdata:
+       for i in ${SCREENSHOT_WIDTH}; do \
+           ${PHANTOMJS} phantom/screenshot.js http://www.indexdata.com ${IMAGES}/indexdata-$$i.png $$i 1000 &  \
+       done; wait
+       ls -l ${IMAGES}
+
 jsbeautifier jsb indent:
-       for i in ./spec/*.js ./js/*.js; do \
+       for i in package.json ./spec*/*.js ./js/*.js ./phantom/*.js; do \
          jsbeautifier -j $$i > $@.tmp && mv -f $@.tmp $$i; \
        done
 
-node-modules: node_modules
-node_modules:
-       npm install jquery jsdom request jasmine-node
+perltidy:
+       @ls ${PERL_SCRIPTS} | xargs -n1 -P16 perl -c 2>/dev/null
+       @ls ${PERL_SCRIPTS} | xargs -n2 -P8 perltidy -b
+
+
+node_modules node-modules:
+       npm install ${NPM_INSTALL_FLAGS}
+
+apache-start:
+       bin/apache-template-update
+       ${APACHE_HTTPD} -f `pwd`/${TMP_DIR}/jasmine-dev
+
+apache-stop:
+       @-if [ -e ${TMP_DIR}/mkws-jasmine.pid ]; then \
+          kill `cat ${TMP_DIR}/mkws-jasmine.pid`; \
+       else \
+          killall apache2 2> /dev/null; \
+       fi
+       @sleep 0.3
+       rm -f ${TMP_DIR}/mkws-jasmine.pid
 
 help:
        @echo "make [ all | check | clean | distclean ]"
-       @echo "     [ jsbeautifier | node-modules ]"
+       @echo "     [ phantomjs | screenshot ]"
+       @echo "     [ jsbeautifier | perltidy ]"
+       @echo "     [ node-modules ]"
+       @echo "     [ apache-stop apache-start ]"
+       @echo ""
+       @echo "DEBUG=1 make phantomjs PHANTOMJS_TIMEOUT=8 PHANTOMJS_URL=${PHANTOMJS_URL}"
+
index 6a49092..fb6bfa5 100644 (file)
@@ -6,7 +6,7 @@ This directory contains the MasterKey Widget Set (MKWS) Test framework.
 To install (some) prerequisites, run:
 
 $ sudo apt-get install npm
-$ sudo npm install jasmine-node -g
+$ sudo npm install -g
 
 To run the tests, run:
 
@@ -18,6 +18,12 @@ Finished in 2.024 seconds
 39 tests, 194 assertions, 0 failures, 0 skipped
 
 
+$ make phantomjs
+[ headless jasmine test with console.log() messages
+
+$ DEBUG=1 make phantomjs PHANTOM_URL=https://mkws-dev.indexdata.com/jasmine-popup.html
+[ less debug noise ]
+
 To get a basic help, run:
 $ make help
 
@@ -26,14 +32,14 @@ File system hierarchy
 --------------------------------------
 ./spec         contains *spec.js files
 ./js           jasmine runtime JS lib
-
-README.txt     this file
+./phantom      scripts for phantomjs tests
 
 
 Documentation
 ---------------------------------------
 http://pivotal.github.io/jasmine/
 https://github.com/pivotal/jasmine
+http://phantomjs.org/
 
 
 Installation
@@ -43,5 +49,5 @@ Installation
 $ make node-modules
 
 --
-Copyright (c) 2013 IndexData ApS. http://indexdata.com
-Dec 2013, Wolfram
+Copyright (c) 2013-2014 IndexData ApS. http://indexdata.com
+Feb 2014, Wolfram
diff --git a/test/bin/apache-template-update b/test/bin/apache-template-update
new file mode 100755 (executable)
index 0000000..0ad5bc9
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/sh
+# Copyright (c) 2014-2014 IndexData ApS. http://indexdata.com
+# Wolfram Schneider
+#
+# generate temp config file for testing
+#
+
+export APACHE_SERVER_ROOT=$(pwd)
+export APACHE_RUN_USER=$(whoami)
+export APACHE_RUN_GROUP=$(groups | awk '{ print $1 }')
+
+export APACHE_LOG_DIR=$APACHE_SERVER_ROOT/logs
+export APACHE_PID_FILE=$APACHE_LOG_DIR/mkws-jasmine.pid
+export APACHE_PORT=4040
+
+export MKWS_ROOT=$(pwd)/..
+
+: ${MKWS_APACHE_TEMPLATE="$MKWS_ROOT/tools/apache2/jasmine-dev.template"}
+: ${MKWS_APACHE_FILE="$APACHE_LOG_DIR/jasmine-dev"}
+
+perl -npe 's,\${(.*?)},$ENV{$1},g; ' $MKWS_APACHE_TEMPLATE > $MKWS_APACHE_FILE.tmp
+mv -f $MKWS_APACHE_FILE.tmp $MKWS_APACHE_FILE
+
diff --git a/test/bin/bomb.pl b/test/bin/bomb.pl
new file mode 100755 (executable)
index 0000000..6211eab
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/perl
+# Copyright (c) 2014 IndexData ApS. http://indexdata.com
+#
+# bomb.pl - wrapper to stop a process after N seconds
+#
+
+use Getopt::Long;
+use POSIX ":sys_wait_h";
+
+use strict;
+use warnings;
+
+my $debug = 0;
+my $help;
+my $timeout = 100;
+my $pid;
+
+binmode \*STDOUT, ":utf8";
+binmode \*STDERR, ":utf8";
+
+sub usage () {
+    <<EOF;
+usage: $0 [ options ] command args ....
+
+--debug=0..3    debug option, default: $debug
+--timeout=1..N  timeout in seconds, default: $timeout
+EOF
+}
+
+GetOptions(
+    "help"      => \$help,
+    "debug=i"   => \$debug,
+    "timeout=f" => \$timeout,
+) or die usage;
+
+my @system = @ARGV;
+
+die usage if $help;
+die usage if !@system;
+
+#
+# use fork/exec instead system()
+#
+$pid = fork();
+die "fork() failed: $!" unless defined $pid;
+
+# child
+if ($pid) {
+    alarm($timeout);
+    exec(@system) or die "exec @system: $!\n";
+}
+
+# parent
+else { }
+
+1;
+
diff --git a/test/etc/logrotate.d/mkws b/test/etc/logrotate.d/mkws
new file mode 100644 (file)
index 0000000..35fefe3
--- /dev/null
@@ -0,0 +1,5 @@
+"mkws-error.png" "mkws-error.html" {
+   rotate 7
+   missingok
+}
+
diff --git a/test/images/.gitkeep b/test/images/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/test/logs/.gitkeep b/test/logs/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/test/package.json b/test/package.json
new file mode 100644 (file)
index 0000000..3f69e77
--- /dev/null
@@ -0,0 +1,24 @@
+{
+    "name": "MKWS",
+    "version": "0.9.1",
+    "license": "GPL, http://www.indexdata.com/licences/gpl",
+    "contributors": [{
+        "name": "Mike Taylor",
+        "email": "mike@indexdata.com"
+    }, {
+        "name": "Wolfram Schneider",
+        "email": "wosch@indexdata.com"
+    }],
+    "devDependencies": {
+        "jQuery": "*",
+        "xmlhttprequest": "*",
+        "jsdom": "*",
+        "request": "*",
+        "jasmine-node": "*",
+        "phantomjs": "*"
+    },
+    "repository": {
+        "type": "git",
+        "url": "http://git.indexdata.com/mkws.git"
+    }
+}
diff --git a/test/phantom/evaluate.js b/test/phantom/evaluate.js
new file mode 100644 (file)
index 0000000..0cf56f0
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+    Fetch a mkws/jasmine based page into node.js, evaluate the page and check if test status
+    This should make it possible to run the test on the command line in jenkins.  e.g.:
+
+      phantomjs evaluate.js https://mkws-dev.indexdata.com/jasmine-local-popup.html
+*/
+
+var page = require('webpage').create(),
+    system = require('system');
+
+if (system.args.length === 1) {
+    console.log('Usage: screenshot.js <some URL>');
+    phantom.exit();
+}
+var url = system.args[1];
+
+var run_time = 8; // poll up to seconds
+if (system.args[2] && parseFloat(system.args[2]) > 0) {
+    run_time = parseFloat(system.args[2]);
+}
+
+page.viewportSize = {
+    width: 1200,
+    height: 1000
+};
+
+// 0: silent, 1: some infos,  2: display console.log() output
+var debug = 2;
+if (typeof system.env['DEBUG'] != 'undefined' && parseInt(system.env['DEBUG']) != NaN) {
+    debug = system.env['DEBUG'];
+    if (debug > 0) console.log("reset debug level to: " + debug);
+}
+
+/************************/
+
+function wait_for_jasmine(checkFx, readyFx, failFx, timeout) {
+    var max_timeout = timeout ? timeout : run_time * 1000,
+        start = new Date().getTime(),
+        result, condition = false;
+
+    var interval = setInterval(function () {
+        if (debug == 1) console.log(".");
+
+        // success
+        if (condition) {
+            // console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
+            result.time = (new Date().getTime() - start);
+            readyFx(result);
+            clearInterval(interval);
+            phantom.exit(0);
+        }
+
+        // timeout
+        else if (new Date().getTime() - start >= max_timeout) {
+            result.time = (new Date().getTime() - start);
+            failFx(result);
+            phantom.exit(1);
+        }
+
+        // checking
+        else {
+            result = checkFx();
+            if (result) condition = result.mkws.jasmine_done;
+        }
+
+    }, 500); //< repeat check every N ms
+};
+
+// redirect webkit console.log() output
+page.onConsoleMessage = function (message) {
+    if (debug >= 2) console.log(message);
+};
+
+// cat webkit alert()
+page.onAlert = function (msg) {
+    console.log("Alert: " + msg);
+};
+
+// display HTTP errors
+page.onResourceError = function (resourceError) {
+    // console.log('phantomjs error code: ' + resourceError.errorCode);
+    console.log(resourceError.errorString);
+    phantom.exit(3);
+};
+
+page.open(url, function (status) {
+    if (debug >= 1) console.log("fetch " + url + " with status: " + status);
+
+    if (status != 'success') {
+        console.log("Failed to fetch page, give up. Network error?");
+        phantom.exit(1);
+    }
+
+    if (debug >= 1) console.log("polling MKWS jasmine test status for " + run_time + " seconds");
+
+
+    var exit = wait_for_jasmine(function () {
+        return page.evaluate(function () {
+            if (!window || !window.$ || !window.mkws) {
+                return false;
+            } else {
+                var passing = window.$(".passingAlert").text() || window.$(".failingAlert").text();
+
+                return {
+                    mkws: window.mkws,
+                    html: window.$("html").html(),
+                    duration: window.$(".duration").text(),
+                    passing: passing
+                };
+            }
+        })
+    },
+
+    function (result) {
+        if (debug < 1) return;
+
+        console.log("");
+        console.log("MKWS tests are successfully done in " + result.time / 1000 + " seconds. Hooray!");
+        console.log("jasmine duration: " + result.duration);
+        console.log("jasmine passing: " + result.passing);
+    },
+
+    function (result) {
+        var error_png = "./mkws-error.png";
+        var error_html = "./mkws-error.html";
+
+        console.log("MKWS tests failed after " + result.time / 1000 + " seconds");
+        console.log("keep screenshot in '" + error_png + "'");
+        page.render(error_png);
+
+        console.log("keep html DOM in '" + error_html + "'");
+        var html = result.html + "\n\n<!-- mkws: " + JSON.stringify(result.mkws) + " -->\n";
+        var fs = require('fs');
+        fs.write(error_html, html, "wb");
+    }, run_time * 1000);
+});
diff --git a/test/phantom/run-jasmine.js b/test/phantom/run-jasmine.js
new file mode 100644 (file)
index 0000000..27c465b
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+    Fetch a mkws/jasmine based page into node.js, evaluate the page and check if test status
+    This should make it possible to run the test on the command line in jenkins.  e.g.:
+
+      phantomjs evaluate.js https://mkws-dev.indexdata.com/jasmine-local-popup.html
+*/
+
+var page = require('webpage').create(),
+    system = require('system');
+
+if (system.args.length === 1) {
+    console.log('Usage: screenshot.js <some URL>');
+    phantom.exit();
+}
+var url = system.args[1];
+
+var run_time = 8; // poll up to seconds
+if (system.args[2] && parseFloat(system.args[2]) > 0) {
+    run_time = parseFloat(system.args[2]);
+}
+
+page.viewportSize = {
+    width: 1200,
+    height: 1000
+};
+
+// 0: silent, 1: some infos,  2: display console.log() output
+var debug = 2;
+if (typeof system.env['DEBUG'] != 'undefined' && parseInt(system.env['DEBUG']) != NaN) {
+    debug = system.env['DEBUG'];
+    if (debug > 0) console.log("reset debug level to: " + debug);
+}
+
+/************************/
+
+function wait_for_jasmine(checkFx, readyFx, failFx, timeout) {
+    var max_timeout = timeout ? timeout : run_time * 1000,
+        start = new Date().getTime(),
+        result, condition = false;
+
+    var interval = setInterval(function () {
+        if (debug == 1) console.log(".");
+
+        // done
+        if (condition) {
+            clearInterval(interval);
+            result.time = (new Date().getTime() - start);
+            result.failed ? failFx(result) : readyFx(result);
+            phantom.exit(result.failed == 0 ? 0 : 2);
+        }
+
+        // timeout
+        else if (new Date().getTime() - start >= max_timeout) {
+            result.time = (new Date().getTime() - start);
+            failFx(result);
+            phantom.exit(1);
+        }
+
+        // checking
+        else {
+            result = checkFx();
+            if (result) condition = result.done;
+        }
+
+    }, 500); //< repeat check every N ms
+};
+
+function dump_html(file) {
+    // not yet implemented
+    var spawn = require('child_process').spawn,
+        lynx = spawn("lynx", ["-nolist", "-dump", file]);
+
+    lynx.stdout.on('data', function (data) {
+        console.log('lynx >> ' + data);
+    });
+
+    // lynx.stderr.on('data', function (data) { console.log('stderr: ' + data); });
+    // lynx.on('close', function (code) { console.log('child process exited with code ' + code) });
+};
+
+// redirect webkit console.log() output
+page.onConsoleMessage = function (message) {
+    if (debug >= 2) console.log(message);
+};
+
+// cat webkit alert()
+page.onAlert = function (msg) {
+    console.log("Alert: " + msg);
+};
+
+// display HTTP errors
+page.onResourceError = function (resourceError) {
+    // console.log('phantomjs error code: ' + resourceError.errorCode);
+    console.log(resourceError.errorString);
+    phantom.exit(3);
+};
+
+page.open(url, function (status) {
+    if (debug >= 1) console.log("fetch " + url + " with status: " + status);
+
+    if (status != 'success') {
+        console.log("Failed to fetch page, give up. Network error?");
+        phantom.exit(1);
+    }
+
+    if (debug >= 1) console.log("polling MKWS jasmine test status for " + run_time + " seconds");
+
+
+    var exit = wait_for_jasmine(function () {
+        return page.evaluate(function () {
+            if (!window || !window.$ || !window.mkws) {
+                console.log("No window object found");
+                return false;
+            }
+
+            var $ = window.$;
+            var error_msg = [""];
+            var passing = $(".passingAlert").text() || window.$(".failingAlert").text();
+
+            // extract failed tests
+            var list = $('.results > #details > .specDetail.failed');
+            if (list && list.length > 0) {
+                error_msg.push("==> " + list.length + ' test(s) FAILED:');
+                for (i = 0; i < list.length; ++i) {
+                    var el = list[i],
+                        desc = el.querySelector('.description'),
+                        msg = el.querySelector('.resultMessage.fail');
+                    error_msg.push($(desc).text());
+                    error_msg.push($(msg).text());
+                }
+            }
+
+            return {
+                mkws: window.mkws,
+                done: $('.symbolSummary .pending').length == 0,
+                html: $("html").html(),
+                duration: $(".duration").text(),
+                error_msg: error_msg,
+                failed: list.length,
+                passing: passing
+            };
+        })
+    },
+
+    function (result) {
+        if (debug < 1) return;
+
+        console.log("");
+        console.log("MKWS tests are successfully done in " + result.time / 1000 + " seconds. Hooray!");
+        console.log("jasmine duration: " + result.duration);
+        console.log("jasmine passing: " + result.passing);
+    },
+
+    function (result) {
+        var error_png = "./mkws-error.png";
+        var error_html = "./mkws-error.html";
+
+        var html = result.html + "\n\n<!-- mkws: " + JSON.stringify(result.mkws) + " -->\n";
+        var fs = require('fs');
+        fs.write(error_html, html, "wb");
+        dump_html(error_html);
+
+        console.log("MKWS tests failed after " + result.time / 1000 + " seconds");
+        console.log(result.error_msg.join("\n"));
+        console.log("keep screenshot in '" + error_png + "'");
+        page.render(error_png);
+
+        console.log("keep html DOM in '" + error_html + "'");
+        // console.log("you may run: lynx -nolist -dump " + error_html);
+    }, run_time * 1000);
+});
diff --git a/test/phantom/screenshot.js b/test/phantom/screenshot.js
new file mode 100644 (file)
index 0000000..ac4de3b
--- /dev/null
@@ -0,0 +1,29 @@
+var page = require('webpage').create(),
+    system = require('system');
+
+var url = system.args[1] || 'http://www.indexdata.com/';
+var file_png = system.args[2] || 'indexdata.png';
+
+if (system.args.length === 1) {
+    console.log('Usage: screenshot.js <some URL> <file.png>');
+    phantom.exit();
+}
+
+// page.zoomFactor = 1.0;
+page.viewportSize = {
+    width: system.args[3] ? system.args[3] : 1200,
+    height: system.args[4] ? system.args[4] : 1000
+};
+
+page.clipRect = {
+    width: page.viewportSize.width,
+    height: page.viewportSize.height
+};
+
+page.open(url, function () {
+    // small delay
+    setTimeout(function () {
+        var ret = page.render(file_png);
+        phantom.exit();
+    }, 200);
+});
diff --git a/test/spec-dev/mkws.spec.js b/test/spec-dev/mkws.spec.js
new file mode 100644 (file)
index 0000000..2e6e3ea
--- /dev/null
@@ -0,0 +1,91 @@
+describe("jsdom/jQuery suite simple", function () {
+    it("jsdom test", function () {
+        var jsdom = require("jsdom");
+        var DOMParser = require('xmldom').DOMParser;
+
+        var w = undefined;
+        var $ = undefined;
+
+        jsdom.env({
+            url: "http://mkws-dev.indexdata.com/jasmine-local-popup.html",
+            scripts: [""],
+            features: {
+                FetchExternalResources: ["script"]
+            },
+
+            done: function (errors, window) {
+                var DOMParser = require('xmldom').DOMParser;
+
+                w = window;
+                $ = window.$;
+
+                $(window).ready(function () {
+                    console.log("document ready event");
+                    console.log("mkws: " + window.mkws_config.pazpar2_url);
+
+                    // setTimeout( function () { console.log("timeer...") }, 1000);
+                });
+
+                // spyOn(window, 'alert').andCallFake(function(msg) {  console.log("fake allert: " + msg); });
+                window.alert = console.log;
+                window.console = console;
+
+                console.log("window.DOMParser: " + window.DOMParser);
+                console.log("window.document: " + window.document);
+
+
+                var xmlstring = "<rss version='2.0' jsessionId='CD8AFDD3040A81CFFDDD4EC066497139'><channel><title>RSS Title</title></channel></rss>";
+                $.parseXML = function (data) {
+                    return new DOMParser().parseFromString(data)
+                };;
+                console.log("parseXML: " + $.parseXML(xmlstring).documentElement.getAttribute('jsessionId'));
+            }
+        });
+
+        waitsFor(function () {
+            if (!w) {
+                console.log(".");
+            } else if (w && !w.mkws) {
+                console.log("*");
+            } else {
+                // console.log("+");
+            }
+
+            return w && w.mkws && w.mkws.authenticated;
+        }, "window object done", 2 * 1000);
+
+        runs(function () {
+            console.log("got window");
+            console.log("got mkws auth: " + w.mkws.authenticated);
+            console.log("window.DOMParser: " + w.$.parseXML);
+            // console.log("W: " + $("html").text() );
+            expect(w).toBeDefined();
+        });
+
+        waitsFor(function () {
+            // console.log(".");
+            return w.mkws.jasmine_done;
+        }, "jasmine test done", 3 * 1000);
+
+        runs(function () {
+            console.log("jasmine test done: " + w.mkws.jasmine_done);
+            expect(w.mkws.jasmine_done).toBeTruthy();
+        });
+    });
+
+    it("jsdom test2", function () {
+        // expect($).toBeDefined();
+    });
+
+});
+
+console.log("EOF");
+
+/*
+jsdom.defaultDocumentFeatures = {
+  FetchExternalResources   : ['script'],
+  ProcessExternalResources : ['script'],
+  MutationEvents           : false,
+  QuerySelector            : false
+};
+*/
diff --git a/test/spec-dev/parseXML.js b/test/spec-dev/parseXML.js
new file mode 100644 (file)
index 0000000..ec37219
--- /dev/null
@@ -0,0 +1,24 @@
+// Workaround for broken XML parser in node.js/jquery
+// see https://github.com/coolaj86/node-jquery/issues/29
+var jsdom = require("jsdom");
+var DOMParser = require('xmldom').DOMParser;
+var xmlstring = '<?xml version="1.0" encoding="UTF-8"?><process>yes</process>';
+
+jsdom.env('<html/>',
+// ["http://code.jquery.com/jquery.js"],
+
+function (errors, window) {
+    // var $ = window.$; 
+    var $ = require('jQuery');
+
+    // override jquery xml parser with external XML lib xmldoc.DOMParser
+    $.parseXML = function (data) {
+        return new DOMParser().parseFromString(data)
+    };;
+
+    // parse XML string, extract "process" node and keep the text value of the node
+    var result = $($.parseXML(xmlstring)).find("process").text();
+
+    // should output "yes"
+    console.log("Testing jsdom/xmldom/jQuery $.parseXML() support: " + result);
+});
diff --git a/test/spec-dev/parseXML.spec.js b/test/spec-dev/parseXML.spec.js
new file mode 100644 (file)
index 0000000..6827642
--- /dev/null
@@ -0,0 +1,48 @@
+describe("jsdom/jQuery suite simple", function () {
+    it("jsdom test", function () {
+        var jsdom = require("jsdom");
+
+        var $, w;
+        jsdom.env('<p><a class="the-link" href="http://indexdata.com">jsdom\'s Homepage</a></p>', ["http://code.jquery.com/jquery.js"], function (errors, window) {
+            console.log("contents of a.the-link:", window.$("a.the-link").text());
+            w = window;
+            $ = window.$;
+        });
+
+        waitsFor(function () {
+            if (!w) {
+                console.log(".");
+            }
+            return w;
+        }, "window object done", 2 * 1000);
+
+        runs(function () {
+            console.log("got window");
+            expect(w).toBeDefined();
+            expect(w.document).toBeDefined();
+            expect($.parseXML).toBeDefined();
+
+            var xmlstring = "<rss version='2.0' jsessionId='CD8AFDD3040A81CFFDDD4EC066497139'><channel><title>RSS Title</title></channel></rss>";
+
+            var DOMParser = require('xmldom').DOMParser;
+            var doc = new DOMParser().parseFromString(xmlstring);
+            console.log("doc: " + doc.documentElement.getAttribute('jsessionId'));
+
+            var xmlDoc = doc; // $.parseXML(xml);
+            var xml = $(xmlDoc);
+            var title = xml.find("title");
+
+            console.log("title: " + $(title).text());
+            $.parseXML = function (data) {
+                return new DOMParser().parseFromString(data)
+            };;
+
+            console.log("parseXML: " + $($.parseXML(xmlstring)).text());
+
+            // console.log(w.document);
+        })
+    });
+
+});
+
+console.log("EOF");
index 2f3baa8..1f473c9 100644 (file)
@@ -5,7 +5,7 @@
  */
 
 describe("jQuery suite simple", function () {
-    var $ = require('jquery');
+    var $ = require('jQuery');
 
     it("jQuery append test", function () {
         $("body").append("<h1>test passes h1</h1>");
index 725d052..c83809a 100644 (file)
@@ -8,9 +8,9 @@ describe("jQuery suite", function () {
     var jsdom = require('jsdom').jsdom;
     var myWindow = jsdom().createWindow();
 
-    var $ = require('jquery');
-    var jq = require('jquery').create();
-    var jQuery = require('jquery').create(myWindow);
+    var $ = require('jQuery');
+    var jq = require('jQuery').create();
+    var jQuery = require('jQuery').create(myWindow);
 
     it("jQuery append test", function () {
         jQuery("<h1>test passes h1</h1>").appendTo("body");
index 6ec792d..d168fb2 100644 (file)
@@ -1,28 +1,28 @@
-/* Copyright (c) 2013 IndexData ApS. http://indexdata.com
+/* Copyright (c) 2013-2014 IndexData ApS. http://indexdata.com
  *
  * check mkws_config = {} object in browser
  *
  */
 
-describe("Check mkws_config object", function () {
-    it("mkws_config exists", function () {
-        expect(mkws_config).not.toBe(undefined);
+describe("Check mkws.config object", function () {
+    it("mkws.config exists", function () {
+        expect(mkws.config).not.toBe(undefined);
     });
 
-    it("mkws_config locale check German", function () {
+    it("mkws.config locale check German", function () {
         expect(mkws.locale_lang.de.Authors).toMatch(/^Autoren$/);
         expect(mkws.locale_lang.de.Location).toMatch(/^Ort$/);
     });
-    it("mkws_config locale check Danish", function () {
+    it("mkws.config locale check Danish", function () {
         expect(mkws.locale_lang.da.Authors).toMatch(/^Forfattere$/);
         expect(mkws.locale_lang.da.Location).toMatch(/^Lokation$/);
     });
 
-    it("mkws_config service proxy enabled/disabled", function () {
-        if (mkws_config.use_service_proxy) {
-            expect(mkws_config.use_service_proxy).toBe(true);
+    it("mkws.config service proxy enabled/disabled", function () {
+        if (mkws.config.use_service_proxy) {
+            expect(mkws.config.use_service_proxy).toBe(true);
         } else {
-            expect(mkws_config.use_service_proxy).toBe(false);
+            expect(mkws.config.use_service_proxy).toBe(false);
         }
     });
 
@@ -31,24 +31,12 @@ describe("Check mkws_config object", function () {
 
 describe("Check pazpar2 config", function () {
     it("pazpar2path is a path or an full URL", function () {
-        expect(mkws_config.pazpar2_url).toMatch(/^(\/|http:\/\/)/)
-    });
-
-    it("Check usesessions true/false", function () {
-        if (mkws_config.use_service_proxy) {
-            expect(mkws.usesessions).toBe(false);
-        } else {
-            expect(mkws.usesessions).toBe(true);
-        }
-    });
-
-    it("my_paz is defined", function () {
-        expect(mkws.my_paz).not.toBe(undefined);
+        expect(mkws.config.pazpar2_url).toMatch(/^(\/|https?:\/\/)/)
     });
 });
 
 describe("Check pazpar2 runtime", function () {
-    it("pazpar2 was successfully initialize", function () {
-        expect(mkws_config.error).toBe(undefined);
+    it("pazpar2 was successfully initialized", function () {
+        expect(mkws.config.error).toBe(undefined);
     });
 });
diff --git a/test/spec/mkws-index-jsdom-remote.spec.js b/test/spec/mkws-index-jsdom-remote.spec.js
deleted file mode 100644 (file)
index c885d67..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-/* Copyright (c) 2013 IndexData ApS. http://indexdata.com
- *
- * jQuery test with DOM/windows object
- *
- */
-
-
-var fs = require("fs");
-var utils = require("./mkws_utils.js");
-
-/*
- * parse HTML data to DOM, and run jQuery request on it
- *
- */
-
-function jsdom_check(file, tags_array, ignore_doctype) {
-    var html = fs.readFileSync(file, "utf-8");
-    var tags = utils.flat_list(tags_array);
-
-    describe("language.html jsdom + jquery for " + file, function () {
-        var window = require('jsdom').jsdom(html, null, {
-            FetchExternalResources: false,
-            ProcessExternalResources: false,
-            MutationEvents: false,
-            QuerySelector: false
-        }).createWindow();
-
-        /* apply jquery to the window */
-        var $ = require('jquery').create(window);
-
-
-        it("html jquery test", function () {
-            expect(html).toBeDefined();
-
-            expect($("body").length == 0).toEqual(false);
-            expect($("body").length == 1).toEqual(true);
-            expect($("head").length == 1).toEqual(true);
-
-            for (var i = 0; i < tags.length; i++) {
-                expect($("#" + tags[i]).length == 1).toEqual(true);
-            }
-        });
-
-        it("html jquery fail test", function () {
-            expect(html).toBeDefined();
-
-            expect($("body_does_not_exists").length == 1).toEqual(false);
-            expect($("#body_does_not_exists").length == 1).toEqual(false);
-        });
-    });
-}
-
-/*
-jsdom_check('../examples/htdocs/language.html', [utils.tags.required, utils.tags.optional, utils.tags.optional2]);
-jsdom_check('../examples/htdocs/mobile.html', [utils.tags.required, utils.tags.optional]);
-jsdom_check('../examples/htdocs/popup.html', [], true);
-jsdom_check('../examples/htdocs/jquery.html', []);
-jsdom_check('../examples/htdocs/mike.html', [utils.tags.required, utils.tags.optional], true);
-*/
diff --git a/test/spec/mkws-index-jsdom.spec.js b/test/spec/mkws-index-jsdom.spec.js
deleted file mode 100644 (file)
index 8ed7993..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-/* Copyright (c) 2013 IndexData ApS. http://indexdata.com
- *
- * jQuery test with DOM/windows object
- *
- */
-
-
-var fs = require("fs");
-var utils = require("./mkws_utils.js");
-
-/*
- * parse HTML data to DOM, and run jQuery request on it
- *
- */
-
-function jsdom_check(file, tags_array, ignore_doctype) {
-    var html = fs.readFileSync(file, "utf-8");
-    var tags = utils.flat_list(tags_array);
-
-    describe("language.html jsdom + jquery for " + file, function () {
-        var window = require('jsdom').jsdom(html, null, {
-            FetchExternalResources: false,
-            ProcessExternalResources: false,
-            MutationEvents: false,
-            QuerySelector: false
-        }).createWindow();
-
-        /* apply jquery to the window */
-        var $ = require('jquery').create(window);
-
-
-        it("html jquery test", function () {
-            expect(html).toBeDefined();
-
-            expect($("body").length == 0).toEqual(false);
-            expect($("body").length == 1).toEqual(true);
-            expect($("head").length == 1).toEqual(true);
-
-            for (var i = 0; i < tags.length; i++) {
-                expect($("#" + tags[i]).length == 1).toEqual(true);
-            }
-        });
-
-        it("html jquery fail test", function () {
-            expect(html).toBeDefined();
-
-            expect($("body_does_not_exists").length == 1).toEqual(false);
-            expect($("#body_does_not_exists").length == 1).toEqual(false);
-        });
-    });
-}
-
-jsdom_check('../examples/htdocs/language.html', [utils.tags.required, utils.tags.optional, utils.tags.optional2]);
-jsdom_check('../examples/htdocs/mobile.html', [utils.tags.required, utils.tags.optional]);
-jsdom_check('../examples/htdocs/popup.html', [], true);
-jsdom_check('../examples/htdocs/jquery.html', []);
-// jsdom_check('../examples/htdocs/mike.html', [utils.tags.required, utils.tags.optional], true);
diff --git a/test/spec/mkws-index-simple.spec.js b/test/spec/mkws-index-simple.spec.js
deleted file mode 100644 (file)
index a91acf7..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/* Copyright (c) 2013 IndexData ApS. http://indexdata.com
- *
- * jQuery test with DOM/windows object
- *
- */
-
-
-var fs = require("fs");
-var utils = require("./mkws_utils.js");
-
-/*
- * simple test with string matching of the HTML page
- *
- */
-
-function html_check(file, tags_array, ignore_doctype) {
-    var html = fs.readFileSync(file, "utf-8");
-    var tags = utils.flat_list(tags_array);
-
-    describe("language.html string test for " + file, function () {
-        it("html test", function () {
-            expect(html).toBeDefined();
-
-            // forgotten doctype declaration
-            if (!ignore_doctype) {
-                expect(html).toMatch(/<html.*?>/);
-                expect(html).toMatch(/<\/html.*?>/);
-            }
-            expect(html).toMatch(/<head.*?>/);
-            expect(html).toMatch(/<body.*?>/);
-            expect(html).toMatch(/<\/head.*?>/);
-            expect(html).toMatch(/<\/body.*?>/);
-
-            expect(html).toMatch(/<meta .*?charset=utf-8/i);
-            expect(html).toMatch(/<title>.+<\/title>/i);
-            expect(html).toMatch(/<link .*?type="text\/css" href=".*?\/?mkws.css"/);
-
-
-            for (var i = 0, data = ""; i < tags.length; i++) {
-                data = '<div id="' + tags[i] + '">';
-                // console.log(data)
-                expect(html).toMatch(data);
-            }
-        });
-    });
-}
-
-html_check('../examples/htdocs/language.html', [utils.tags.required, utils.tags.optional, utils.tags.optional2]);
-html_check('../examples/htdocs/mobile.html', [utils.tags.required, utils.tags.optional]);
-html_check('../examples/htdocs/popup.html', [], true);
-html_check('../examples/htdocs/jquery.html', []);
-// html_check('../examples/htdocs/mike.html', [utils.tags.required, utils.tags.optional], true);
index 002a705..3433115 100644 (file)
@@ -1,4 +1,4 @@
-/* Copyright (c) 2013 IndexData ApS. http://indexdata.com
+/* Copyright (c) 2013-2014 IndexData ApS. http://indexdata.com
  *
  * perform papzpar2 / pz2.js search & retrieve request in the browser
  *
 
 // get references from mkws.js, lazy evaluation
 var debug = function (text) {
-        mkws.debug_function(text)
+        mkws.log("Jasmine: " + text)
     }
 
+    // Define empty jasmine_config for simple applications that don't define it.
+if (jasmine_config == null || typeof jasmine_config != 'object') {
+    var jasmine_config = {};
+}
+
+var jasmine_status = {
+    source_click: 0
+};
+
+/* check config for jasmine test
+ *
+ * you can override the default values in the config
+ * object: jasmine_config = {};
+ *
+ */
+function init_jasmine_config() {
+
+    var jasmine_config_default = {
+        search_query: "freebsd",
+        max_time: 17,
+        // in seconds
+        expected_hits: 80,
+        // at least expected hit counter
+        second: 1000,
+        // miliseconds to seconds
+        show_record_url: true,
+        // check for valid URL in records
+        check_motd: true,
+        dummy: false
+    };
+
+    // use default values for undefined values
+    for (var key in jasmine_config_default) {
+        if (!jasmine_config.hasOwnProperty(key)) {
+            jasmine_config[key] = jasmine_config_default[key];
+        }
+        debug("jasmine config: " + key + " => " + jasmine_config[key]);
+    }
+
+    mkws.jasmine_done = false;
+}
+
 var get_hit_counter = function () {
         // not yet here
-        if ($("#mkwsPager").length == 0) return -1;
+        if ($(".mkwsPager").length == 0) return -1;
 
-        var found = $("#mkwsPager").text();
+        var found = $(".mkwsPager").text();
         var re = /\([A-Za-z]+:\s+([0-9]+)\)/;
         re.exec(found);
         var hits = -1;
 
         if (RegExp.$1) {
             hits = parseInt(RegExp.$1);
-            expect(hits).toBeGreaterThan(0);
+            if (hits <= 0) {
+                debug("Oooops in get_hit_counter: " + RegExp.$1 + " '" + found + "'");
+            }
         }
 
         //debug("Hits: " + hits);
         return hits;
     }
 
+describe("Init jasmine config", function () {
+    it("jasmine was successfully initialized", function () {
+        init_jasmine_config();
+
+        expect(jasmine_config.search_query).toMatch(/\w/);
+        expect(jasmine_config.second).toBeGreaterThan(100);
+        expect(jasmine_config.max_time).toBeGreaterThan(1);
+        expect(jasmine_config.expected_hits).toBeGreaterThan(1);
+    });
+});
+
+//disabled
+xdescribe("Check MOTD before search", function () {
+    // Check that the MOTD has been moved into its container, and
+    // is visible before the search.
+    // the mkwsMOTD div was originally inside a testMOTD div, which should
+    // now be empty
+    // Note that the testMOTD is a regular div, and uses #testMOTD,
+    // since the automagic class-making does not apply to it.
+    it("MOTD is hidden", function () {
+        expect($(".mkwsMOTD").length).toBe(1);
+        expect($("#testMOTD").length).toBe(1);
+        expect($("#testMOTD").text()).toMatch("^ *$");
+    });
+
+    it("mkwsMOTDContainer has received the text", function () {
+        expect($(".mkwsMOTDContainer").length).toBe(1);
+        expect($(".mkwsMOTDContainer").text()).toMatch(/MOTD/);
+    });
+});
+
 describe("Check pazpar2 search", function () {
-    it("pazpar2 was successfully initialize", function () {
-        expect(mkws_config.error).toBe(undefined);
+    it("pazpar2 was successfully initialized", function () {
+        expect(mkws.config.error).toBe(undefined);
     });
 
     it("validate HTML id's", function () {
-        expect($("input#mkwsQuery").length).toBe(1);
-        expect($("input#mkwsButton").length).toBe(1);
+        expect($("input.mkwsQuery").length).toBe(1);
+        expect($("input.mkwsButton").length).toBe(1);
 
-        expect($("#mkwsNext").length).not.toBe(1);
-        expect($("#mkwsPrev").length).not.toBe(1);
+        expect($(".mkwsNext").length).not.toBe(1);
+        expect($(".mkwsPrev").length).not.toBe(1);
     });
 
     it("run search query", function () {
-        var search_query = "freebsd"; // short hit counter with some paging
-        $("input#mkwsQuery").val(search_query);
+        var search_query = jasmine_config.search_query; // short hit counter with some paging
+        $("input.mkwsQuery").val(search_query);
         debug("set search query: " + search_query)
-        expect($("input#mkwsQuery").val()).toMatch("^" + search_query + "$");
+        expect($("input.mkwsQuery").val()).toMatch("^" + search_query + "$");
 
-        if (mkws_config.use_service_proxy) {
+        if (mkws.config.use_service_proxy) {
             // wait for service proxy auth
             waitsFor(function () {
                 return mkws.authenticated;
-            }, "SP auth done", 10 * 1000);
+            }, "SP auth done", 10 * jasmine_config.second);
         } else {
             debug("running raw pp2, don't wait for mkws auth");
         }
 
         runs(function () {
             debug("Click on submit button");
-            var click = $("input#mkwsButton").trigger("click");
-            expect(click.length).toBe(1);
+            $("input.mkwsButton").trigger("click");
         })
     });
 });
 
+describe("Check MOTD after search", function () {
+    it("MOTD is hidden", function () {
+        if (!jasmine_config.check_motd) {
+            return;
+        }
+
+        expect($(".mkwsMOTD").length).toBe(1);
+        expect($(".mkwsMOTD").is(":hidden")).toBe(true);
+        debug("motd t=" + $(".mkwsMOTD").text());
+        debug("motd v=" + $(".mkwsMOTD").is(":visible"));
+    });
+});
+
 
 /*
  * This part runs in background. It should be rewritten with
@@ -72,55 +159,50 @@ describe("Check pazpar2 search", function () {
 describe("Check pazpar2 navigation", function () {
     // Asynchronous part
     it("check running search next/prev", function () {
-        expect($("#mkwsPager").length).toBe(1);
+        expect($(".mkwsPager").length).toBe(1);
 
         function my_click(id, time) {
             setTimeout(function () {
                 debug("trigger click on id: " + id);
-                var click = $(id).trigger("click");
-
-                debug("next/prev: " + id + " click is success: " + click.length);
-                expect(click.length).toBe(1);
-            }, time * 1000);
+                $(id).trigger("click");
+            }, time * jasmine_config.second);
         }
 
         waitsFor(function () {
-            return $("div#mkwsPager div:nth-child(2) a").length >= 2 ? true : false;
-        }, "Expect next link 2", 10 * 1000);
+            return $("div.mkwsPager div:nth-child(2) a").length >= 2 ? true : false;
+        }, "Expect next link 2", 10 * jasmine_config.second);
 
         runs(function () {
             // click next/prev after N seconds
-            my_click("#mkwsNext", 0);
+            my_click(".mkwsNext", 0);
         });
 
         waitsFor(function () {
-            return $("div#mkwsPager div:nth-child(2) a").length >= 3 ? true : false;
-        }, "Expect next link 3", 5 * 1000);
+            return $("div.mkwsPager div:nth-child(2) a").length >= 3 ? true : false;
+        }, "Expect next link 3", 5 * jasmine_config.second);
 
         runs(function () {
             // click next/prev after N seconds
-            my_click("#mkwsNext", 0);
-            my_click("#mkwsPrev", 0.2);
+            my_click(".mkwsNext", 0);
+            my_click(".mkwsPrev", 0.2);
         });
     });
 });
 
 describe("Check pazpar2 hit counter", function () {
     it("check running search hit counter", function () {
-        var max_time = 16; // in seconds
-        var expected_hits = 80; // at least expected hit counter
+        var max_time = jasmine_config.max_time; // in seconds
+        var expected_hits = jasmine_config.expected_hits; // at least expected hit counter
         var hits = 0;
 
         waitsFor(function () {
             hits = get_hit_counter();
-
             return hits > expected_hits;
-        }, "Expect " + expected_hits + " hits", max_time * 1000);
-
+        }, "Expect " + expected_hits + " hits", max_time * jasmine_config.second);
 
         runs(function () {
             debug("mkws pager found records: '" + hits + "'");
-            expect($("#mkwsPager").length).toBe(1);
+            expect($(".mkwsPager").length).toBe(1);
             expect(hits).toBeGreaterThan(expected_hits);
         });
     });
@@ -128,84 +210,206 @@ describe("Check pazpar2 hit counter", function () {
 
 describe("Check Termlist", function () {
     it("found Termlist", function () {
-        var termlist = $("div#mkwsTermlists");
+        var termlist = $("div.mkwsTermlists");
         debug("Termlist success: " + termlist.length);
         expect(termlist.length).toBe(1);
 
         waitsFor(function () {
-            return $("div#mkwsFacetSources").length == 1 ? true : false;
-        }, "check for facet sources", 2 * 1000);
-
+            return $("div.mkwsFacet[data-mkws-facet='xtargets']").length == 1 ? true : false;
+        }, "check for facet sources", 4 * jasmine_config.second);
 
         // everything displayed?
         runs(function () {
-            var sources = $("div#mkwsFacetSources");
+            var sources = $("div.mkwsFacet[data-mkws-facet='xtargets']");
             debug("Termlist sources success: " + sources.length);
             expect(sources.length).toBe(1);
 
-            var subjects = $("div#mkwsFacetSubjects");
+            var subjects = $("div.mkwsFacet[data-mkws-facet='subject']");
             expect(subjects.length).toBe(1);
 
-            var authors = $("div#mkwsFacetAuthors");
+            var authors = $("div.mkwsFacet[data-mkws-facet='author']");
             expect(authors.length).toBe(1);
         });
 
         waitsFor(function () {
-            return $("div#mkwsFacetAuthors div.term").length >= 2 ? true : false;
-        }, "At least one author link displayed", 2 * 1000);
+            return $("div.mkwsFacet[data-mkws-facet='author'] div.term").length >= 2 ? true : false;
+        }, "At least one author link displayed", 4 * jasmine_config.second);
 
         runs(function () {
-            expect($("div#mkwsFacetAuthors div.term").length).toBeGreaterThan(1);
+            expect($("div.mkwsFacet[data-mkws-facet='author'] div.term").length).toBeGreaterThan(1);
         });
     });
+});
 
+describe("Check Author Facets", function () {
     it("limit search to first author", function () {
+        if (mkws.config.disable_facet_authors_search) {
+            debug("Facets: ignore limit search for authors");
+            return;
+        }
+
         var hits_all_targets = get_hit_counter();
+        var author_number = 2; // 2=first author
+        // do not click on author with numbers, e.g.: "Bower, James M. Beeman, David, 1938-"
+        // do not click on author names without a comma, e.g.: "Joe Barbara"
+        // because searching on such authors won't find anything.
+        runs(function () {
+            var terms = $("div.mkwsFacet[data-mkws-facet='author'] div.term a");
+            for (var i = 0; i < terms.length; i++) {
+                var term = $(terms[i]).text();
+                if (term.match(/[0-9].+[0-9]/i) || !term.match(/,/)) {
+                    debug("ignore author facet: " + term);
+                    author_number++;
+                } else {
+                    break;
+                }
+            }
+            if ($("div.mkwsFacet[data-mkws-facet='author'] div.term:nth-child(" + author_number + ") a").text().length == 0) {
+                debug("No good authors found. Not clicking on the bad ones");
+                return;
+            }
 
-        var click = $("div#mkwsFacetAuthors div.term:nth-child(2) a").trigger("click");
-        debug("limit author click is success: " + click.length);
-        expect(click.length).toBe(1);
+            debug("Clicking on author (" + author_number + ") " + $("div.mkwsFacet[data-mkws-facet='author'] div.term:nth-child(" + author_number + ") a").text());
+            $("div.mkwsFacet[data-mkws-facet='author'] div.term:nth-child(" + author_number + ") a").trigger("click");
+        });
 
         waitsFor(function () {
-            return get_hit_counter() < hits_all_targets ? true : false;
-        }, "Limited author search for less than " + hits_all_targets + " hits", 8 * 1000);
+            var hits_single_target = get_hit_counter();
+            return hits_single_target > 0 && hits_single_target < hits_all_targets ? true : false;
+        }, "Limited author search for less than " + hits_all_targets + " hits", 4.5 * jasmine_config.second);
 
         runs(function () {
             var hits_single_target = get_hit_counter();
             debug("get less hits for authors: " + hits_all_targets + " > " + hits_single_target);
-            expect(hits_all_targets).toBeGreaterThan(hits_single_target);
         });
     });
+});
+
+describe("Check active clients author", function () {
+    it("check for active clients after limited author search", function () {
+        waitsFor(function () {
+            var clients = $("div.mkwsStat span.clients");
+            // debug("clients: " + clients.text());
+            return clients.length == 1 && clients.text().match("/[1-9]+[0-9]*$");
+        }, "wait for Active clients: x/y", 5.5 * jasmine_config.second);
 
+        runs(function () {
+            var clients = $("div.mkwsStat span.clients");
+            debug("span.clients: " + clients.text());
+            expect(clients.text()).toMatch("/[1-9]+[0-9]*$");
+
+            // exact match of active clients (e.g. a SP misconfiguration)
+            if (jasmine_config.active_clients) {
+                debug("check for " + jasmine_config.active_clients + " active connections");
+                expect(clients.text()).toMatch(" [0-9]+/" + jasmine_config.active_clients + "$");
+            }
+        });
+    });
+});
+
+describe("Check Source Facets", function () {
     it("limit search to first source", function () {
         var hits_all_targets = get_hit_counter();
         var source_number = 2; // 2=first source
-        var source_name = $("div#mkwsFacetSources div.term:nth-child(" + source_number + ") a").text();
+        // wait for a stat response
+        var waitcount = 0;
         // do not click on wikipedia link - no author or subject facets possible
-        if (source_name.match(/wikipedia/i)) {
-            source_number++;
-        }
+        var link = "div.mkwsFacet[data-mkws-facet='xtargets'] div.term a";
 
-        var click = $("div#mkwsFacetSources div.term:nth-child(" + source_number + ") a").trigger("click");
-        debug("limit source click " + (source_number - 1) + " is success: " + click.length);
-        expect(click.length).toBe(1);
+        // wait for a visible source link in facets
+        waitsFor(function () {
+            var terms = $(link);
+            return terms && terms.length > 0;
+        }, "wait for source facets after author search", 5 * jasmine_config.second);
+
+
+        runs(function () {
+            var terms = $(link);
+            for (var i = 0; i < terms.length; i++) {
+                var term = $(terms[i]).text();
+                debug("check for good source: " + term);
+
+                if (term.match(/wikipedia/i)) {
+                    debug("ignore source facet: " + term);
+                    source_number++;
+                } else {
+                    break;
+                }
+            }
+            debug("Source counter: " + terms.length + ", select: " + (source_number - 1));
+
+            if ($("div.mkwsFacet[data-mkws-facet='xtargets'] div.term:nth-child(" + source_number + ") a").text().length == 0) {
+                debug("No good source found. Not clicking on the bad ones");
+                return;
+            }
+
+            debug("click on source link nth-child(): " + source_number);
+            $("div.mkwsFacet[data-mkws-facet='xtargets'] div.term:nth-child(" + source_number + ") a").trigger("click");
+
+            $(".mkwsPager").bind("DOMNodeInserted DOMNodeRemoved propertychange", function () {
+                waitcount++;
+                debug("DOM wait for stat: " + waitcount);
+            });
+        });
 
         waitsFor(function () {
-            if ($("div#mkwsNavi").length && $("div#mkwsNavi").text().match(/Source: /)) {
+            if ($("div.mkwsNavi").length && $("div.mkwsNavi").text().match(/(Source|datenquelle|kilder): /i)) {
                 return true;
             } else {
                 return false;
             }
-        }, "Search for source in navi bar", 1000);
+        }, "Search for source in navi bar", 4 * jasmine_config.second);
 
+        // Note: it may happens that limited source search returns the same number of hits
+        // as before. Thats not really an error, but unfortunate
         waitsFor(function () {
-            return get_hit_counter() < hits_all_targets ? true : false;
-        }, "Limited source earch for less than " + hits_all_targets + " hits", 9 * 1000);
+            var hits_single_target = get_hit_counter();
+
+            return waitcount >= 2 && hits_single_target > 0 && hits_single_target <= hits_all_targets ? true : false;
+        }, "Limited source search for less than " + hits_all_targets + " hits", 5 * jasmine_config.second);
 
         runs(function () {
             var hits_single_target = get_hit_counter();
-            debug("get less hits for sources: " + hits_all_targets + " > " + hits_single_target);
-            expect(hits_all_targets).toBeGreaterThan(hits_single_target);
+            debug("get less hits for sources: " + hits_all_targets + " >= " + hits_single_target);
+            expect(hits_all_targets).not.toBeLessThan(hits_single_target);
+            jasmine_status.source_click = 1;
+
+            $(".mkwsPager").unbind("DOMNodeInserted DOMNodeRemoved propertychange");
+        });
+    });
+});
+
+
+describe("Check record list", function () {
+    it("check for single active client", function () {
+        if (!jasmine_status.source_click) {
+            debug("skip clients check due missing source click");
+            return;
+        }
+
+        waitsFor(function () {
+            var clients = $("div.mkwsStat span.clients");
+            //debug("clients: " + clients.text());
+            return clients.length == 1 && clients.text().match("/1$");
+        }, "wait for Active clients: x/1", 5 * jasmine_config.second);
+
+        runs(function () {
+            var clients = $("div.mkwsStat span.clients");
+            debug("span.clients: " + clients.text());
+            expect(clients.text()).toMatch("/1$");
+        });
+    });
+
+    it("got a record", function () {
+        var linkaddr = "div.mkwsRecords div.record:nth-child(1) a";
+
+        waitsFor(function () {
+            // remove + insert node: must be at least 2
+            return $(linkaddr).length > 0;
+        }, "wait until we see a new record", 2.5 * jasmine_config.second);
+
+        runs(function () {
+            expect($(linkaddr).length).toBeGreaterThan(0);
         });
     });
 });
@@ -213,73 +417,76 @@ describe("Check Termlist", function () {
 describe("Show record", function () {
     var record_number = 1; // the Nth record in hit list
     it("show record author", function () {
-        var click = $("div#mkwsRecords div.record:nth-child(" + record_number + ") a").trigger("click");
+        var click = $("div.mkwsRecords div.record:nth-child(" + record_number + ") a").trigger("click");
         debug("show record click is success: " + click.length);
         expect(click.length).toBe(1);
 
         // wait until the record pops up
         waitsFor(function () {
-            var show = $("div#mkwsRecords div.record:nth-child(" + record_number + ") div");
+            var show = $("div.mkwsRecords div.record:nth-child(" + record_number + ") > div.details");
+            //debug("poprecord: " + (show ? show.length : -1) + " " + $("div.mkwsRecords div.record").text());
             return show != null && show.length ? true : false;
-        }, "wait some miliseconds to show up a record", 2 * 1000);
+        }, "wait some miliseconds to show up a record", 2 * jasmine_config.second);
 
         runs(function () {
             debug("show record pop up");
-            expect($("div#mkwsRecords div.record:nth-child(" + record_number + ") div")).not.toBe(null);
+            expect($("div.mkwsRecords div.record:nth-child(" + record_number + ") div")).not.toBe(null);
         });
     });
 
     it("extract URL", function () {
-        if (mkws_config.jasmine && mkws_config.jasmine.show_record_url == false) {
+        if (jasmine_config.show_record_url == false) {
             debug("ignore test for URL in record")
             return;
         }
 
-        var url = $("div#mkwsRecords div.record:nth-child(" + record_number + ") div table tbody tr td a").text();
-        debug("extracted URL from record: " + url);
+        var urls = $("div.mkwsRecords div.record:nth-child(" + record_number + ") div table tbody tr td a");
+        debug("number of extracted URL from record: " + urls.length);
+        // expect(urls.length).toBeGreaterThan(0); // LoC has records without links
+        for (var i = 0; i < urls.length; i++) {
+            var url = $(urls[i]);
+            debug("URL: " + url.attr('href') + " text: " + url.text());
 
-        expect(url).not.toBe(null);
-        expect(url).toMatch(/^http:\/\/[a-z0-9]+\.[0-9a-z].*\//i);
+            expect(url.attr('href')).not.toBe(null);
+            expect(url.attr('href')).toMatch(/^https?:\/\/[a-z0-9\-]+\.[0-9a-z].*\//i);
+            expect(url.text()).not.toBe("");
+        }
     });
 });
 
 describe("Check switch menu Records/Targets", function () {
     it("check mkwsSwitch", function () {
-        expect($("div#mkwsSwitch").length).toBe(1);
+        expect($("div.mkwsSwitch").length).toBe(1);
 
         // expect 2 clickable links
-        expect($("div#mkwsSwitch a").length).toBe(2);
+        expect($("div.mkwsSwitch a").length).toBe(2);
     });
 
     it("switch to target view", function () {
-        var click = $("a#mkwsSwitch_targets").trigger("click");
-        debug("target view click is success: " + click.length);
-        expect(click.length).toBe(1);
+        $("div.mkwsSwitch").children('a').eq(1).trigger("click");
 
         // now the target table must be visible
-        expect($("div#mkwsBytarget").is(":visible")).toBe(true);
-        expect($("div#mkwsRecords").is(":visible")).toBe(false);
+        expect($("div.mkwsBytarget").is(":visible")).toBe(true);
+        expect($("div.mkwsRecords").is(":visible")).toBe(false);
 
         // wait a half second, to show the target view
         var time = (new Date).getTime();
         waitsFor(function () {
             return (new Date).getTime() - time > 700 ? true : false;
-        }, "wait some miliseconds", 1 * 1000);
+        }, "wait some miliseconds", 1 * jasmine_config.second);
 
         // look for table header
         runs(function () {
-            expect($("div#mkwsBytarget").html()).toMatch(/Target ID/);
+            expect($("div.mkwsBytarget").html()).toMatch(/Target ID/);
         });
     });
 
     it("switch back to record view", function () {
-        var click = $("a#mkwsSwitch_records").trigger("click");
-        debug("record view click is success: " + click.length);
-        expect(click.length).toBe(1);
+        $("div.mkwsSwitch").children('a').eq(0).trigger("click");
 
         // now the target table must be visible
-        expect($("div#mkwsBytarget").is(":visible")).toBe(false);
-        expect($("div#mkwsRecords").is(":visible")).toBe(true);
+        expect($("div.mkwsBytarget").is(":visible")).toBe(false);
+        expect($("div.mkwsRecords").is(":visible")).toBe(true);
     });
 });
 
@@ -291,29 +498,32 @@ describe("Check status client counter", function () {
     var time = get_time();
 
     it("check status clients", function () {
+        if (!jasmine_status.source_click) {
+            debug("skip clients check due missing source click");
+            return;
+        }
+
         waitsFor(function () {
-            var clients = $("div#mkwsStat span.clients");
+            var clients = $("div.mkwsStat span.clients");
+            debug("clients: " + clients.text());
             if (clients.length == 1 && clients.text().match("0/1$")) {
                 return true;
             } else {
                 return false;
             }
+        }, "wait for Active clients: 0/1", 4 * jasmine_config.second);
 
-        }, "wait for Active clients: 0/1", 4 * 1000);
-
-/*
         runs(function () {
-            var clients = $("div#mkwsStat span.clients");
+            var clients = $("div.mkwsStat span.clients");
             debug("span.clients: " + clients.text());
-            expect(clients.text()).toEqual("0/1");
+            expect(clients.text()).toMatch("0/1$");
         });
-        */
-
     });
-
 });
 
-/* dummy EOF */
+/* done */
 describe("All tests are done", function () {
-    it(">>> hooray <<<", function () {});
+    it(">>> hooray <<<", function () {
+        mkws.jasmine_done = true;
+    });
 });
diff --git a/test/spec/mkws_utils.js b/test/spec/mkws_utils.js
deleted file mode 100644 (file)
index 3256f70..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/* Copyright (c) 2013 IndexData ApS. http://indexdata.com
- *
- * helper functions for other test *.spec.js files
- *
- */
-
-/*
- * combine arrays, return a flat list
- * [["a","b"], ["c"], "d"] => ["a", "b", "c", "d"]
- *
- */
-var flat_list = function (list) {
-        var data = [];
-
-        for (var i = 0; i < list.length; i++) {
-            if (typeof list[i] == 'object') {
-                for (var j = 0; j < list[i].length; j++) {
-                    data.push(list[i][j]);
-                }
-
-            } else {
-                data.push(list[i]);
-            }
-        }
-
-        return data;
-    };
-
-/*
- * list of div id to check
- *
- */
-var tags = {
-    required: ["mkwsSearch", "mkwsResults"],
-    optional: ["mkwsLang", "mkwsTargets"],
-    optional2: ["mkwsMOTD", "mkwsStat", "footer"]
-};
-
-// node.js exports
-module.exports = {
-    flat_list: flat_list,
-    tags: tags
-};
index 351fc7f..c87487d 100644 (file)
@@ -1,5 +1,8 @@
 You will need to enable the Rewrite module for this to work:
 
-$ sudo a2enmod rewrite
+$ sudo a2enmod rewrite headers proxy deflate
 $ sudo service apache2 reload
 
+There are example configurations under examples/apache2
+
+Most developers have their own setup under tools/apache2
diff --git a/tools/apache2/jasmine-dev.template b/tools/apache2/jasmine-dev.template
new file mode 100644 (file)
index 0000000..a2aac5a
--- /dev/null
@@ -0,0 +1,73 @@
+# Configuration for the apache web server                 -*- apache -*-
+
+#####################################################################
+# global apache2 config
+#
+User ${APACHE_RUN_USER}
+Group ${APACHE_RUN_GROUP}
+PidFile ${APACHE_PID_FILE}
+
+ServerName localhost
+ServerRoot ${APACHE_SERVER_ROOT}
+NameVirtualHost *:${APACHE_PORT}
+Listen ${APACHE_PORT}
+
+LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
+
+Include /etc/apache2/mods-available/alias.load
+Include /etc/apache2/mods-available/authz*.load
+Include /etc/apache2/mods-available/proxy*.load
+Include /etc/apache2/mods-available/rewrite.load
+Include /etc/apache2/mods-available/headers.load
+Include /etc/apache2/mods-available/mime.load
+Include /etc/apache2/mods-available/deflate.load
+
+Include /etc/apache2/mods-available/alias*.conf
+Include /etc/apache2/mods-available/authz*.conf
+Include /etc/apache2/mods-available/proxy*.conf
+Include /etc/apache2/mods-available/mime.conf
+Include /etc/apache2/mods-available/deflate.conf
+
+# pazpar2 / service proxy config
+<VirtualHost *:${APACHE_PORT}>
+    ServerName localhost
+    ServerAlias mkws-dev a.mkws.indexdata.com a.mkws-dev.indexdata.com mkws-dev.indexdata.com 127.0.0.1
+
+    ServerAdmin webmaster@indexdata.com
+    ErrorLog ${APACHE_LOG_DIR}/mkws-jasmine-error.log
+    CustomLog ${APACHE_LOG_DIR}/mkws-jasmine-access.log combined
+
+    RewriteEngine on
+    RewriteLogLevel 1
+    RewriteLog ${APACHE_LOG_DIR}/mkws-jasmine-rewrite.log 
+
+    DocumentRoot ${MKWS_ROOT}/examples/htdocs
+    Alias /tools/htdocs ${MKWS_ROOT}/tools/htdocs
+    Alias /src ${MKWS_ROOT}/src
+    Alias /test ${MKWS_ROOT}/test
+    Alias /jasmine ${MKWS_ROOT}/examples/jasmine
+
+    # CORS setting
+    Header set Access-Control-Allow-Credentials true
+    Header set Access-Control-Allow-Origin "*"
+
+    # compress text output
+    <Location />
+        AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml
+        SetOutputFilter DEFLATE 
+    </Location> 
+
+    # jasmine test account
+    RewriteRule /service-proxy-testauth  /service-proxy/?command=auth&action=login&username=mkwstest&password=mkwstest [P] # [NE,P]
+
+    # mkws devel account (e.g. memached testing)
+    RewriteRule /service-proxy-auth  /service-proxy/?command=auth&action=login&username=mkwsdev&password=mkwsdev [P] # [NE,P]
+
+    ProxyPass        /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
+    ProxyPassReverse /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
+
+    ProxyPass        /pazpar2/         http://localhost:8004/pazpar2/
+    ProxyPassReverse /pazpar2/         http://localhost:8004/pazpar2/
+
+</VirtualHost>
+
index 6d19365..9805492 100644 (file)
@@ -2,9 +2,10 @@
 
 # pazpar2 / service proxy config
 <VirtualHost *:80>
-    ServerName spclient.example.com
-    ServerAlias spclient-dev.indexdata.com mkws-dev.indexdata.com
-    ServerAdmin webmaster@example.com
+    ServerName mkws-dev.indexdata.com
+    ServerAlias mkws-dev a.mkws.indexdata.com a.mkws-dev.indexdata.com
+
+    ServerAdmin webmaster@indexdata.com
     ErrorLog /var/log/apache2/mkws-dev-error.log
     CustomLog /var/log/apache2/mkws-dev-access.log combined
 
     RewriteLogLevel 1
     RewriteLog /var/log/apache2/mkws-dev-rewrite.log 
 
-    DocumentRoot /home/indexdata/mkws/examples/htdocs
-    Alias /tools/htdocs /home/indexdata/mkws/tools/htdocs
+    DocumentRoot /home/indexdata/mkws-dev/examples/htdocs
+    Alias /tools/htdocs /home/indexdata/mkws-dev/tools/htdocs
+    Alias /src /home/indexdata/mkws-dev/src
+    Alias /test /home/indexdata/mkws-dev/test
+    Alias /jasmine /home/indexdata/mkws-dev/examples/jasmine
+
+    # CORS setting
+    Header set Access-Control-Allow-Credentials true
+    Header set Access-Control-Allow-Origin "*"
 
     # compress text output
     <Location />
         SetOutputFilter DEFLATE 
     </Location> 
 
-    RewriteRule /service-proxy-auth  /service-proxy/?command=auth&action=login&username=guest&password=guest [P] # [NE,P]
+    # jasmine test account
+    RewriteRule /service-proxy-testauth  /service-proxy/?command=auth&action=login&username=mkwstest&password=mkwstest [P] # [NE,P]
+
+    # mkws devel account (e.g. memached testing)
+    RewriteRule /service-proxy-auth  /service-proxy/?command=auth&action=login&username=mkwsdev&password=mkwsdev [P] # [NE,P]
 
     ProxyPass        /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
     ProxyPassReverse /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
diff --git a/tools/apache2/mkws-dev-proxy b/tools/apache2/mkws-dev-proxy
deleted file mode 100644 (file)
index ef05c02..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<VirtualHost *:80>
-    ServerName mkws-dev.indexdata.com
-    ServerAlias spclient-dev.indexdata.com
-
-    ProxyRequests off
-    ProxyVia On
-    ProxyPreserveHost On
-    <Proxy *>
-      Order deny,allow
-      Allow from all
-    </Proxy>
-
-    ProxyPass         / http://dart:80/
-    ProxyPassReverse  / http://dart:80/
-
-    # These are the logs for the proxying operation
-    ErrorLog /var/log/apache2/mkws-dev-proxy-error.log
-    CustomLog /var/log/apache2/mkws-dev-proxy-access.log combined
-</VirtualHost>
diff --git a/tools/apache2/mkws-dev-px b/tools/apache2/mkws-dev-px
new file mode 100644 (file)
index 0000000..07ad2a8
--- /dev/null
@@ -0,0 +1,19 @@
+<VirtualHost *:80>
+    ServerName mkws-dev.indexdata.com
+    ServerAlias mkws-test.indexdata.com spclient-dev.indexdata.com
+
+    ProxyRequests off
+    ProxyVia On
+    ProxyPreserveHost On
+    <Proxy *>
+      Order deny,allow
+      Allow from all
+    </Proxy>
+
+    ProxyPass         / http://dart:80/
+    ProxyPassReverse  / http://dart:80/
+
+    # These are the logs for the proxying operation
+    ErrorLog /var/log/apache2/mkws-dev-px-error.log
+    CustomLog /var/log/apache2/mkws-dev-px-access.log combined
+</VirtualHost>
diff --git a/tools/apache2/mkws-dev-ssl-px b/tools/apache2/mkws-dev-ssl-px
new file mode 100644 (file)
index 0000000..e3d9e83
--- /dev/null
@@ -0,0 +1,48 @@
+# A very simple configuration to proxy the irspy
+
+<VirtualHost *:443>
+    ServerName mkws.indexdata.com 
+    ServerAlias mkws-dev.indexdata.com mkws-test.indexdata.com
+
+  <IfModule mod_ssl.c>
+    SSLEngine on
+    SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown
+
+    #SSLCertificateFile /etc/ssl/certs/indexdata.com/id.cert
+    #SSLCertificateKeyFile /etc/ssl/certs/indexdata.com/id.key
+
+    SSLProxyEngine on
+  </IfModule>
+
+    # Remove the X-Forwarded-For header, as the proxy appends to it,
+    # and we need a clean ip address for the statistics
+    # RequestHeader unset X-Forwarded-For early
+    # Never mind
+
+    # ProxyRequests off
+    <Proxy *>
+      Order deny,allow
+      Allow from all
+    </Proxy>
+
+    ProxyPreserveHost On
+    ProxyPass         / http://dart/
+    ProxyPassReverse  / http://dart/
+
+    # Experiments to hunt down bu 3716
+    # Increase buffer size so that we don't go for chunked stuff
+    # ProxyIOBufferSize 8192
+    # Didn't help
+    # Disable gzipping
+    # RequestHeader unset Accept-Encoding
+    # Didn't help 
+    # ProxyReceiveBufferSize 8192
+    # Didn't help 
+    SetEnv force-proxy-request-1.0 1
+    SetEnv proxy-nokeepalive 1
+
+    # These are the logs for the proxying operation
+    ErrorLog     /var/log/apache2/mkws-dev-ssl-error.log
+    CustomLog    /var/log/apache2/mkws-dev-ssl-access.log combined
+</VirtualHost>
+
diff --git a/tools/apache2/mkws-heikki b/tools/apache2/mkws-heikki
new file mode 100644 (file)
index 0000000..9206f3b
--- /dev/null
@@ -0,0 +1,55 @@
+# Apache config for Heikki's workstation (tapas)
+# May also work on Heikki's home machine (corelli) and laptop (tsatsiki)
+# Needs the host name to be defined in /etc/hosts
+# cd /etc/apache2/sites-available
+# sudo ln -s /home/heikki/proj/mkws/tools/apache2/mkws-heikki .
+# sudo a2ensite mkws-heikki
+# sudo service apache2 reload
+# (make sure the path from /home to .../htdocs is world-readable!)
+# 
+
+<VirtualHost *:80>
+  ServerName mkws-heikki.localdomain
+  ServerAlias mkws mkws-heikki
+
+  RewriteEngine on
+  RewriteLogLevel 1
+  RewriteLog /var/log/apache2/mkws-dev-rewrite.log
+
+  DocumentRoot /home/heikki/proj/mkws/examples/htdocs
+  Alias /tools/htdocs /home/heikki/proj/mkws/tools/htdocs
+  Alias /src /home/heikki/proj/mkws/src
+  Alias /test /home/heikki/proj/mkws/test
+  Alias /jasmine /home/heikki/proj/mkws/examples/jasmine
+
+  # allow cors
+  Header set Access-Control-Allow-Credentials true
+  Header set Access-Control-Allow-Origin "*"
+
+  # compress text output
+  <Location />
+    AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml
+    SetOutputFilter DEFLATE
+  </Location>
+
+  RewriteRule /service-proxy-auth/ /service-proxy/?command=auth&action=login&username=demo&password=demo [P] # [NE,P]
+
+  # For MKC Service Proxy
+    ProxyPass        /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
+    ProxyPassReverse /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
+
+    ProxyPass        /pazpar2/         http://localhost:8004/pazpar2/
+    ProxyPassReverse /pazpar2/         http://localhost:8004/pazpar2/
+
+#  ProxyPass        /service-proxy/ http://mk2-test.indexdata.com/service-proxy/
+#  ProxyPassReverse /service-proxy/ http://mk2-test.indexdata.com/service-proxy/
+
+  <Directory />
+    AllowOverride None
+    Options FollowSymLinks
+    Order allow,deny
+    Allow from all
+  </Directory>
+
+</VirtualHost>
+
index e3ed207..0b48898 100644 (file)
     RewriteEngine on
     RewriteLogLevel 1
     RewriteLog /var/log/apache2/mkws-rewrite.log 
-    RewriteRule /service-proxy-auth /service-proxy/?command=auth&action=login&username=demo&password=demo [P] # [NE,P]
+
+    # standard MKWS account
+    RewriteRule /service-proxy-auth /service-proxy/?command=auth&action=login&username=mkws&password=mkws [P] # [NE,P]
+
+    # jasmine test account
+    RewriteRule /service-proxy-testauth  /service-proxy/?command=auth&action=login&username=mkwstest&password=mkwstest [P] # [NE,P]
 
     # The following rule allows the server to accept service-proxy
     # requests that begin with an escaped "%3F" rather than a literal
@@ -25,8 +30,8 @@
     Header set Access-Control-Allow-Credentials true
 
     # For MKC Service Proxy
-    ProxyPass        /service-proxy/ http://mk2-test.indexdata.com/service-proxy/
-    ProxyPassReverse /service-proxy/ http://mk2-test.indexdata.com/service-proxy/
+    ProxyPass        /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
+    ProxyPassReverse /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
 
     PerlOptions +Parent
     PerlSwitches -I/home/indexdata/mkws/tools/mod_perl
index c6ade11..d781dcb 100644 (file)
     CustomLog /opt/local/apache2/logs/example-access.log combined
 
     DocumentRoot /usr/local/src/git/mkws/examples/htdocs
+    Alias /tools/htdocs/ /usr/local/src/git/mkws/tools/htdocs/
+    Alias /src/ /usr/local/src/git/mkws/src/
+    Alias /jasmine/ /usr/local/src/git/mkws/examples/jasmine/
+    Alias /test/ /usr/local/src/git/mkws/test/
 
     # Needed on Mac, which locks Apache down hard by default.
     <Directory />
diff --git a/tools/apache2/mkws-ne b/tools/apache2/mkws-ne
new file mode 100644 (file)
index 0000000..6b613fd
--- /dev/null
@@ -0,0 +1,45 @@
+# Configuration for the apache web server                 -*- apache -*-
+
+# pazpar2 / service proxy config
+<VirtualHost *:80>
+    ServerName mkws-ne
+    ServerAlias mkws-ne
+
+    ServerAdmin webmaster@indexdata.com
+    ErrorLog /var/log/apache2/mkws-dev-error.log
+    CustomLog /var/log/apache2/mkws-dev-access.log combined
+
+    RewriteEngine on
+    RewriteLogLevel 1
+    RewriteLog /var/log/apache2/mkws-dev-rewrite.log
+
+    DocumentRoot /indexdata/gitprojects/mkws/examples/htdocs
+    Alias /tools/htdocs /indexdata/gitprojects/mkws/tools/htdocs
+    Alias /src /indexdata/gitprojects/mkws/src
+    #Alias /test /home/indexdata/mkws-dev/test
+    #Alias /jasmine /home/indexdata/mkws-dev/examples/jasmine
+
+    # CORS setting
+    #Header set Access-Control-Allow-Credentials true
+    #Headers set Access-Control-Allow-Origin "*"
+
+    # compress text output
+    <Location />
+        AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml
+        SetOutputFilter DEFLATE
+    </Location>
+
+    # jasmine test account
+    RewriteRule /service-proxy-testauth  /service-proxy/?command=auth&action=login&username=mkwstest&password=mkwstest [P] # [NE,P]
+
+    # mkws devel account (e.g. memached testing)
+    RewriteRule /service-proxy-auth  /service-proxy/?command=auth&action=login&username=mkwsdev&password=mkwsdev [P] # [NE,P]
+
+    ProxyPass        /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
+    ProxyPassReverse /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
+
+    ProxyPass        /pazpar2/         http://mk2-test.indexdata.com/test-pazpar2/
+    ProxyPassReverse /pazpar2/         http://mk2-test.indexdata.com/test-pazpar2/
+
+</VirtualHost>
+
diff --git a/tools/apache2/mkws-ssl-px b/tools/apache2/mkws-ssl-px
new file mode 100644 (file)
index 0000000..d160cc1
--- /dev/null
@@ -0,0 +1,48 @@
+# A very simple configuration to proxy the irspy
+
+<VirtualHost *:443>
+    ServerName mkws.indexdata.com 
+    ServerAlias mkws-dev.indexdata.com
+
+  <IfModule mod_ssl.c>
+    SSLEngine on
+    SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown
+
+    #SSLCertificateFile /etc/ssl/certs/indexdata.com/id.cert
+    #SSLCertificateKeyFile /etc/ssl/certs/indexdata.com/id.key
+
+    SSLProxyEngine on
+  </IfModule>
+
+    # Remove the X-Forwarded-For header, as the proxy appends to it,
+    # and we need a clean ip address for the statistics
+    # RequestHeader unset X-Forwarded-For early
+    # Never mind
+
+    # ProxyRequests off
+    <Proxy *>
+      Order deny,allow
+      Allow from all
+    </Proxy>
+
+    ProxyPreserveHost On
+    ProxyPass         / http://caliban/
+    ProxyPassReverse  / http://caliban/
+
+    # Experiments to hunt down bu 3716
+    # Increase buffer size so that we don't go for chunked stuff
+    # ProxyIOBufferSize 8192
+    # Didn't help
+    # Disable gzipping
+    # RequestHeader unset Accept-Encoding
+    # Didn't help 
+    # ProxyReceiveBufferSize 8192
+    # Didn't help 
+    SetEnv force-proxy-request-1.0 1
+    SetEnv proxy-nokeepalive 1
+
+    # These are the logs for the proxying operation
+    ErrorLog     /var/log/apache2/mkws-ssl-error.log
+    CustomLog    /var/log/apache2/mkws-ssl-access.log combined
+</VirtualHost>
+
diff --git a/tools/apache2/mkws-test b/tools/apache2/mkws-test
new file mode 100644 (file)
index 0000000..495b6b7
--- /dev/null
@@ -0,0 +1,45 @@
+# Configuration for the apache web server                 -*- apache -*-
+
+# pazpar2 / service proxy config
+<VirtualHost *:80>
+    ServerName mkws-test.indexdata.com
+    ServerAlias mkws-test spclient-dev.indexdata.com
+    ServerAdmin webmaster@indexdata.com
+    ErrorLog /var/log/apache2/mkws-test-error.log
+    CustomLog /var/log/apache2/mkws-test-access.log combined
+
+    RewriteEngine on
+    RewriteLogLevel 1
+    RewriteLog /var/log/apache2/mkws-test-rewrite.log 
+
+    DocumentRoot /home/indexdata/mkws-test/examples/htdocs
+    Alias /tools/htdocs /home/indexdata/mkws-test/tools/htdocs
+    Alias /src /home/indexdata/mkws-test/src
+    Alias /test /home/indexdata/mkws-test/test
+    Alias /jasmine /home/indexdata/mkws-test/examples/jasmine
+
+
+    # CORS setting
+    Header set Access-Control-Allow-Credentials true
+    Header set Access-Control-Allow-Origin "*"
+
+    # compress text output
+    <Location />
+        AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml
+        SetOutputFilter DEFLATE 
+    </Location> 
+
+    # standard MKWS account
+    RewriteRule /service-proxy-auth  /service-proxy/?command=auth&action=login&username=mkws&password=mkws [P] # [NE,P]
+
+    # jasmine test account
+    RewriteRule /service-proxy-testauth  /service-proxy/?command=auth&action=login&username=mkwstest&password=mkwstest [P] # [NE,P]
+
+    ProxyPass        /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
+    ProxyPassReverse /service-proxy/ http://sp-mkc.indexdata.com/service-proxy/
+
+    ProxyPass        /pazpar2/         http://localhost:8004/pazpar2/
+    ProxyPassReverse /pazpar2/         http://localhost:8004/pazpar2/
+
+</VirtualHost>
+
diff --git a/tools/bin/mkws-bootstrap.sh b/tools/bin/mkws-bootstrap.sh
new file mode 100755 (executable)
index 0000000..6473ceb
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+# Copyright (c) 2010-2013 by Index Data ApS. http://www.indexdata.com
+#
+# mkws-bootstrap.sh - build the MKWS from GIT repo in a sandbox and run full tests
+#
+
+# fail on error
+set -e
+
+dir=$(mktemp -d -t mkws-bootstrap.XXXXXXXX)
+cd $dir
+
+: ${debug=""}
+
+git clone -q ssh://git.indexdata.com:222/home/git/private/mkws.git
+cd mkws
+
+test -n "$debug" && echo "start bootstraping in $dir"
+if make check phantomjs > mkws.log 2>&1; then
+    test -n "$debug" && echo "Ok"
+    test -z "$debug" && rm -rf $dir
+    exit 0
+else
+    echo "Failure, see `pwd`/mkws.log"
+    exit 1
+fi
+
diff --git a/tools/bin/nagios-service-proxy.sh b/tools/bin/nagios-service-proxy.sh
new file mode 100755 (executable)
index 0000000..2904e85
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/sh
+# Copyright (c) 2014 Index Data ApS, http://indexdata.com
+#
+# nagios test if the the service proxy is up and running
+
+set -e
+: ${mkws_host="http://mkws.indexdata.com/service-proxy/"}
+: ${mkws_username="mkws"}
+: ${mkws_password="mkws"}
+: ${user_agent="nagios service-proxy v0.9"}
+
+tempfile=$(mktemp)
+exit=0
+
+url="$mkws_host?command=auth&action=login&username=$mkws_username&password=$mkws_password"
+if curl -sSf -A "$user_agent" "$url" > $tempfile; then
+    if ! egrep -q '<status>OK</status>' $tempfile; then
+       echo "status not OK"
+       exit=1
+    fi
+    if ! egrep -q '<response jsessionId="[0-9A-F]+"' $tempfile; then
+       echo "response jsessionId missing"
+       exit=1
+    fi
+else
+    echo "URL: $url failed"
+    exit=1
+fi
+
+if [ $exit -gt 0 ]; then
+    cat $tempfile
+fi
+
+rm -f $tempfile
+exit $exit
+
index 8928913..ff5f6fa 100644 (file)
@@ -1,12 +1,14 @@
+NEWS
+README.html
+VERSION
 handlebars-v1.1.2.js
 jquery-1.10.0.min.js
 jquery.json-2.4.js
 mkws-complete.js
-mkws.min.js
 mkws-complete.min.js
-README.html
-README.odt
-README.pdf
+mkws-doc.css
+mkws-jquery.js
+mkws.js
+mkws.min.js
+pz2.js
 whitepaper.html
-whitepaper.odt
-whitepaper.pdf
diff --git a/tools/htdocs/Makefile b/tools/htdocs/Makefile
deleted file mode 100644 (file)
index 7b4fbe2..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-######################################################################
-# Copyright (c) 2013 IndexData ApS. http://indexdata.com
-#
-
-##############################
-# select a jquery version
-#
-#JQUERY_URL=   http://code.jquery.com/jquery-2.0.3.min.js
-JQUERY_URL=    http://code.jquery.com/jquery-1.10.0.min.js
-#JQUERY_URL=   http://code.jquery.com/jquery-1.9.1.min.js
-#JQUERY_URL=   http://code.jquery.com/jquery-1.8.3.min.js
-#JQUERY_URL=   http://code.jquery.com/jquery-1.7.2.min.js
-#JQUERY_URL=   http://code.jquery.com/jquery-1.6.4.min.js
-#JQUERY_URL=   http://code.jquery.com/jquery-1.4.4.min.js
-
-JQUERY_UI_URL= http://code.jquery.com/ui/1.10.3/jquery-ui.js
-#JQUERY_UI_URL=        http://code.jquery.com/ui/1.8.0/jquery-ui.min.js
-JQUERY_JSON_URL= https://jquery-json.googlecode.com/files/jquery.json-2.4.js
-HANDLEBARS_URL=        http://builds.handlebarsjs.com.s3.amazonaws.com/handlebars-v1.1.2.js -o $@
-VERSION = $(shell tr -d '\012' < VERSION)
-
-MKWS_JS=       mkws-complete.js
-PZ2API_JS=     ../../../pazpar2/js/pz2.js
-PZ2API_GIT=    ssh://git.indexdata.com:222/home/git/pub/pazpar2
-
-JQUERY_FILE := $(shell basename ${JQUERY_URL})
-JQUERY_JSON_FILE := $(shell basename ${JQUERY_JSON_URL})
-HANDLEBARS_FILE := $(shell basename ${HANDLEBARS_URL})
-##############################
-
-DOCS = README.html README.odt README.pdf \
-       whitepaper.html whitepaper.odt whitepaper.pdf
-
-# Default rule when "make" is invoked without a target
-**default**: ${MKWS_JS} mkws-js-min README.html whitepaper.html
-
-all: ${MKWS_JS} mkws-js-min $(DOCS)
-
-docs: $(DOCS)
-
-pz2api-git-checkout:
-       @if ! test -e ${PZ2API_JS}; then \
-           ( cd ../../.. && git clone ${PZ2API_GIT} ); \
-       fi
-
-mkws-js ${MKWS_JS}: Makefile mkws.js ${JQUERY_FILE} ${JQUERY_JSON_FILE} ${HANDLEBARS_FILE}
-       @if ! test -e ${PZ2API_JS}; then \
-           echo "The pazpar2 JS file ${PZ2API_JS} does not exists."; \
-           echo "Did you checked out the source from the git repo?"; \
-           echo ""; \
-           echo "Please run:"; \
-           echo "$$ make pz2api-git-checkout"; \
-           echo ""; \
-           exit 1; \
-       fi
-       ( echo "/*! Copyright (c) 2013 IndexData ApS. http://indexdata.com"; \
-         echo "   created at: $$(date)"; \
-         echo "   mkws.js GIT id: $$(git log mkws.js | head -n 1 | perl -npe 's,\S+\s+,,')"; \
-         echo "   $$(basename ${PZ2API_JS}) GIT id: $$(cd $$(dirname ${PZ2API_JS}) && git log $$(basename ${PZ2API_JS}) | head -n 1 | perl -npe 's,\S+\s+,,')"; \
-         echo "*/"; \
-         cat ${JQUERY_FILE}; \
-         cat ${JQUERY_JSON_FILE}; \
-         cat ${HANDLEBARS_FILE}; \
-         cat ${PZ2API_JS}; \
-         cat  mkws.js; \
-       ) > ${MKWS_JS}.new
-       mv -f ${MKWS_JS}.new ${MKWS_JS}
-
-mkws-js-min: mkws.min.js mkws-complete.min.js
-
-%.min.js: %.js
-       yui-compressor $? > $@.new
-       mv -f $@.new $@
-
-${JQUERY_FILE}:
-       curl -sSf ${JQUERY_URL} -o $@.new
-       perl -npe 's,sourceMappingURL=jquery.*map,,' $@.new > $@
-       rm -f $@.new
-
-${JQUERY_JSON_FILE}:
-       curl -sSf ${JQUERY_JSON_URL} -o $@
-
-${HANDLEBARS_FILE}:
-       curl -sSf ${HANDLEBARS_URL} -o $@
-
-release: mkws.js mkws-complete.js mkws.min.js mkws-complete.min.js
-       @if [ -f releases/mkws-$(VERSION).js ]; then \
-               echo "*** There is already a release $(VERSION)"; \
-       else \
-               cp -p mkws.js releases/mkws-$(VERSION).js; \
-               cp -p mkws.min.js releases/mkws-$(VERSION).min.js; \
-               cp -p mkws-complete.js releases/mkws-complete-$(VERSION).js; \
-               cp -p mkws-complete.min.js releases/mkws-complete-$(VERSION).min.js; \
-               echo "Made release $(VERSION)"; \
-       fi
-
-# For a description of pandoc's markdown format, see:
-# http://johnmacfarlane.net/pandoc/demo/example9/pandocs-markdown.html -->
-
-# for older pandoc (<1.9) run first:
-# perl -i.bak -npe 's/"(Authors|Subjects)": "(.*?)"/"$1": "test"/' tools/htdocs/whitepaper.markdown
-#
-%.html: %.markdown
-       rm -f $@
-       pandoc --standalone --toc -c mkws-doc.css $< | sed '/^<col width="[0-9]*%" \/>$//d' > $@
-       chmod ugo-w $@
-
-%.odt: %.markdown
-       rm -f $@
-       pandoc --standalone $< -o $@
-       chmod ugo-w $@
-
-# ### In order to compile the whitepaper, which has tables, to PDF,
-# you will need to install the Debian package
-#      texlive-latex-recommended
-%.pdf: %.markdown
-       rm -f $@
-       pandoc --standalone $< -o $@
-       chmod ugo-w $@
-
-##############################
-# helper targets
-#
-distclean: clean
-       rm -f *.orig *.bak *.rej
-
-clean:
-       rm -f ${JQUERY_FILE} ${JQUERY_JSON_FILE} ${HANDLEBARS_FILE}
-       rm -f mkws.min.js ${MKWS_JS} mkws-complete.min.js
-       rm -f $(DOCS)
-
-help:
-       @echo "make [ help | docs | clean ]"
-       @echo "     [ mkws-js | mkws-js-min ]"
-       @echo ""
-       @echo "make JQUERY_URL=http://code.jquery.com/jquery-2.0.3.min.js clean mkws-js"
-       @echo ""
-       @echo "Please check ./README file too!"
diff --git a/tools/htdocs/NEWS b/tools/htdocs/NEWS
deleted file mode 100644 (file)
index fc11797..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-Version history for the MasterKey Widget Set (MKWS)
-
-0.9 series
-----------
-
-The 0.9.x series of releases are beta quality. They are functional but
-have not yet been extensively battle-tested.
-
-0.9.1 Thu Dec 19 15:33:13 GMT 2013
-       - First public release.
-
diff --git a/tools/htdocs/README.markdown b/tools/htdocs/README.markdown
deleted file mode 100644 (file)
index 95f03f0..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-% The MasterKey Widget Set
-% Mike Taylor; Wolfram Schneider
-% 10 July 2013
-
-
-Introduction
-------------
-
-This is the MasterKey Widget Set. The initial version was based on the
-"jsdemo" application distributed with pazpar2, but it is now far
-removed from those beginnnings.
-
-As much of the searching functionality as possible is hosted on
-       <http://mkws.indexdata.com/>
-so that very simple websites such as
-       <http://example.indexdata.com/>
-can have MasterKey searching with minimal effort.
-
-The following files are hosted on `mkws.indexdata.com`:
-
-* `mkws.js`
-* `/pazpar2/js/pz2.js`
-* `mkws-complete.js` -- a single file consisting of `mkws.js`,
-  jQuery (which it uses), Handlebars (ditto) and `pz2.js`
-* `mkws.css`
-
-
-Supported Browsers
-------------------
-
-Any modern browser will work fine. JavaScript must be enabled.
-
-* IE8 or later
-* Firefox 17 or later
-* Google Chrome 27 or later
-* Safari 6 or later
-* Opera  12 or later
-* iOS 6.x (iPhone, iPad)
-* Android 4.x
-
-Not supported: IE6, IE7
-
-
-Configuring a client (short version)
-------------------------------------
-
-The application's HTML must contains the following elements as well as
-whatever makes up the application itself:
-
-Prerequisites:
-
-~~~
-       <link rel="stylesheet" href="http://mkws.indexdata.com/mkws.css" />
-       <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
-~~~
-
-Then the following special `<div>`s can be added (with no content), and
-will be filled in by MKWS:
-
-* `<div id="mkwsSwitch"></div>` -- switch between record and target views
-* `<div id="mkwsLang"></div>  ` -- switch between English, Danish and German
-* `<div id="mkwsSearch"></div>` -- search box and button
-* `<div id="mkwsResults"></div>` -- result list, including pager/sorting
-* `<div id="mkwsTargets"></div>` -- target list, including status
-* `<div id="mkwsStat"></div>` -- summary statistics
-
-You can configure and control the client by creating an `mkws_config`
-object _before_ loading the widget-set.  Here is an example of all
-possible options:
-
-~~~
-    <script type="text/javascript">
-      var mkws_config = {
-        use_service_proxy: true,    /* true, flase: use service proxy instead pazpar2 */
-        show_lang: true,            /* true, false: show/hide language menu */
-        show_sort: true,            /* true, false: show/hide sort menu */
-        show_perpage: true,         /* true, false: show/hide perpage menu */
-        lang_options: ["en", "de", "da"],
-                                    /* display languages links for given languages, [] for all */
-        facets: ["sources", "subjects", "authors"],
-                                    /* display facets, in this order, [] for none */
-        sort_default: "relevance",  /* "relevance", "title:1", "date:0", "date:1" */
-        query_width: 50,            /* 5..50 */
-        perpage_default: 20,        /* 10, 20, 30, 50 */
-        lang: "en",                 /* "en", "de", "da" */
-        debug_level: 0,             /* debug level for development: 0..2 */
-
-        responsive_design_wodth: 600,    /* page reflows for devices < 600 pixels wide */
-        pazpar2_url: "/service-proxy/",            /* URL */
-        service_proxy_auth: "/service-proxy-auth", /* URL */
-        // TODO: language_*, perpage_options, sort_options
-      };
-    </script>
-~~~
-
-For much more detail, see
-[the MKWS whitepaper](whitepaper.html).
-
-
-- - -
-
-Copyright 2013 IndexData ApS. <http://indexdata.com>
diff --git a/tools/htdocs/VERSION b/tools/htdocs/VERSION
deleted file mode 100644 (file)
index f374f66..0000000
+++ /dev/null
@@ -1 +0,0 @@
-0.9.1
diff --git a/tools/htdocs/debugging-notes.txt b/tools/htdocs/debugging-notes.txt
deleted file mode 100644 (file)
index c28d520..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-config_default.debug_level is initialised to 1
-mkws_config.debug_level may be set by the application
-mkws.debug_level is set from one of the above
-mkws.debug_time is self-contained, and used well
-mkws.debug_function is a function
-var debug is a local alias of mkws.debug_function
-debug2 is a function that calls debug after 500 ms -- what?!
diff --git a/tools/htdocs/handlebars-test.html b/tools/htdocs/handlebars-test.html
deleted file mode 100644 (file)
index 7a9d3b7..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<html>
-  <head>
-    <script type="text/javascript" src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
-    <script type="text/javascript" src="handlebars-v1.1.2.js"></script>
-    <script id="entry-template" type="text/x-handlebars-template">
-      <div class="entry">
-       <h1>{{title}}</h1>
-       <div class="body">
-         {{body}}
-       </div>
-      </div>
-    </script>
-  </head>
-  <body>
-    <div id="content"/>
-    <script type="text/javascript">
-      var source = $("#entry-template").html();
-      var template = Handlebars.compile(source);
-      var context = {title: "My New Post", body: "This is my first post!"}
-      var html = template(context);
-      $("#content").html(html);
-    </script>
-  </body>
-</html>
diff --git a/tools/htdocs/html-structure.txt b/tools/htdocs/html-structure.txt
deleted file mode 100644 (file)
index c6d0da8..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-The HTML structure of the MKWS <div>s is as follows. This is useful to
-know when working on stylesheets. As in CSS, #ID indicates a unique
-identifier and .CLASS indicates an instance of a class.
-
-
-#mkwsSwitch
-  a*
-
-#mkwsLang
-  ( a | span )*
-
-#mkwsSearch
-  form
-    input#mkwsQuery type=text
-    input#mkwsButton type=submit
-
-#mkwsBlanket
-  (no contents -- used only for masking)
-
-#mkwsResults
-  table
-    tbody
-      tr
-        td
-          #mkwsTermlists
-            div.title
-            div.facet*
-              div.termtitle
-              ( a span br )*
-        td
-          div#mkwsRanking
-            form#mkwsSelect
-              select#mkwsSort
-              select#mkwsPerpage
-          #mkwsPager
-          #mkwsNavi
-          #mkwsRecords
-            div.record*
-              span (for sequence number)
-              a (for title)
-              span (for other information such as author)
-              div.details (sometimes)
-                table
-                  tbody
-                    tr*
-                      th
-                      td
-#mkwsTargets
-  #mkwsBytarget
-    table
-      thead
-        tr*
-          td*
-      tbody
-        tr*
-          td*
-
-#mkwsStat
-  span.head
-  span.clients
-  span.records
-
index 67ad015..c1341be 100644 (file)
       <ul>
        <li>
           A very simple application at
-          <a href="http://example.indexdata.com/"
-             >http://example.indexdata.com/</a>.
+          <a href="//example.indexdata.com/"
+             >//example.indexdata.com/</a>.
        </li>
        <li>
-          <a href="http://example.indexdata.com/minimal.html"
+          <a href="//example.indexdata.com/minimal.html"
              >The absolutely minimal application</a>
          listed above.
        </li>
        <li>
-          <a href="http://example.indexdata.com/language.html"
+          <a href="//example.indexdata.com/language.html"
              >A more detailed version</a>
          that contains a configuration structure instead of accepting
           the defaults. Includes a custom translation option to present
           the application in Arabic.
        </li>
        <li>
-          <a href="http://example.indexdata.com/mobile.html"
+          <a href="//example.indexdata.com/mobile.html"
              >A version suitable for mobile devices</a>,
          with a responsive design that moves components around
           depending on the screen size.
       <ul>
        <li>
          An application that
-         <a href="http://example.indexdata.com/lowlevel.html"
+         <a href="//example.indexdata.com/lowlevel.html"
             >uses lower-level MKWS components</a>
          rather than the all-in-one <tt>#mkwsResults</tt> division,
          allowing it to use a rather different layout.
        </li>
        <li>
          An application that specifies how to display brief and full records
-         <a href="http://example.indexdata.com/templates.html"
+         <a href="//example.indexdata.com/templates.html"
             >using Handlebar templates</a>.
          (Read about
          <a href="http://handlebarsjs.com/"
             >the templating language</a>.)
        </li>
        <li>
-         <a href="http://example.indexdata.com/localauth.html"
+         An application that
+         <a href="http://example.indexdata.com/images.html?q=portrait"
+            >displays thumbnail images</a>.
+       </li>
+       <li>
+         <a href="//example.indexdata.com/localauth.html"
             >An application that uses a local authentication regime</a>,
          and the corresponding
-         <a href="http://example.indexdata.com/apache-config.txt"
+         <a href="//example.indexdata.com/apache-config.txt"
             >Apache2 configuration stanza</a>.
        </li>
        <li>
           The
-          <a href="http://example.indexdata.com/jquery.html"
+          <a href="//example.indexdata.com/jquery.html"
              >jQuery plugin</a>
           version, consisting of a single JavaScript statement.
        </li>
        <li>
-          <a href="http://example.indexdata.com/popup.html"
+          <a href="//example.indexdata.com/popup.html"
              >A version that uses a jQuery popup</a>.
        </li>
       </ul>
       <h3>Non-standard interfaces</h3>
       <ul>
        <li>
-          <a href="http://example.indexdata.com/dict.html"
+          <a href="//example.indexdata.com/dict.html"
              >An application that uses MKWS to find dictionary
             definitions of words when you highlight them</a>.
        </li>
        <li>
-          <a href="http://example.indexdata.com/auto.html"
+          <a href="//example.indexdata.com/auto.html"
              >An application that runs an automatic search on load</a>.
        </li>
        <li>
diff --git a/tools/htdocs/mkws-doc.css b/tools/htdocs/mkws-doc.css
deleted file mode 100644 (file)
index a58c10f..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-body {
-    font-family: Times, "Times Roman", "Times New Roman";
-}
-
-h1, h2, h3, h4 {
-    color: #68a;
-    font-weight: bold;
-    font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
-}
-
-h2 a, h3 a, h4 a, div#TOC a {
-    color: #68a;
-    text-decoration: none;
-}
-
-h2, h3 {
-    /* Default spacing is way off in both Chrome and Firefox */
-    margin-bottom: -0.5em;
-}
-
-h1 {
-    background: #e0e8f8;
-    padding: 0.2em;
-    font-weight: normal;
-}
-
-h2.author {
-    font-size: 120%;
-    color: black
-}
-
-h3.date {
-    font-size: 100%;
-    color: black
-}
-
-body > p, ul, ol, pre, table, h4 {
-    margin-left: 10%;
-}
-
-p, ul {
-    max-width: 40em;
-}
-
-pre {
-    background: #eee;
-}
-
-table tr th {
-    color: white;
-    background: #68a;
-    font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
-}
-
-th, td {
-    padding: 0.2em 0.5em;
-    vertical-align: top;
-}
-
-table tr:nth-child(odd) {
-    background: #a9c6e3;
-}
-
-table tr:nth-child(even) {
-    background: #bfdcf8;
-}
-
-/*
- * Works with the HTML emitted by pandoc. It would better if pandoc
- * were to emit class names that we can use. But it doesn't.
- */  
-body > p:last-of-type {
-    font-size: small;
-    max-width: none;
-    text-align: right;
-}
index 4cd04c3..f3acc34 100644 (file)
@@ -1,82 +1,86 @@
-#mkwsLang,
-#mkwsSwitch,
-#mkwsSearch,
-#mkwsTermlists,
-#mkwsRanking,
-#mkwsPager,
-#mkwsNavi,
-#mkwsRecords,
-#mkwsTargets,
-#mkwsStat,
-#mkwsMOTD {
+.mkwsLang,
+.mkwsSwitch,
+.mkwsSearch,
+.mkwsTermlists,
+.mkwsFacet,
+.mkwsRanking,
+.mkwsPager,
+.mkwsNavi,
+.mkwsRecords,
+.mkwsRecord,
+.mkwsTargets,
+.mkwsStat,
+.mkwsMOTD {
     font-family: Gill Sans, "Gillius ADF", Gillius, GilliusADF, Verdana, Sans-Serif;
 }
 
-#mkwsLang {
+.mkwsLang {
     float: left;
     padding-left: 1em;
     padding-top: 0.4em;
 }
 
-#mkwsLang a {
+.mkwsLang a {
     background: #d0e0ff;
     padding: 1px 4px;
 }
 
-#mkwsLang span {
+.mkwsLang span {
     border: solid 1px #d0e0ff;
     padding: 0px 3px;
 }
 
-#mkwsSearch {
+.mkwsSearch {
     float: right;
 }
 
-#mkwsSwitch {
+.mkwsSwitch {
     float: right;
     padding-left: 1em;
     padding-top: 0.4em;
 }
 
-#mkwsTargets {
+.mkwsTargets {
     background-color: #fafafa;
 }
 
-#mkwsStat {
+.mkwsStat {
     margin-top: 10px;
     border-top: 1px solid  #156a16;
     padding-top: 5px;
     font-size: small;
 }
 
-#mkwsStat span.head {
+.mkwsStat span.head {
     font-weight: bold;
 }
 
-#mkwsSwitch a,
-#mkwsLang a,
-#mkwsTermlists a,
-#mkwsRanking a,
-#mkwsPager a,
-#mkwsNavi a,
-#mkwsRecords a {
+.mkwsSwitch a,
+.mkwsLang a,
+.mkwsFacet a,
+.mkwsRanking a,
+.mkwsPager a,
+.mkwsNavi a,
+.mkwsRecords a,
+.mkwsRecord a {
     color: #005701;
     text-decoration: none;
 }
 
-#mkwsSwitch a:hover,
-#mkwsLang a:hover,
-#mkwsTermlists a:hover,
-#mkwsPager a:hover,
-#mkwsRecords a:hover {
+.mkwsSwitch a:hover,
+.mkwsLang a:hover,
+.mkwsFacet a:hover,
+.mkwsPager a:hover,
+.mkwsRecords a:hover,
+.mkwsRecord a:hover {
     text-decoration: underline;
 }
 
-#mkwsNavi a.crossout:hover {
+.mkwsNavi a.crossout:hover {
     text-decoration: line-through;
 }
 
-#mkwsSearch input#mkwsButton {
+.mkwsSearch input.mkwsButton {
     border: 3px outset #132194;
     background-color: #132194;
     padding: 2px;
     cursor: pointer;
 }
 
-#mkwsSearch input#mkwsQuery {
+.mkwsSearch input.mkwsQuery {
     border: 2px inset #e0f0ff;
     padding: 3px;
     font-size: 12px;
     background: #f0f8ff;
 }
 
-#mkwsTermlists .title {
+.mkwsTermlists .title {
     font-size: large;
     font-weight: bold;
     text-transform: uppercase;
 }
 
-#mkwsTermlists {
+.mkwsTermlists {
     background: #d0e0ff;
     padding: 0.7em;
     font-size: small;
     -webkit-border-top-right-radius: 10px;
 }
 
-#mkwsTermlists div.facet {
+.mkwsFacet {
     background: #e0f0ff;
     padding: 0.7em;
     margin-top: 0.7em;
     -webkit-border-top-right-radius: 10px;
 }
 
-#mkwsTermlists div.facet div.term {
+.mkwsFacet div.term {
     clear: both;
 }
 
-#mkwsTermlists div.facet div.term span {
+.mkwsFacet div.term span {
     float: right;
 }
 
-#mkwsTermlists div.termtitle {
+.mkwsFacet div.termtitle {
     font-weight: bold;
 }
 
-#mkwsRecords div.record {
+.mkwsRecords div.record,
+.mkwsRecord div.record {
     padding: 5px;
 }
 
-#mkwsRecords div.details {
+.mkwsRecords div.details,
+.mkwsRecord div.details {
     border: 1px solid #404040;
     background: #e8e8e8;
-    color: #A9A9A9;
+    color: black;
     padding: 5px 10px;
     margin: 10px 0px;
     box-shadow: 10px 10px 5px #808080;
     -webkit-border-top-right-radius: 10px;
 }
 
-#mkwsRecords div.details th {
+.mkwsRecords div.details th,
+.mkwsRecord div.details th {
     text-align: right;
     vertical-align: top;
     padding-right: 0.6em;
 }
 
-#mkwsRecords div.details th:after {
+.mkwsRecords div.details th:after,
+.mkwsRecord div.details th:after {
     content: ":";
 }
 
-#mkwsPager {
+.mkwsPager {
     background: #e0e0e0;
     padding: 0.3em;
 }
 
-#mkwsBytarget table thead tr td {
+.mkwsBytarget table thead tr td {
     background-color: #132194;
     color: white;
     font-weight: bold;
     padding: 0.2em 0.5em;
 }
 
-#mkwsBytarget table tbody tr:nth-child(odd) {
+.mkwsBytarget table tbody tr:nth-child(odd) {
     background-color: #e0f0ff;
 }
 
-#mkwsBytarget table tbody tr:nth-child(even) {
+.mkwsBytarget table tbody tr:nth-child(even) {
     background-color: #d0e0ff;
 }
 
-#mkwsBytarget table tbody tr td {
+.mkwsBytarget table tbody tr td {
     padding: 0.2em 0.5em;
 }
+
+.mkwsSelected {
+    padding: 0.1em 0.5em;
+    background: #508751;
+    color: white;
+}
diff --git a/tools/htdocs/mkws.js b/tools/htdocs/mkws.js
deleted file mode 100644 (file)
index 8b40745..0000000
+++ /dev/null
@@ -1,1292 +0,0 @@
-/*! MKWS, the MasterKey Widget Set. Copyright (C) 2013, Index Data */
-
-"use strict"; // HTML5: disable for debug_level >= 2
-
-// Set up namespace and some state.
-var mkws = {
-    sort: 'relevance',
-    authenticated: false,
-    filters: []
-};
-
-/*
- * global config object: mkws_config
- *
- * Needs to be defined in the HTML header before including this JS file.
- * Define empty mkws_config for simple applications that don't define it.
- */
-if (!mkws_config)
-    var mkws_config = {};
-
-// Wrapper for jQuery
-(function ($) {
-
-mkws.locale_lang = {
-    "de": {
-       "Authors": "Autoren",
-       "Subjects": "Schlagw&ouml;rter",
-       "Sources": "Daten und Quellen",
-       "Termlists": "Termlisten",
-       "Next": "Weiter",
-       "Prev": "Zur&uuml;ck",
-       "Search": "Suche",
-       "Sort by": "Sortieren nach",
-       "and show": "und zeige",
-       "per page": "pro Seite",
-       "Displaying": "Zeige",
-       "to": "von",
-       "of": "aus",
-       "found": "gefunden",
-       "Title": "Titel",
-       "Author": "Autor",
-       "Date": "Datum",
-       "Subject": "Schlagwort",
-       "Location": "Ort",
-       // ### to add: Records, Targets
-
-       "dummy": "dummy"
-    },
-
-    "da": {
-       "Authors": "Forfattere",
-       "Subjects": "Emner",
-       "Sources": "Kilder",
-       "Termlists": "Termlists",
-       "Next": "N&aelig;ste",
-       "Prev": "Forrige",
-       "Search": "S&oslash;g",
-       "Sort by": "Sorter efter",
-       "and show": "og vis",
-       "per page": "per side",
-       "Displaying": "Viser",
-       "to": "til",
-       "of": "ud af",
-       "found": "fandt",
-       "Title": "Title",
-       "Author": "Forfatter",
-       "Date": "Dato",
-       "Subject": "Emneord",
-       "Location": "Lokation",
-       // ### to add: Records, Targets
-
-       "dummy": "dummy"
-    }
-};
-
-// keep time state for debugging
-mkws.debug_time = {
-    "start": $.now(),
-    "last": $.now()
-};
-
-mkws.debug_function = function (string) {
-    if (!mkws.debug_level)
-       return;
-
-    if (typeof console === "undefined" || typeof console.log === "undefined") { /* ARGH!!! old IE */
-       return;
-    }
-
-    var now = $.now();
-    var timestamp = ((now - mkws.debug_time.start)/1000).toFixed(3) + " (+" + ((now - mkws.debug_time.last)/1000).toFixed(3) + ") "
-    mkws.debug_time.last = now;
-
-    // you need to disable use strict at the top of the file!!!
-    if (mkws.debug_level >= 3) {
-       console.log(timestamp + arguments.callee.caller);
-    } else if (mkws.debug_level >= 2) {
-       console.log(timestamp + ">>> called from function " + arguments.callee.caller.name + ' <<<');
-    }
-    console.log(timestamp + string);
-}
-var debug = mkws.debug_function; // local alias
-
-
-Handlebars.registerHelper('json', function(obj) {
-    return $.toJSON(obj);
-});
-
-
-// We need {{attr '@name'}} because Handlebars can't parse {{@name}}
-Handlebars.registerHelper('attr', function(attrName) {
-    return this[attrName];
-});
-
-
-/*
- * Use as follows: {{#if-any NAME1 having="NAME2"}}
- * Applicable when NAME1 is the name of an array
- * The guarded code runs only if at least one element of the NAME1
- * array has a subelement called NAME2.
- */
-Handlebars.registerHelper('if-any', function(items, options) {
-    var having = options.hash.having;
-    for (var i in items) {
-       var item = items[i]
-       if (!having || item[having]) {
-           return options.fn(this);
-       }
-    }
-    return "";
-});
-
-
-Handlebars.registerHelper('first', function(items, options) {
-    var having = options.hash.having;
-    for (var i in items) {
-       var item = items[i]
-       if (!having || item[having]) {
-           return options.fn(item);
-       }
-    }
-    return "";
-});
-
-
-Handlebars.registerHelper('commaList', function(items, options) {
-    var out = "";
-
-    for (var i in items) {
-       if (i > 0) out += ", ";
-       out += options.fn(items[i])
-    }
-
-    return out;
-});
-
-
-{
-    /* default mkws config */
-    var config_default = {
-       use_service_proxy: true,
-       pazpar2_url: "http://mkws.indexdata.com/service-proxy/",
-       service_proxy_auth: "http://mkws.indexdata.com/service-proxy-auth",
-       lang: "",
-       sort_options: [["relevance"], ["title:1", "title"], ["date:0", "newest"], ["date:1", "oldest"]],
-       perpage_options: [10, 20, 30, 50],
-       sort_default: "relevance",
-       perpage_default: 20,
-       query_width: 50,
-       show_lang: true,        /* show/hide language menu */
-       show_sort: true,        /* show/hide sort menu */
-       show_perpage: true,     /* show/hide perpage menu */
-       lang_options: [],       /* display languages links for given languages, [] for all */
-       facets: ["sources", "subjects", "authors"], /* display facets, in this order, [] for none */
-       responsive_design_width: undefined, /* a page with less pixel width considered as narrow */
-       debug_level: 1,     /* debug level for development: 0..2 */
-
-       dummy: "dummy"
-    };
-
-    /* set global debug_level flag early */
-    if (typeof mkws_config.debug_level !== 'undefined') {
-       mkws.debug_level = mkws_config.debug_level;
-    } else if (typeof config_default.debug_level !== 'undefined') {
-       mkws.debug_level = config_default.debug_level;
-    }
-
-    /* override standard config values by function parameters */
-    for (var k in config_default) {
-       if (typeof mkws_config[k] === 'undefined')
-          mkws_config[k] = config_default[k];
-       debug("Set config: " + k + ' => ' + mkws_config[k]);
-    }
-}
-
-mkws.sort = mkws_config.sort_default;
-debug("copied mkws_config.sort_default '" + mkws_config.sort_default + "' to mkws.sort");
-
-mkws.usesessions = mkws_config.use_service_proxy ? false : true;
-
-if (mkws_config.query_width < 5 || mkws_config.query_width > 150) {
-    debug("Reset query width: " + mkws_config.query_width);
-    mkws_config.query_width = 50;
-}
-
-for (var key in mkws_config) {
-    if (mkws_config.hasOwnProperty(key)) {
-       if (key.match(/^language_/)) {
-           var lang = key.replace(/^language_/, "");
-           // Copy custom languages into list
-           mkws.locale_lang[lang] = mkws_config[key];
-           debug("Added locally configured language '" + lang + "'");
-       }
-    }
-}
-
-
-// create a parameters array and pass it to the pz2's constructor
-// then register the form submit event with the pz2.search function
-// autoInit is set to true on default
-var my_paz = new pz2( { "onshow": my_onshow,
-                    "showtime": 500,            //each timer (show, stat, term, bytarget) can be specified this way
-                    "pazpar2path": mkws_config.pazpar2_url,
-                    "oninit": my_oninit,
-                    "onstat": my_onstat,
-                    "onterm": my_onterm,
-                    "termlist": "xtargets,subject,author",
-                    "onbytarget": my_onbytarget,
-                   "usesessions" : mkws.usesessions,
-                    "showResponseType": '', // or "json" (for debugging?)
-                    "onrecord": my_onrecord } );
-
-mkws.my_paz = my_paz; // export
-
-// some state vars
-var curPage = 1;
-var recPerPage = 20;
-var totalRec = 0;
-var curDetRecId = '';
-var curDetRecData = null;
-var submitted = false;
-var SourceMax = 16;
-var SubjectMax = 10;
-var AuthorMax = 10;
-
-//
-// pz2.js event handlers:
-//
-function my_oninit() {
-    my_paz.stat();
-    my_paz.bytarget();
-}
-
-function my_onshow(data) {
-    totalRec = data.merged;
-    // move it out
-    var pager = document.getElementById("mkwsPager");
-    if (pager) {
-       pager.innerHTML = "";
-       pager.innerHTML +='<div style="float: right">' + M('Displaying') + ': '
-            + (data.start + 1) + ' ' + M('to') + ' ' + (data.start + data.num) +
-            ' ' + M('of') + ' ' + data.merged + ' (' + M('found') + ': '
-            + data.total + ')</div>';
-       drawPager(pager);
-    }
-
-    // navi
-    var results = document.getElementById("mkwsRecords");
-
-    var html = [];
-    for (var i = 0; i < data.hits.length; i++) {
-        var hit = data.hits[i];
-       html.push('<div class="record" id="mkwsRecdiv_' + hit.recid + '" >',
-                 renderSummary(hit),
-                 '</div>');
-       if (hit.recid == curDetRecId) {
-            html.push(renderDetails(curDetRecData));
-       }
-    }
-    replaceHtml(results, html.join(''));
-}
-
-
-function renderSummary(hit)
-{
-    if (mkws.templateSummary === undefined) {
-       loadTemplate("Summary");
-    }
-
-    hit._id = "mkwsRec_" + hit.recid;
-    hit._onclick = "mkws.showDetails(this.id);return false;"
-    return mkws.templateSummary(hit);
-}
-
-
-function my_onstat(data) {
-    var stat = document.getElementById("mkwsStat");
-    if (stat == null)
-       return;
-
-    stat.innerHTML = '<span class="head">' + M('Status info') + '</span>' +
-       ' -- ' +
-       '<span class="clients">' + M('Active clients') + ': ' + data.activeclients + '/' + data.clients + '</span>' +
-       ' -- ' +
-        '<span class="records">' + M('Retrieved records') + ': ' + data.records + '/' + data.hits + '</span>';
-}
-
-function my_onterm(data) {
-    // no facets
-    if (!mkws_config.facets || mkws_config.facets.length == 0) {
-       $("#mkwsTermlists").hide();
-       return;
-    }
-
-    // display if we first got results
-    $("#mkwsTermlists").show();
-
-    var acc = [];
-    acc.push('<div class="title">' + M('Termlists') + '</div>');
-    var facets = mkws_config.facets;
-
-    for(var i = 0; i < facets.length; i++) {
-       if (facets[i] == "sources") {
-           add_single_facet(acc, "Sources",  data.xtargets, SourceMax, null);
-       } else if (facets[i] == "subjects") {
-           add_single_facet(acc, "Subjects", data.subject,  SubjectMax, "subject");
-       } else if (facets[i] == "authors") {
-           add_single_facet(acc, "Authors",  data.author,   AuthorMax, "author");
-       } else {
-           alert("bad facet configuration: '" + facets[i] + "'");
-       }
-    }
-
-    var termlist = document.getElementById("mkwsTermlists");
-    if (termlist)
-       replaceHtml(termlist, acc.join(''));
-}
-
-function add_single_facet(acc, caption, data, max, pzIndex) {
-    acc.push('<div class="facet" id="mkwsFacet' + caption + '">');
-    acc.push('<div class="termtitle">' + M(caption) + '</div>');
-    for (var i = 0; i < data.length && i < max; i++ ) {
-       acc.push('<div class="term">');
-        acc.push('<a href="#" ');
-       var action;
-       if (!pzIndex) {
-           // Special case: target selection
-           acc.push('target_id='+data[i].id+' ');
-           action = 'mkws.limitTarget(this.getAttribute(\'target_id\'),this.firstChild.nodeValue)';
-       } else {
-           action = 'mkws.limitQuery(\'' + pzIndex + '\', this.firstChild.nodeValue)';
-       }
-       acc.push('onclick="' + action + ';return false;">' + data[i].name + '</a>'
-                + ' <span>' + data[i].freq + '</span>');
-       acc.push('</div>');
-    }
-    acc.push('</div>');
-}
-
-function my_onrecord(data) {
-    // FIXME: record is async!!
-    clearTimeout(my_paz.recordTimer);
-    // in case on_show was faster to redraw element
-    var detRecordDiv = document.getElementById('mkwsDet_'+data.recid);
-    if (detRecordDiv) return;
-    curDetRecData = data;
-    var recordDiv = document.getElementById('mkwsRecdiv_'+curDetRecData.recid);
-    var html = renderDetails(curDetRecData);
-    recordDiv.innerHTML += html;
-}
-
-function my_onbytarget(data) {
-    var targetDiv = document.getElementById("mkwsBytarget");
-    if (!targetDiv) {
-       // No mkwsTargets div.
-       return;
-    }
-
-    var table ='<table><thead><tr>' +
-       '<td>' + M('Target ID') + '</td>' +
-       '<td>' + M('Hits') + '</td>' +
-       '<td>' + M('Diags') + '</td>' +
-       '<td>' + M('Records') + '</td>' +
-       '<td>' + M('State') + '</td>' +
-       '</tr></thead><tbody>';
-
-    for (var i = 0; i < data.length; i++ ) {
-        table += "<tr><td>" + data[i].id +
-            "</td><td>" + data[i].hits +
-            "</td><td>" + data[i].diagnostic +
-            "</td><td>" + data[i].records +
-            "</td><td>" + data[i].state + "</td></tr>";
-    }
-
-    table += '</tbody></table>';
-    targetDiv.innerHTML = table;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-////////////////////////////////////////////////////////////////////////////////
-
-// wait until the DOM is ready
-function domReady ()
-{
-    document.mkwsSearchForm.onsubmit = onFormSubmitEventHandler;
-    document.mkwsSearchForm.mkwsQuery.value = '';
-    if (document.mkwsSelect) {
-       if (document.mkwsSelect.mkwsSort)
-           document.mkwsSelect.mkwsSort.onchange = onSelectDdChange;
-       if (document.mkwsSelect.mkwsPerpage)
-           document.mkwsSelect.mkwsPerpage.onchange = onSelectDdChange;
-    }
-}
-
-// when search button pressed
-function onFormSubmitEventHandler()
-{
-    newSearch(document.mkwsSearchForm.mkwsQuery.value);
-    return false;
-}
-
-function newSearch(query, sort, targets)
-{
-    debug("newSearch: " + query);
-   
-    if (mkws_config.use_service_proxy && !mkws.authenticated) {
-       alert("searching before authentication");
-       return;
-    }
-
-    mkws.filters = []
-    redraw_navi();
-    resetPage();
-    loadSelect();
-    triggerSearch(query, sort, targets);
-    mkws.switchView('records'); // In case it's configured to start off as hidden
-    submitted = true;
-}
-
-function onSelectDdChange()
-{
-    if (!submitted) return false;
-    resetPage();
-    loadSelect();
-    my_paz.show(0, recPerPage, mkws.sort);
-    return false;
-}
-
-function resetPage()
-{
-    curPage = 1;
-    totalRec = 0;
-}
-
-function triggerSearch (query, sort, targets)
-{
-    var pp2filter = "";
-    var pp2limit = "";
-
-    // Re-use previous query/sort if new ones are not specified
-    if (query) {
-       mkws.query = query;
-    }
-    if (sort) {
-       mkws.sort = sort;
-    }
-    if (targets) {
-       // ### should support multiple |-separated targets
-       mkws.filters.push({ id: targets, name: targets });
-    }
-
-    for (var i in mkws.filters) {
-       var filter = mkws.filters[i];
-       if (filter.id) {
-           if (pp2filter)
-               pp2filter += ",";
-           if (filter.id.match(/^[a-z:]+[=~]/)) {
-               debug("filter '" + filter.id + "' already begins with SETTING OP");
-           } else {
-               filter.id = 'pz:id=' + filter.id;
-           }
-           pp2filter += filter.id;
-       } else {
-           if (pp2limit)
-               pp2limit += ",";
-           pp2limit += filter.field + "=" + filter.value.replace(/[\\|,]/g, '\\$&');
-       }
-    }
-
-    debug("triggerSearch(" + mkws.query + "): filters = " + $.toJSON(mkws.filters) + ", pp2filter = " + pp2filter + ", pp2limit = " + pp2limit);
-    my_paz.search(mkws.query, recPerPage, mkws.sort, pp2filter, undefined, { limit: pp2limit });
-}
-
-function loadSelect ()
-{
-    if (document.mkwsSelect) {
-       if (document.mkwsSelect.mkwsSort)
-           mkws.sort = document.mkwsSelect.mkwsSort.value;
-       if (document.mkwsSelect.mkwsPerpage)
-           recPerPage = document.mkwsSelect.mkwsPerpage.value;
-    }
-}
-
-// limit the query after clicking the facet
-mkws.limitQuery = function (field, value)
-{
-    debug("limitQuery(field=" + field + ", value=" + value + ")");
-    mkws.filters.push({ field: field, value: value });
-    redraw_navi();
-    resetPage();
-    loadSelect();
-    triggerSearch();
-    return false;
-}
-
-// limit by target functions
-mkws.limitTarget  = function (id, name)
-{
-    debug("limitTarget(id=" + id + ", name=" + name + ")");
-    mkws.filters.push({ id: id, name: name });
-    redraw_navi();
-    resetPage();
-    loadSelect();
-    triggerSearch();
-    return false;
-}
-
-mkws.delimitQuery = function (field, value)
-{
-    debug("delimitQuery(field=" + field + ", value=" + value + ")");
-    var newFilters = [];
-    for (var i in mkws.filters) {
-       var filter = mkws.filters[i];
-       if (filter.field &&
-           field == filter.field &&
-           value == filter.value) {
-           debug("delimitTarget() removing filter " + $.toJSON(filter));
-       } else {
-           debug("delimitTarget() keeping filter " + $.toJSON(filter));
-           newFilters.push(filter);
-       }
-    }
-    mkws.filters = newFilters;
-
-    redraw_navi();
-    resetPage();
-    loadSelect();
-    triggerSearch();
-    return false;
-}
-
-
-mkws.delimitTarget = function (id)
-{
-    debug("delimitTarget(id=" + id + ")");
-    var newFilters = [];
-    for (var i in mkws.filters) {
-       var filter = mkws.filters[i];
-       if (filter.id) {
-           debug("delimitTarget() removing filter " + $.toJSON(filter));
-       } else {
-           debug("delimitTarget() keeping filter " + $.toJSON(filter));
-           newFilters.push(filter);
-       }
-    }
-    mkws.filters = newFilters;
-
-    redraw_navi();
-    resetPage();
-    loadSelect();
-    triggerSearch();
-    return false;
-}
-
-
-function redraw_navi ()
-{
-    var navi = document.getElementById('mkwsNavi');
-    if (!navi) return;
-
-    var text = "";
-    for (var i in mkws.filters) {
-       if (text) {
-           text += " | ";
-       }
-       var filter = mkws.filters[i];
-       if (filter.id) {
-           text += 'Source: <a class="crossout" href="#" onclick="mkws.delimitTarget(' +
-               "'" + filter.id + "'" + ');return false;">' + filter.name + '</a>';
-       } else {
-           text += filter.field + ': <a class="crossout" href="#" onclick="mkws.delimitQuery(' +
-               "'" + filter.field + "', '" + filter.value + "'" +
-               ');return false;">' + filter.value + '</a>';
-       }
-    }
-
-    navi.innerHTML = text;
-}
-
-
-function drawPager (pagerDiv)
-{
-    //client indexes pages from 1 but pz2 from 0
-    var onsides = 6;
-    var pages = Math.ceil(totalRec / recPerPage);
-
-    var firstClkbl = ( curPage - onsides > 0 )
-        ? curPage - onsides
-        : 1;
-
-    var lastClkbl = firstClkbl + 2*onsides < pages
-        ? firstClkbl + 2*onsides
-        : pages;
-
-    var prev = '<span id="mkwsPrev">&#60;&#60; ' + M('Prev') + '</span><b> | </b>';
-    if (curPage > 1)
-        prev = '<a href="#" id="mkwsPrev" onclick="mkws.pagerPrev();">'
-        +'&#60;&#60; ' + M('Prev') + '</a><b> | </b>';
-
-    var middle = '';
-    for(var i = firstClkbl; i <= lastClkbl; i++) {
-        var numLabel = i;
-        if(i == curPage)
-            numLabel = '<b>' + i + '</b>';
-
-        middle += '<a href="#" onclick="mkws.showPage(' + i + ')"> '
-            + numLabel + ' </a>';
-    }
-
-    var next = '<b> | </b><span id="mkwsNext">' + M('Next') + ' &#62;&#62;</span>';
-    if (pages - curPage > 0)
-        next = '<b> | </b><a href="#" id="mkwsNext" onclick="mkws.pagerNext()">'
-        + M('Next') + ' &#62;&#62;</a>';
-
-    var predots = '';
-    if (firstClkbl > 1)
-        predots = '...';
-
-    var postdots = '';
-    if (lastClkbl < pages)
-        postdots = '...';
-
-    pagerDiv.innerHTML += '<div style="float: clear">'
-        + prev + predots + middle + postdots + next + '</div>';
-}
-
-mkws.showPage = function (pageNum)
-{
-    curPage = pageNum;
-    my_paz.showPage( curPage - 1 );
-}
-
-// simple paging functions
-
-mkws.pagerNext = function () {
-    if ( totalRec - recPerPage*curPage > 0) {
-        my_paz.showNext();
-        curPage++;
-    }
-}
-
-mkws.pagerPrev = function () {
-    if ( my_paz.showPrev() != false )
-        curPage--;
-}
-
-// switching view between targets and records
-
-mkws.switchView = function(view) {
-    debug("switchView: " + view);
-
-    var targets = document.getElementById('mkwsTargets');
-    var results = document.getElementById('mkwsResults') ||
-                 document.getElementById('mkwsRecords');
-    var blanket = document.getElementById('mkwsBlanket');
-    var motd    = document.getElementById('mkwsMOTD');
-
-    switch(view) {
-        case 'targets':
-            if (targets) targets.style.display = "block";
-            if (results) results.style.display = "none";
-            if (blanket) blanket.style.display = "none";
-            if (motd) motd.style.display = "none";
-            break;
-        case 'records':
-            if (targets) targets.style.display = "none";
-            if (results) results.style.display = "block";
-            if (blanket) blanket.style.display = "block";
-            if (motd) motd.style.display = "none";
-            break;
-       case 'none':
-            if (targets) targets.style.display = "none";
-            if (results) results.style.display = "none";
-            if (blanket) blanket.style.display = "none";
-            if (motd) motd.style.display = "none";
-            break;
-        default:
-            alert("Unknown view '" + view + "'");
-    }
-}
-
-// detailed record drawing
-mkws.showDetails = function (prefixRecId) {
-    var recId = prefixRecId.replace('mkwsRec_', '');
-    var oldRecId = curDetRecId;
-    curDetRecId = recId;
-
-    // remove current detailed view if any
-    var detRecordDiv = document.getElementById('mkwsDet_'+oldRecId);
-    // lovin DOM!
-    if (detRecordDiv)
-      detRecordDiv.parentNode.removeChild(detRecordDiv);
-
-    // if the same clicked, just hide
-    if (recId == oldRecId) {
-        curDetRecId = '';
-        curDetRecData = null;
-        return;
-    }
-    // request the record
-    my_paz.record(recId);
-}
-
-function replaceHtml(el, html) {
-  var oldEl = typeof el === "string" ? document.getElementById(el) : el;
-  /*@cc_on // Pure innerHTML is slightly faster in IE
-    oldEl.innerHTML = html;
-    return oldEl;
-    @*/
-  var newEl = oldEl.cloneNode(false);
-  newEl.innerHTML = html;
-  oldEl.parentNode.replaceChild(newEl, oldEl);
-  /* Since we just removed the old element from the DOM, return a reference
-     to the new element, which can be used to restore variable references. */
-  return newEl;
-};
-
-function renderDetails(data, marker)
-{
-    if (mkws.templateRecord === undefined) {
-       loadTemplate("Record");
-    }
-
-    var template = mkws.templateRecord;
-    var details = template(data);
-    return '<div class="details" id="mkwsDet_' + data.recid + '">' + details + '</div>';
-}
-
-
-function loadTemplate(name)
-{
-    var source = $("#mkwsTemplate" + name).html();
-    if (!source) {
-       source = defaultTemplate(name);
-    }
-
-    var template = Handlebars.compile(source);
-    debug("compiled template '" + name + "'");
-    mkws['template' + name] = template;
-}
-
-
-function defaultTemplate(name)
-{
-    if (name === 'Record') {
-       return '\
-      <table>\
-       <tr>\
-         <th>Title</th>\
-         <td>\
-           {{md-title}}\
-           {{#if md-title-remainder}}\
-             ({{md-title-remainder}})\
-           {{/if}}\
-           {{#if md-title-responsibility}}\
-             <i>{{md-title-responsibility}}</i>\
-           {{/if}}\
-         </td>\
-       </tr>\
-       {{#if md-date}}\
-       <tr>\
-         <th>Date</th>\
-         <td>{{md-date}}</td>\
-       </tr>\
-       {{/if}}\
-       {{#if md-author}}\
-       <tr>\
-         <th>Author</th>\
-         <td>{{md-author}}</td>\
-       </tr>\
-       {{/if}}\
-       {{#if md-electronic-url}}\
-       <tr>\
-         <th>URL</th>\
-         <td>\
-           {{#each md-electronic-url}}\
-             <a href="{{this}}">{{this}}</a><br/>\
-           {{/each}}\
-         </td>\
-       </tr>\
-       {{/if}}\
-       {{#if-any location having="md-subject"}}\
-       <tr>\
-         <th>Subject</th>\
-         <td>\
-           {{#first location having="md-subject"}}\
-             {{#if md-subject}}\
-               {{md-subject}}\
-             {{/if}}\
-           {{/first}}\
-         </td>\
-       </tr>\
-       {{/if-any}}\
-       <tr>\
-         <th>Locations</th>\
-         <td>\
-           {{#commaList location}}\
-             {{attr "@name"}}{{/commaList}}\
-         </td>\
-       </tr>\
-      </table>\
-';
-    } else if (name === "Summary") {
-       return '\
-      <a href="#" id="{{_id}}" onclick="{{_onclick}}">\
-       <b>{{md-title}}</b>\
-      </a>\
-      {{#if md-title-remainder}}\
-        <span>{{md-title-remainder}}</span>\
-      {{/if}}\
-      {{#if md-title-responsibility}}\
-       <span><i>{{md-title-responsibility}}</i></span>\
-      {{/if}}\
-';
-    }
-
-    var s = "There is no default '" + name +"' template!";
-    alert(s);
-    return s;
-}
-
-
-/*
- * All the HTML stuff to render the search forms and
- * result pages.
- */
-function mkws_html_all() {
-    mkws_set_lang();
-    if (mkws_config.show_lang)
-       mkws_html_lang();
-
-    // For some reason, doing this programmatically results in
-    // document.mkwsSearchForm.mkwsQuery being undefined, hence the raw HTML.
-    debug("HTML search form");
-    $("#mkwsSearch").html('\
-    <form name="mkwsSearchForm" action="" >\
-      <input id="mkwsQuery" type="text" size="' + mkws_config.query_width + '" />\
-      <input id="mkwsButton" type="submit" value="' + M('Search') + '" />\
-    </form>');
-
-    debug("HTML records");
-    // If the application has an #mkwsResults, populate it in the
-    // usual way. If not, assume that it's a smarter application that
-    // defines its own subcomponents:
-    // #mkwsTermlists
-    // #mkwsRanking
-    // #mkwsPager
-    // #mkwsNavi
-    // #mkwsRecords
-    if ($("#mkwsResults").length) {
-       $("#mkwsResults").html('\
-      <table width="100%" border="0" cellpadding="6" cellspacing="0">\
-        <tr>\
-          <td id="mkwsTermlistContainer1" width="250" valign="top">\
-            <div id="mkwsTermlists"></div>\
-          </td>\
-          <td id="mkwsMOTDContainer" valign="top">\
-            <div id="mkwsRanking"></div>\
-            <div id="mkwsPager"></div>\
-            <div id="mkwsNavi"></div>\
-            <div id="mkwsRecords"></div>\
-          </td>\
-        </tr>\
-        <tr>\
-          <td colspan="2">\
-            <div id="mkwsTermlistContainer2"></div>\
-          </td>\
-        </tr>\
-      </table>');
-    }
-
-    if ($("#mkwsRanking").length) {
-       var ranking_data = '';
-       ranking_data += '<form name="mkwsSelect" id="mkwsSelect" action="" >';
-       if (mkws_config.show_sort) {
-           ranking_data +=  M('Sort by') + ' ' + mkws_html_sort() + ' ';
-       }
-       if (mkws_config.show_perpage) {
-           ranking_data += M('and show') + ' ' + mkws_html_perpage() + ' ' + M('per page') + '.';
-       }
-        ranking_data += '</form>';
-
-       $("#mkwsRanking").html(ranking_data);
-    }
-
-    mkws_html_switch();
-
-    if (mkws_config.use_service_proxy) {
-         mkws_service_proxy_auth(mkws_config.service_proxy_auth,
-                                 mkws_config.service_proxy_auth_domain,
-                                 mkws_config.pazpar2_url);
-    } else {
-       // raw pp2
-       run_auto_searches();
-    }
-
-    if (mkws_config.responsive_design_width) {
-       // Responsive web design - change layout on the fly based on
-       // current screen width. Required for mobile devices.
-       $(window).resize( function(e) { mkws_resize_page() });
-       // initial check after page load
-       $(document).ready(function() { mkws_resize_page() });
-    }
-
-    domReady();
-
-    // on first page, hide the termlist
-    $(document).ready(function() { $("#mkwsTermlists").hide(); } );
-    var motd = document.getElementById("mkwsMOTD");
-    var container = document.getElementById("mkwsMOTDContainer");
-    if (motd && container) {
-       // Move the MOTD from the provided element down into the container
-        motd.parentNode.removeChild(motd);
-       container.appendChild(motd);
-    }
-}
-
-
-function run_auto_searches() {
-    debug("run auto searches");
-
-    var node = $('#mkwsRecords');
-    if (node.attr('autosearch')) {
-       var query = node.attr('autosearch');
-       var sort = node.attr('sort');
-       var targets = node.attr('targets');
-       var s = "running auto search: '" + query + "'";
-       if (sort) s += " sorted by '" + sort + "'";
-       if (targets) s += " in targets '" + targets + "'";
-       debug(s);
-       newSearch(query, sort, targets);
-    }
-}
-
-
-function mkws_set_lang()  {
-    var lang = $.parseQuerystring().lang || mkws_config.lang;
-    if (!lang || !mkws.locale_lang[lang]) {
-       mkws_config.lang = ""
-    } else {
-       mkws_config.lang = lang;
-    }
-
-    debug("Locale language: " + (mkws_config.lang ? mkws_config.lang : "none"));
-    return mkws_config.lang;
-}
-
-function mkws_html_switch() {
-    debug("HTML switch");
-
-    $("#mkwsSwitch").append($('<a href="#" id="mkwsSwitch_records" onclick="mkws.switchView(\'records\')">' + M('Records') + '</a>'));
-    $("#mkwsSwitch").append($("<span/>", { text: " | " }));
-    $("#mkwsSwitch").append($('<a href="#" id="mkwsSwitch_targets" onclick="mkws.switchView(\'targets\')">' + M('Targets') + '</a>'));
-
-    debug("HTML targets");
-    $("#mkwsTargets").html('\
-      <div id="mkwsBytarget">\
-       No information available yet.\
-      </div>');
-    $("#mkwsTargets").css("display", "none");
-}
-
-function mkws_html_sort() {
-    debug("HTML sort, mkws.sort = '" + mkws.sort + "'");
-    var sort_html = '<select name="mkwsSort" id="mkwsSort">';
-
-    for(var i = 0; i < mkws_config.sort_options.length; i++) {
-       var opt = mkws_config.sort_options[i];
-       var key = opt[0];
-       var val = opt.length == 1 ? opt[0] : opt[1];
-
-       sort_html += '<option value="' + key + '"';
-       if (mkws.sort == key || mkws.sort == val) {
-           sort_html += ' selected="selected"';
-       }
-       sort_html += '>' + M(val) + '</option>';
-    }
-    sort_html += '</select>';
-
-    return sort_html;
-}
-
-function mkws_html_perpage() {
-    debug("HTML perpage");
-    var perpage_html = '<select name="mkwsPerpage" id="mkwsPerpage">';
-
-    for(var i = 0; i < mkws_config.perpage_options.length; i++) {
-       var key = mkws_config.perpage_options[i];
-
-       perpage_html += '<option value="' + key + '"';
-       if (key == mkws_config.perpage_default) {
-           perpage_html += ' selected="selected"';
-       }
-       perpage_html += '>' + key + '</option>';
-    }
-    perpage_html += '</select>';
-
-    return perpage_html;
-}
-
-/*
- * Run service-proxy authentication in background (after page load).
- * The username/password is configured in the apache config file
- * for the site.
- */
-function mkws_service_proxy_auth(auth_url, auth_domain, pp2_url) {
-    debug("Run service proxy auth URL: " + auth_url);
-
-    if (!auth_domain) {
-       auth_domain = pp2_url.replace(/^http:\/\/(.*?)\/.*/, '$1');
-       debug("guessed auth_domain '" + auth_domain + "' from pp2_url '" + pp2_url + "'");
-    }
-
-    var request = new pzHttpRequest(auth_url, function(err) {
-         alert("HTTP call for authentication failed: " + err)
-         return;
-    }, auth_domain);
-
-    request.get(null, function(data) {
-       if (!$.isXMLDoc(data)) {
-           alert("service proxy auth response document is not valid XML document, give up!");
-           return;
-       }
-       var status = $(data).find("status");
-       if (status.text() != "OK") {
-           alert("service proxy auth repsonse status: " + status.text() + ", give up!");
-           return;
-       }
-
-       debug("Service proxy auth successfully done");
-       mkws.authenticated = true;
-       run_auto_searches();
-    });
-}
-
-/* create locale language menu */
-function mkws_html_lang() {
-    var lang_default = "en";
-    var lang = mkws_config.lang || lang_default;
-    var list = [];
-
-    /* display a list of configured languages, or all */
-    var lang_options = mkws_config.lang_options || [];
-    var hash = {};
-    for (var i = 0; i < lang_options.length; i++) {
-       hash[lang_options[i]] = 1;
-    }
-
-    for (var k in mkws.locale_lang) {
-       if (hash[k] == 1 || lang_options.length == 0)
-           list.push(k);
-    }
-
-    // add english link
-    if (lang_options.length == 0 || hash[lang_default] == 1)
-        list.push(lang_default);
-
-    debug("Language menu for: " + list.join(", "));
-
-    /* the HTML part */
-    var data = "";
-    for(var i = 0; i < list.length; i++) {
-       var l = list[i];
-
-       if (data)
-           data += ' | ';
-
-       if (lang == l) {
-           data += ' <span>' + l + '</span> ';
-       } else {
-           data += ' <a href="?lang=' + l + '">' + l + '</a> '
-       }
-    }
-
-    $("#mkwsLang").html(data);
-}
-
-function mkws_resize_page () {
-    var list = ["mkwsSwitch"];
-
-    var width = mkws_config.responsive_design_width;
-    var parentId = $("#mkwsTermlists").parent().attr('id');
-
-    if ($(window).width() <= width &&
-       parentId === "mkwsTermlistContainer1") {
-       debug("changing from wide to narrow: " + $(window).width());
-       $("#mkwsTermlists").appendTo($("#mkwsTermlistContainer2"));
-       $("#mkwsTermlistContainer1").hide();
-       $("#mkwsTermlistContainer2").show();
-       for(var i = 0; i < list.length; i++) {
-           $("#" + list[i]).hide();
-       }
-    } else if ($(window).width() > width &&
-       parentId === "mkwsTermlistContainer2") {
-       debug("changing from narrow to wide: " + $(window).width());
-       $("#mkwsTermlists").appendTo($("#mkwsTermlistContainer1"));
-       $("#mkwsTermlistContainer1").show();
-       $("#mkwsTermlistContainer2").hide();
-       for(var i = 0; i < list.length; i++) {
-           $("#" + list[i]).show();
-       }
-    }
-};
-
-/* locale */
-function M(word) {
-    var lang = mkws_config.lang;
-
-    if (!lang || !mkws.locale_lang[lang])
-       return word;
-
-    return mkws.locale_lang[lang][word] || word;
-}
-
-/*
- * implement jQuery plugins
- */
-$.extend({
-    // implement $.parseQuerystring() for parsing URL parameters
-    parseQuerystring: function() {
-       var nvpair = {};
-       var qs = window.location.search.replace('?', '');
-       var pairs = qs.split('&');
-       $.each(pairs, function(i, v){
-           var pair = v.split('=');
-           nvpair[pair[0]] = pair[1];
-       });
-       return nvpair;
-    },
-
-    debug2: function(string) { // delayed debug, internal variables are set after dom ready
-       setTimeout(function() { debug(string); }, 500);
-    },
-
-    // service-proxy or pazpar2
-    pazpar2: function(config) {
-       var id_popup = config.id_popup || "#mkwsPopup";
-       id_popup = id_popup.replace(/^#/, "");
-
-       // simple layout
-       var div = '<div id="mkwsSwitch"></div>\
-       <div id="mkwsLang"></div>\
-       <div id="mkwsSearch"></div>\
-       <div id="mkwsResults"></div>\
-       <div id="mkwsTargets"></div>\
-        <div id="mkwsStat"></div>';
-
-       // new table layout
-       var table = '\
-       <style type="text/css">\
-         #mkwsTermlists div.facet {\
-         float:left;\
-         width: 30%;\
-         margin: 0.3em;\
-         }\
-         #mkwsStat {\
-         text-align: right;\
-         }\
-       </style>\
-           \
-       <table width="100%" border="0">\
-         <tr>\
-           <td>\
-             <div id="mkwsSwitch"></div>\
-             <div id="mkwsLang"></div>\
-             <div id="mkwsSearch"></div>\
-           </td>\
-         </tr>\
-         <tr>\
-           <td>\
-             <div style="height:500px; overflow: auto">\
-               <div id="mkwsPager"></div>\
-               <div id="mkwsNavi"></div>\
-               <div id="mkwsRecords"></div>\
-               <div id="mkwsTargets"></div>\
-               <div id="mkwsRanking"></div>\
-             </div>\
-           </td>\
-         </tr>\
-         <tr>\
-           <td>\
-             <div style="height:300px; overflow: hidden">\
-               <div id="mkwsTermlists"></div>\
-             </div>\
-           </td>\
-         </tr>\
-         <tr>\
-           <td>\
-             <div id="mkwsStat"></div>\
-           </td>\
-         </tr>\
-       </table>';
-
-       var popup = '\
-         <div id="mkwsSearch"></div>\
-         <div id="' + id_popup + '">\
-           <div id="mkwsSwitch"></div>\
-           <div id="mkwsLang"></div>\
-           <div id="mkwsResults"></div>\
-           <div id="mkwsTargets"></div>\
-           <div id="mkwsStat"></div>\
-         </div>'
-
-       if (config && config.layout == 'div') {
-           this.debug2("jquery plugin layout: div");
-           document.write(div);
-       } else if (config && config.layout == 'popup') {
-           this.debug2("jquery plugin layout: popup with id: " + id_popup);
-           document.write(popup);
-           $(document).ready( function() { init_popup(config); } );
-       } else {
-           this.debug2("jquery plugin layout: table");
-           document.write(table);
-       }
-    }
-});
-
-function init_popup(obj) {
-    var config = obj ? obj : {};
-
-    var height = config.height || 760;
-    var width = config.width || 880;
-    var id_button = config.id_button || "input#mkwsButton";
-    var id_popup = config.id_popup || "#mkwsPopup";
-
-    debug("popup height: " + height + ", width: " + width);
-
-    // make sure that jquery-ui was loaded afte jQuery core lib, e.g.:
-    // <script src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
-    if (!$.ui) {
-       debug("Error: jquery-ui.js is missing, did you included it after jquery core in the HTML file?");
-       return;
-    }
-
-    $(id_popup).dialog({
-      closeOnEscape: true,
-      autoOpen: false,
-      height: height,
-      width: width,
-      modal: true,
-      resizable: true,
-      buttons: {
-             Cancel: function() {
-                     $(this).dialog("close");
-             }
-      },
-      close: function() { }
-    });
-
-    $(id_button)
-      .button()
-      .click(function() {
-             $(id_popup).dialog("open");
-      });
-};
-
-
-
-
-/* magic */
-$(document).ready(function() {
-    try {
-       mkws_html_all()
-    }
-
-    catch (e) {
-       mkws_config.error = e.message;
-       // alert(e.message);
-    }
-});
-
-})(jQuery);
diff --git a/tools/htdocs/whitepaper.markdown b/tools/htdocs/whitepaper.markdown
deleted file mode 100644 (file)
index 7ae655d..0000000
+++ /dev/null
@@ -1,595 +0,0 @@
-% Embedded metasearching with the MasterKey Widget Set
-% Mike Taylor
-% July-September 2013
-
-
-Introduction
-------------
-
-There are lots of practical problems in building resource discovery
-solutions. One of the biggest, and most ubiquitous is incorporating
-metasearching functionality into existing web-sites -- for example,
-content-management systems, library catalogues or intranets. In
-general, even when access to core metasearching functionality is
-provided by simple web-services such as
-[Pazpar2](http://www.indexdata.com/pazpar2), integration work is seen
-as a major part of most projects.
-
-Index Data provides several different toolkits for communicating with
-its metasearching middleware, trading off varying degrees of
-flexibility against convenience:
-
-* pz2.js -- a low-level JavaScript library for interrogating the
-  Service Proxy and Pazpar2. It allows the HTML/JavaScript programmer
-  to create JavaScript applications display facets, records, etc. that
-  are fetched from the metasearching middleware.
-
-* masterkey-ui-core -- a higher-level, complex JavaScript library that
-  uses pz2.js to provide the pieces needed for building a
-  full-featured JavaScript application.
-
-* MasterKey Demo UI -- an example of a searching application built on
-  top of masterkey-ui-core. Available as a public demo at
-  http://mk2.indexdata.com/
-
-* MKDru -- a toolkit for embedding MasterKey-like searching into
-  Drupal sites.
-
-All of these approaches require programming to a greater or lesser
-extent. Against this backdrop, we introduced MKWS (the MasterKey
-Widget Set) -- a set of simple, very high-level HTML+CSS+JavaScript
-components that can be incorporated into any web-site to provide
-MasterKey searching facilities. By placing `<div>`s with well-known
-identifiers in any HTML page, the various components of an application
-can be embedded: search-boxes, results areas, target information, etc.
-
-
-Simple Example
---------------
-
-The following is a complete MKWS-based searching application:
-
-    <html>
-      <head>
-        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-        <title>MKWS demo client</title>
-        <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
-        <link rel="stylesheet" href="http://mkws.indexdata.com/mkws.css" />
-      </head>
-      <body>
-        <div id="mkwsSearch"></div>
-        <div id="mkwsResults"></div>
-      </body>
-    </html>
-
-Go ahead, try it! You don't even need a web-server. Just copy and
-paste this HTML into a file on your computer -- `/tmp/magic.html`,
-say -- and point your web-browser at it:
-`file:///tmp/magic.html`. Just like that, you have working
-metasearching.
-
-
-How the example works
----------------------
-
-If you know any HTML, the structure of the file will be familar to
-you: the `<html>` element at the top level contains a `<head>` and a
-`<body>`. In addition to whatever else you might want to put on your
-page, you can add MKWS elements.
-
-These fall into two categories. First, the prerequisites in the HTML
-header, which are loaded from the tool site mkws.indexdata.com:
-
-* `mkws-complete.js`
-  contains all the JavaScript needed by the widget-set.
-
-* `mkws.css`
-  provides the default CSS styling 
-
-Second, within the HTML body, `<div>` elements with special IDs that
-begin `mkws` can be provided. These are filled in by the MKWS code,
-and provide the components of the searching UI. The very simple
-application above has only two such components: a search box and a
-results area. But more are supported. The main `<div>`s are:
-
-* `mkwsSearch` -- provides the search box and button.
-
-* `mkwsResults` -- provides the results area, including a list of
-   brief records (which open out into full versions when clicked),
-   paging for large results sets, facets for refining a search,
-   sorting facilities, etc.
-
-* `mkwsLang` -- provides links to switch between one of several
-   different UI languages. By default, English, Danish and German are
-   provided.
-
-* `mkwsSwitch` -- provides links to switch between a view of the
-   result records and of the targets that provide them. Only
-   meaningful when `mkwsTargets` is also provided.
-
-* `mkwsTargets` -- the area where per-target information will appear
-   when selected by the link in the `mkwsSwitch` area. Of interest
-   mostly for fault diagnosis rather than for end-users.
-
-* `mkwsStat` --provides a status line summarising the statistics of
-   the various targets.
-
-To see all of these working together, just put them all into the HTML
-`<body>` like so:
-
-        <div id="mkwsSwitch"></div>
-        <div id="mkwsLang"></div>
-        <div id="mkwsSearch"></div>
-        <div id="mkwsResults"></div>
-        <div id="mkwsTargets"></div>
-        <div id="mkwsStat"></div>
-
-Configuration
--------------
-
-Many aspects of the behaviour of MKWS can be modified by setting
-parameters into the `mkws_config` object. **This must be done *before*
-including the MKWS JavaScript** so that when that code is executed it
-can refer to the configuration values. So the HTML header looks like
-this:
-
-        <script type="text/javascript">
-          var mkws_config = {
-            lang: "da",
-            sort_default: "title",
-            query_width: 60
-          };
-        </script>
-        <script type="text/javascript" src="http://mkws.indexdata.com/mkws-complete.js"></script>
-
-This configuration sets the UI language to Danish (rather than the
-default of English), initially sorts search results by title rather
-than relevance (though as always this can be changed in the UI) and
-makes the search box a bit wider than the default.
-
-The full set of supported configuration items is described in the
-reference guide below.
-
-
-Control over HTML and CSS
--------------------------
-
-More sophisticated applications will not simply place the `<div>`s
-together, but position them carefully within an existing page
-framework -- such as a Drupal template, an OPAC or a SharePoint page.
-
-While it's convenient for simple applications to use a monolithic
-`mkwsResults` area which contains record, facets, sorting options,
-etc., customised layouts may wish to treat each of these components
-separately. In this case, `mkwsResults` can be omitted, and the
-following lower-level components provided instead:
-
-* `mkwsTermlists` -- provides the facets
-
-* `mkwsRanking` -- provides the options for how records are sorted and
-   how many are included on each page of results.
-
-* `mkwsPager` -- provides the links for navigating back and forth
-   through the pages of records.
-
-* `mkwsNavi` -- when a search result has been narrowed by one or more
-   facets, this area shows the names of those facets, and allows the
-   selected values to be clicked in order to remove them.
-
-* `mkwsRecords` -- lists the actual result records.
-
-Customisation of MKWS searching widgets can also be achieved by
-overriding the styles set in the toolkit's CSS stylesheet. The default
-styles can be inspected in `mkws.css` and overridden in any
-styles that appears later in the HTML than that file. At the simplest
-level, this might just mean changing fonts, sizes and colours, but
-more fundamental changes are also possible.
-
-To properly apply styles, it's necessary to understand how the HTML is
-structured, e.g. which elements are nested within which
-containers. The structures used by the widget-set are described in the
-reference guide below.
-
-
-Refinements
------------
-
-
-### Message of the day
-
-Some applications might like to open with content in the area that
-will subsequently be filled with result-records -- a message of the
-day, a welcome message or a help page. This can be done by placing an
-`mkwsMOTDContainer` division on the page next to `mkwsResults` or
-`mkwsRecords`. The contents of this element are initially displayed,
-but will be hidden when a search is made.
-
-
-### Customised display using Handlebars templates
-
-Certain aspects of the widget-set's display can be customised by
-providing Handlebars templates with well-known IDs that begin with the
-string `mkwsTemplate`. At present, the supported templates are:
-
-* `mkwsTemplateSummary` -- used for each summary record in a list of
-  results.
-
-* `mkwsTemplateRecord` -- used when displaying a full record.
-
-For both of these the metadata record is passed in, and its fields can
-be referenced in the template. As well as the metadata fields
-(`md-*`), two special fields are provided to the `mkwsTemplateSummary`
-template, for creating popup links for full records. These are `_id`,
-which must be provided as the `id` attribute of a link tag, and
-`_onclick`, which must be provided as the `onclick` attribute.
-
-For example, an application can install a simple author+title summary
-record in place of the usual one providing the following template:
-
-        <script id="mkwsTemplateSummary" type="text/x-handlebars-template">
-          {{#if md-author}}
-            <span>{{md-author}}</span>
-          {{/if}}
-          <a href="#" id="{{_id}}" onclick="{{_onclick}}">
-            <b>{{md-title}}</b>
-          </a>
-        </script>
-
-For details of Handlebars template syntax, see
-[the online documentation](http://handlebarsjs.com/).
-
-
-### Responsive design
-
-Metasearching applications may need to appear differently on
-small-screened mobile devices, or change their appearance when
-screen-width changes (as when a small device is rotated). To achieve
-this, MKWS supports responsive design which will move the termlists to
-the bottom on narrow screens and to the sidebar on wide screens.
-
-To turn on this behaviour, set the `responsive_design_width` to the desired
-threshhold width in pixels. For example:
-
-        <script type="text/javascript">
-            var mkws_config = {
-                responsive_design_width: 990
-            };
-        </script>
-
-If individual result-related components are in use in place of the
-all-in-one mkwsResults, then the redesigned application needs to
-specify the locations where the termlists should appear in both
-cases. In this case, wrap the wide-screen `mkwsTermlists` element in a
-`mkwsTermlistContainer1` element; and provide an
-`mkwsTermlistContainer2` element in the place where the narrow-screen
-termlists should appear.
-
-
-### Popup results with jQuery UI
-
-The [jQuery UI library](http://en.wikipedia.org/wiki/JQuery_UI)
-can be used to construct MKWS applications in which the only component
-generally visible on the page is a search box, and the results appear
-in a popup. The key part of such an application is this invocation of
-the MKWS jQuery plugin:
-
-        <script type="text/javascript">
-          jQuery.pazpar2({ "layout":"popup", width:800, height:500 });
-        </script>
-
-The necessary scaffolding can be seen in an example application,
-http://example.indexdata.com/index-popup.html
-
-
-### Authentication and target configuration
-
-By default, MKWS configures itself to use a demonstration account on a
-service hosted by mkws.indexdata.com. This account (username `demo`,
-password `demo`) provides access to about a dozen free data
-sources. Authentication onto this service is via an authentication URL
-on the same MKWS server, so no explicit configuration is needed.
-
-In order to search in a customised set of targets, including
-subscription resources, it's necessary to create an account with
-Index Data's hosted service proxy, and protect that account with
-authentication tokens (to prevent unauthorised use of subscription
-resources). But in order to gain access to those resources, the
-authentication tokens have to be available to the widgets in some way,
-and simple embedding them in the JavaScript configuration is not
-acceptable because they are easy to read from there.
-
-The solution to this problem is in three steps.
-
-<b>First</b>
-the application's web-server creates a rewriting rule that takes an
-innocuous URL like
-http://example.indexdata.com/service-proxy-auth/
-and rewrites it as an access to Index Data's authentication service
-with authentication credentials embedded. This can be done using
-Apache2 directives such as
-
-    RewriteEngine on
-    RewriteRule /service-proxy-auth/
-        http://mkws.indexdata.com/service-proxy/?command=auth&action=login&username=U&password=PW [P]
-
-Because the credentials appear only in the application's web-server
-configuration, they are not visible to malicious users.
-
-<b>Second</b>, the broader application that includes MKWS widgets must
-protect access to the authentication URL on its own web-server. This
-can be done using IP authentication, a local username/password scheme,
-Kerberos or any other means.
-
-<b>Third</b>, the MKWS application must be configured to use the
-application-hosted authentication URL instead of the default one. This
-is done by means of the `service_proxy_auth` configuration element,
-which should be set to the authentication URL.
-
-Once these three steps are taken, the MKWS application will
-authenticate by means of a special URL on the application's web
-server, which the application prevents unauthorised access to, and the
-underlying credentials are hidden.
-
-
-Reference Guide
----------------
-
-### Configuration object
-
-The configuration object `mkws_config` may be created before including
-the MKWS JavaScript code to modify default behaviour. This structure
-is a key-value lookup table, whose entries are described in the table
-below. All entries are optional, but if specified must be given values
-of the specified type. If ommitted, each setting takes the indicated
-default value; long default values are in footnotes to keep the table
-reasonably narrow.
-
----
-Element                   Type    Default   Description
---------                  -----   --------- ------------
-debug_level               int     1         Level of debugging output to emit. 0 = none, 1 = messages, 2 = messages with
-                                            datestamps, 3 = messages with datestamps and stack-traces.
-
-facets                    array   *Note 1*  Ordered list of names of facets to display. Supported facet names are 
-                                            `sources`, `subjects` and `authors`.
-
-lang                      string  en        Code of the default language to display the UI in. Supported language codes are `en` =
-                                            English, `de` = German, `da` = Danish, and whatever additional languages are configured
-                                            using `language_*` entries (see below).
-
-lang_options              array   []        A list of the languages to offer as options. If empty (the default), then all
-                                            configured languages are listed.
-
-language_*                hash              Support for any number of languages can be added by providing entries whose name is
-                                            `language_` followed by the code of the language. See the separate section below for
-                                            details.
-
-pazpar2_url               string  *Note 2*  The URL used to access the metasearch middleware. This service must be configured to
-                                            provide search results, facets, etc. It may be either unmediated or Pazpar2 the
-                                            MasterKey Service Proxy, which mediates access to an underlying Pazpar2 instance. In
-                                            the latter case, `service_proxy_auth` must be provided.
-
-perpage_default           string  20        The initial value for the number of records to show on each page.
-
-perpage_options           array   *Note 3*  A list of candidate page sizes. Users can choose between these to determine how many
-                                            records are displayed on each page of results.
-
-query_width               int     50        The width of the query box, in characters.
-
-responsive_design_width   int               If defined, then the facets display moves between two locations as the screen-width
-                                            varies, as described above. The specified number is the threshhold width, in pixels,
-                                            at which the facets move between their two locations.
-
-service_proxy_auth        url     *Note 4*  A URL which, when `use_service_proxy` is true, is fetched once at the beginning of each
-                                            session to authenticate the user and establish a session that encompasses a defined set
-                                            of targets to search in.
-
-service_proxy_auth_domain domain            Can be set to the domain for which `service_proxy_auth` proxies authenticationm, so
-                                            that cookies are rewritten to appear to be from this domain. In general, this is not
-                                            necessary, as this setting defaults to the domain of `pazpar2_url`.
-
-show_lang                 bool    true      Indicates whether or not to display the language menu.
-
-show_perpage              bool    true      Indicates whether or not to display the perpage menu.
-
-show_sort                 bool    true      Indicates whether or not to display the sort menu.
-
-sort_default              string  relevance The label of the default sort criterion to use. Must be one of those in the `sort`
-                                            array.
-
-sort_options              array   *Note 6*  List of supported sort criteria. Each element of the list is itself a two-element list:
-                                            the first element of each sublist is a pazpar2 sort-expression such as `data:0` and
-                                            the second is a human-readable label such as `newest`.
-
-use_service_proxy         bool    true      If true, then a Service Proxy is used to deliver searching services rather than raw
-                                            Pazpar2.
----
-
-Perhaps we should get rid of the `show_lang`, `show_perpage` and
-`show_sort` configuration items, and simply display the relevant menus
-only when their containers are provided -- e.g. an `mkwsLang` element
-for the language menu. But for now we retain these, as an easier route
-to lightly customise the display than my changing providing a full HTML
-structure.
-
-#### Notes
-
-1. ["sources", "subjects", "authors"]
-
-2. /pazpar2/search.pz2
-
-3. [10, 20, 30, 50]
-
-4. http://mkws.indexdata.com/service-proxy-auth
-
-5. http://mkws.indexdata.com/service-proxy/
-
-6. [["relevance"], ["title:1", "title"], ["date:0", "newest"], ["date:1", "oldest"]]
-
-
-### Language specification
-
-Support for another UI language can be added by providing an entry in
-the `mkws_config` object whose name is `language_` followed by the
-name of the language: for example, `language_French` to support
-French. Then value of this entry must be a key-value lookup table,
-mapping the English-language strings of the UI into their equivalents
-in the specified language. For example:
-
-            var mkws_config = {
-              language_French: {
-                "Authors": "Auteurs",
-                "Subjects": "Sujets",
-                // ... and others ...
-              }
-            }
-
-The following strings occurring in the UI can be translated:
-`Displaying`,
-`Next`,
-`Prev`,
-`Records`,
-`Search`,
-`Sort by`,
-`Targets`,
-`Termlists`,
-`and show`,
-`found`,
-`of`,
-`per page`
-and
-`to`.
-
-In addition, facet names can be translated:
-`Authors`,
-`Sources`
-and
-`Subjects`.
-
-Finally, the names of fields in the full-record display can be
-translated. These include, but may not be limited to:
-`Author`,
-`Date`,
-`Location`,
-`Subject`
-and
-`Title`.
-
-
-
-### jQuery plugin invocation
-
-The MasterKey Widget Set can be invoked as a jQuery plugin rather than
-by providing an HTML skeleton explicitly. When this approach is used,
-the invocation is a single line of JavaScript:
-
-        <script>jQuery.pazpar2();</script>
-
-This code should be inserted in the page at the position where the
-metasearch should occur.
-
-When invoking this plugin, a key-value lookup table of named options
-may be passed in to modify the default behaviour, as in the exaple
-above. The available options are as follows:
-
----
-Element    Type    Default           Description
---------   -----   ---------         ------------
-layout     string  popup             Specifies how the user interface should
-                                     appear. Options are `table` (the default,
-                                     with facets at the bottom), `div` (with
-                                     facets at the side) and `popup` (to
-                                     obtain a popup window).
-
-width      int     880               Width of the popup window (if used), in
-                                     pixels.
-
-height     int     760               Height of the popup window (if used), in
-                                     pixels.
-
-id_button  string  input#mkwsButton  (Never change this.)
-
-id_popup   string  #mkwsPopup        (Never change this.)
----
-
-Note that when using the `popup` layout, facilities from the jQuery UI
-toolkit are used, so it's necessary to include both CSS and JavaScript
-from that toolkit. The relevant lines are:
-
-    <script src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
-    <link rel="stylesheet" type="text/css"
-          href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />
-
-
-### The structure of the HTML generated by the MKWS widgets
-
-In order to override the default CSS styles provided by the MasterKey Widget
-Set, it's necessary to understand that structure of the HTML elements that are
-generated within the components. This knowledge make it possible, for example,
-to style each `<div>` with class `term` but only when it occurs inside an
-element with ID `#mkwsTermlists`, so as to avoid inadvertently styling other
-elements using the same class in the non-MKWS parts of the page.
-
-The HTML structure is as follows. As in CSS, #ID indicates a unique identifier
-and .CLASS indicates an instance of a class.
-
-    #mkwsSwitch
-      a*
-
-    #mkwsLang
-      ( a | span )*
-
-    #mkwsSearch
-      form
-        input#mkwsQuery type=text
-        input#mkwsButton type=submit
-
-    #mkwsBlanket
-      (no contents -- used only for masking)
-
-    #mkwsResults
-      table
-        tbody
-          tr
-            td
-              #mkwsTermlists
-                div.title
-                div.facet*
-                  div.termtitle
-                  ( a span br )*
-            td
-              div#mkwsRanking
-                form#mkwsSelect
-                  select#mkwsSort
-                  select#mkwsPerpage
-              #mkwsPager
-              #mkwsNavi
-              #mkwsRecords
-                div.record*
-                  span (for sequence number)
-                  a (for title)
-                  span (for other information such as author)
-                  div.details (sometimes)
-                    table
-                      tbody
-                        tr*
-                          th
-                          td
-    #mkwsTargets
-      #mkwsBytarget
-        table
-          thead
-            tr*
-              td*
-          tbody
-            tr*
-              td*
-
-    #mkwsStat
-      span.head
-      span.clients
-      span.records
-
-- - -
-
-Copyright (C) 2013 by IndexData ApS, <http://www.indexdata.com>