pax_global_header00006660000000000000000000000064124542737040014522gustar00rootroot0000000000000052 comment=22bd2bf61d2dabee08f68413b6a8ff3cd4df4565 xapers-0.6/000077500000000000000000000000001245427370400126715ustar00rootroot00000000000000xapers-0.6/.gitignore000066400000000000000000000000441245427370400146570ustar00rootroot00000000000000*~ *.pyc dist build xapers.egg-info xapers-0.6/COPYING000066400000000000000000000012131245427370400137210ustar00rootroot00000000000000Xapers 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, (in the COPYING-GPL-3 file in this directory). If not, see http://www.gnu.org/licenses/ xapers-0.6/COPYING-GPL-3000066400000000000000000001043741245427370400145150ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. 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. Copyright (C) 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 . 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: Copyright (C) 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 . 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 . xapers-0.6/Makefile000066400000000000000000000017251245427370400143360ustar00rootroot00000000000000# -*- makefile -*- VERSION:=$(shell git describe --tags | sed -e s/_/~/ -e s/-/+/ -e s/-/~/) PV_FILE=lib/xapers/version.py .PHONY: all all: .PHONY: test test: ./test/xapers-test .PHONY: update-version update-version: echo "__version__ = '$(VERSION)'" >$(PV_FILE) .PHONY: release ifdef V update-version: VERSION:=$(V) release: VERSION:=$(V) release: update-version make test git commit -m "Update version for release $(VERSION)." $(PV_FILE) git tag --sign -m "Xapers $(VERSION) release." $(VERSION) else release: git tag -l | grep -v debian/ endif .PHONY: deb-snapshot deb-snapshot: rm -rf build/snapshot mkdir -p build/snapshot/debian git archive HEAD | tar -x -C build/snapshot/ git archive debian:debian | tar -x -C build/snapshot/debian/ cd build/snapshot; make update-version cd build/snapshot; dch -b -v $(VERSION) -D UNRELEASED 'test build, not for upload' cd build/snapshot; echo '3.0 (native)' > debian/source/format cd build/snapshot; debuild -us -uc xapers-0.6/NEWS000066400000000000000000000020371245427370400133720ustar00rootroot00000000000000Xapers 0.6 (2015-01-10) ======================= * lib: Document year is now indexed as a proper value, supporting both 'year:' binary and 'year:..' range searches. Will require running 'restore' from the cli to re-index previously added articles. Thanks to Rafael Laboissi猫re for the contribution. * lib: Much improved performance of Document prefixed term retrieval. * cli: Much improved search output performance. Default output limit of 20 has been removed. * nci: Curses interface now loads entries dynamically, so search results are no longer limited. * nci: Cleaner results display. Document 'summary' has been removed, but other information has been added, such as journal article, year, file info, and document match information. * nci: Extended navigation commands. * sources: Now support PDF download from sources, if the source supports it. * sources: New IACR 'crytoeprint' source. Thanks to Daniel Kahn Gillmor for the contribution. * Other cleanup, bug fixes, and performance improvements. xapers-0.6/README000066400000000000000000000112701245427370400135520ustar00rootroot00000000000000Xapers - personal journal article management system =================================================== Xapers is a personal document indexing system, geared towards academic journal articles. Think of it as your own personal document search engine, or a local cache of online libraries. It provides fast search of document text and bibliographic data and simple document and bibtex retrieval. Document files (in PDF format) and source identifiers (e.g. DOI) are parsed and indexed into a Xapian search engine [0]. Document text is extracted from the PDF and fully indexed. Bibliographic information downloaded from online libraries is indexed as prefixed search terms. Existing bibtex databases can be easily imported as well, including import of pdf files specified in Jabref/Mendeley format. Documents can be arbitrarily tagged. Original document files are easily retrievable from a simple curses search UI. The command line interface allows for exporting bibtex [1] from arbitrary searches, allowing seamless integration into LaTeX work flows. Xapers provides source modules for some common online libraries: * DOI: http://www.doi.org/ * arXiv: http://arxiv.org/ Contributions of additional source interface modules is highly encouraged. See the SOURCES file, included with the xapers source, for info on creating new sources. Xapers is heavily inspired by the notmuch mail indexing system [2]. [0] http://www.xapian.org/ [1] http://www.bibtex.org/ [2] http://notmuchmail.org/ Contact ======= Xapers was written by: Jameson Graef Rollins Xapers has a mailing list: xapers@lists.mayfirst.org https://lists.mayfirst.org/mailman/listinfo/xapers We also hang out on IRC: channel: #xapers server: irc.oftc.net Getting Xapers ============== Source ------ Clone the repo: $ git clone git://finestructure.net/xapers $ cd xapers Dependencies : * python (>= 2.6) * python-xapian - Python Xapian search engine bindings * pybtex - Python bibtex parser Recommends (for curses UI) : * poppler-utils - PDF processing tools * pycurl - Python bindings to libcurl * python-urwid - Python Urwid curses library * xdg-utils - Desktop tools for opening files and URLs * xclip - X clipboard support for copying document fields On Debian: $ sudo apt-get install python-xapian poppler-utils python-pycurl pybtex python-urwid xdg-utils xclip Run the tests: $ make test Debian ------ Debian/Ubuntu snapshot packages can be easily made from the git source. You can build the package from any branch but it requires an up-to-date local branch of origin/debian, e.g.: $ git branch debian origin/debian Then: $ sudo apt-get install build-essential devscripts pkg-config python-all-dev python-setuptools debhelper dpkg-dev fakeroot $ make debian-snapshot $ sudo dpkg -i build/xapers_0.1_amd64.deb Using Xapers ============ See the included xapers(1) man page for detailed usage and information on source modules and searching. Command line interface ---------------------- The main interface to Xapers is the xapers command line utility. From this interface you can import documents, search, tag, etc. The "add" command allows importing or updating single documents with sources. The "import" command allows importing an entire bibtex databases (.bib file). If the bibtex entries include "file" fields (ala. Mendeley or Jabref), then those files are retrieved, indexed, and imported as well. Curses interface ---------------- The curses interface (accessed through 'xapers show ...') provides a simple way to search the database and retrieve files. Documents matching searches are displayed with their bibliographic information and a short text summary. Document tags can be manipulated, files can be viewed, and source URLs can be opened in a browser. xapers-adder ------------ xapers-adder is a simple script that helps the adding of individual documents to your Xapers database. It can be used e.g. as a PDF handler in your favorite browser. It displays the PDF then presents the user with the option to import the document into Xapers. The user is prompted for any sources to retrieve and any initial tags to add. If the source is known, bibtex is retrieved and indexed. The resulting xapers entry for the document is displayed. Development of more clever import methods is highly encouraged. Python library -------------- Xapers is really a python library interface under the hood: >>> import xapers >>> db = xapers.Database('~/.xapers/docs') >>> docs = db.search('tag:new') >>> for doc in docs: doc.add_tags(['foo']) ... >>> Development of new interfaces to the underlying library is highly encouraged. xapers-0.6/SOURCES.txt000066400000000000000000000061011245427370400145530ustar00rootroot00000000000000= SOURCES = A Xapers "source" is a python module that describes how to interact with a single online journal database, from which document files and bibliographic data can be retrieved. Sources are assigned unique prefixes (e.g. "doi"). Online libraries associate unique document identifiers to individual documents (e.g. "10.1364/JOSAA.29.002092"). A particular online document is therefore described by a unique "source identifier", or "sid", which can take two equivalent forms: full URL http://dx.doi.org/10.1364/JOSAA.29.002092 : doi:10.1364/JOSAA.29.002092 == CUSTOM SOURCE MODULES == Custom source modules may be written to extend the base functionality of Xapers. A source module is described by a single python module (although it may import arbitrary other modules). The base name of the module file is interpreted as the nickname or 'prefix' for the source (e.g. if the module is named "doi.py" the source nickname will be "doi"). The module should include the following properties and functions. If any are missing, some xapers functionality may be undefined. description: a brief string description of the source, e.g.: description = "Digital Object Identifier" url: base URL of source, e.g.: url = 'http://dx.doi.org/' url_format: a printf format string that produces a valid source URL for a specified source identifier string, e.g.: url_format = 'http://dx.doi.org/%s' url_regex: a regular expression string that will match the source identifier string from a given full URL, e.g.: url_regex = 'http://dx.doi.org/(10\.\d{4,}[\w\d\:\.\-\/]+)' scan_regex: a regular expression string that will match the source identifier string in a scan of a documents plain text, e.g.: scan_regex = '(?:doi|DOI)[\s\.\:]{0,2}' + id_regex fetch_bibtex(id): a function that will return a bibtex string for a source document specified by id. fetch_file(id): a function that will return a (file_name, file_data) tuple for a source document specified by id. File should be in PDF format. If your source does not provide bibliographic data directly in bibtex format, the xapers.bibtex module has several helper functions for creating bibtex strings from python dictionaries (data2bib) or json objects (json2bib). See existing source module contributed with the xapers source as examples (lib/xapers/sources/). == SOURCE MODULE PATH == Once a custom source module has been created, place it ~/.xapers/sources. The module path can be overridden with the XAPERS_SOURCE_PATH environment variable, which can be a colon-separated list of directories to search for modules. == TESTING == Once a module is in place, use the xapers source* commands (sources, source2url, source2bib, source2file) to test it's functionality. Your new module should show up in the source listing with the "sources" command, and should be able to print the relevant data with the other commands. == CONTRIBUTING == If you think your module is stable and of general usefulness to the community, please consider contributing it upstream. Thanks! xapers-0.6/TODO000066400000000000000000000026601245427370400133650ustar00rootroot00000000000000* DB VERSION * add only opens writable db on doc.sync() * set/get title, author, tags as data or values (is this faster?) * pdf thumbnails: "convert -thumbnail 500x -background white -alpha remove file.pdf[0] thumb.png" ([0] == pdf page) * gtk gui, with pdf thumbs * rework db/doc interface * doc is just directory and xapian_doc * db does write/index on sync doc: * add ability to remove source sid * add ability to replace/remove file * what to do with doc 'data' field: * snippet/summary (current) * data for fast retrieval? * bib abstract * custom annotations/notes * nothing cli: * utilize meta-data pulled from parser * update should re-pull from existing source if available * export should produce full mirror of xapers document structure nci: * custom keybindings * customize helper commands * how to test?? * add update/add commands * customizable palette sources: * add 'hdl': http://handle.net/proxy.html parser: * extract metadata from pdfs * better handle parse errors * better pdf parser (native python: https://gist.github.com/pazz/5455090) * parsers for other document types ? * emacs UI (need json/sexp output) * rename file when importing and copying into docdir? * store bib data in different format (json instead of bibtex)? * clear old indexed terms when importing new file/bib? * vcs integration (git of root)? BUGS ==== * capitalized prefixed terms are not searchable - dcc:T00000 - key:HaEA2009a xapers-0.6/bin/000077500000000000000000000000001245427370400134415ustar00rootroot00000000000000xapers-0.6/bin/xapers000077500000000000000000000000451245427370400146700ustar00rootroot00000000000000#!/bin/sh exec python -m xapers "$@" xapers-0.6/bin/xapers-adder000077500000000000000000000014121245427370400157440ustar00rootroot00000000000000#!/bin/bash -e if [ -z "$1" ] || [[ "$1" == '--help' ]] || [[ "$1" == '-h' ]]; then echo "usage: $(basename $0) [--noterm] " >&2 exit 1 fi if [[ "$1" == '--noterm' ]]; then term=false shift else term=true fi infile="$1" if [ ! -e "$infile" ] ;then echo "File not found: $infile" >&2 exit 1 fi # open the file with preferred application nohup xdg-open "$infile" &>/dev/null & cmd=" echo 'Xapers-adder' echo '============' echo 'Type C-c at any time to cancel...' echo while ! xapers add --file=\"$infile\" --tags=new --prompt --view; do read -N1 -p 'ENTER to try again, or C-c to quit:' OK done " if [[ "$term" == 'true' ]] ; then exec x-terminal-emulator \ -title "xapers-adder" \ -e bash -c "$cmd" else eval "$cmd" fi xapers-0.6/lib/000077500000000000000000000000001245427370400134375ustar00rootroot00000000000000xapers-0.6/lib/xapers/000077500000000000000000000000001245427370400147415ustar00rootroot00000000000000xapers-0.6/lib/xapers/__init__.py000066400000000000000000000015711245427370400170560ustar00rootroot00000000000000""" This file is part of xapers. Xapers 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. Xapers 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 notmuch. If not, see . Copyright 2012 Jameson Rollins """ from database import Database from database import DatabaseError from database import DatabaseUninitializedError from database import DatabaseLockError from documents import Documents, Document xapers-0.6/lib/xapers/__main__.py000077500000000000000000000423531245427370400170450ustar00rootroot00000000000000""" This file is part of xapers. Xapers 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. Xapers 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 notmuch. If not, see . Copyright 2012-2014 Jameson Rollins """ import os import sys import codecs import signal import cli from source import Sources, SourceError from bibtex import Bibtex, BibtexError from parser import ParseError ######################################################################## PROG = 'xapers' def usage(): print "Usage:", PROG, " [args...]" print """ Commands: add [options] [] Add a new document or update existing. If provided, search should match a single document. --source=[|] source id, for online retrieval, or bibtex file path --file[=] PDF file to index and archive --tags=[,...] initial tags --prompt prompt for unspecified options --view view entry after adding import Import entries from a bibtex database. --tags=[,...] tags to apply to all imported documents delete Delete documents from database. --noprompt do not prompt to confirm deletion restore Restore database from an existing xapers root directory. tag +|- [...] [--] Add/remove tags. search [options] Search for documents. --output=[summary|bibtex|tags|sources|keys|files] output format (default is 'summary') --limit=N limit number of results returned bibtex Short for \"search --output=bibtex\". view View search in curses UI. count Count matches. export Export documents to a directory of files named for document titles. sources List available sources. source2url [...] Output URLs for sources. source2bib [...] Retrieve bibtex for sources and print to stdout. source2file Retrieve file for source and write to stdout. scandoc Scan PDF file for source ids. version Print version number. help [search] This usage, or search term help. The xapers document store is specified by the XAPERS_ROOT environment variable, or defaults to '~/.xapers/docs' if not specified (the directory is allowed to be a symlink). See 'xapers help search' for more information on term definitions and search syntax.""" def usage_search(): print """Xapers supports a common syntax for search terms. Search can consist of free-form text and quoted phrases. Terms can be combined with standard Boolean operators. All terms are combined with a logical OR by default. Parentheses can be used to group operators, but must be protect from shell interpretation. The string '*' will match all documents. Additionally, the following prefixed terms are understood (where indicate user-supplied values): id: Xapers document id author: string in authors (also a:) title: string in title (also t:) tag: specific user tag : specific source id (sid) source: specific Xapers source key: specific bibtex citation key year: specific publication year (also y:) year:.. publication year range (also y:) year:.. year:.. Publication years must be four-digit integers. See the following for more information on search terms: http://xapian.org/docs/queryparser.html""" ######################################################################## # combine a list of terms with spaces between, so that simple queries # don't have to be quoted at the shell level. def make_query_string(terms, require=True): string = str.join(' ', terms) if string == '': if require: print >>sys.stderr, "Must specify a search term." sys.exit(1) else: string = '*' return string def import_nci(): try: import nci except ImportError: print >>sys.stderr, "The python-urwid package does not appear to be installed." print >>sys.stderr, "Please install to be able to use the curses UI." sys.exit(1) return nci def set_stdout_codec(): # set the stdout codec to properly handle utf8 characters SYS_STDOUT = sys.stdout sys.stdout = codecs.getwriter('utf8')(sys.stdout) ######################################################################## if __name__ == '__main__': signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGPIPE, signal.SIG_DFL) if len(sys.argv) > 1: cmd = sys.argv[1] else: cmd = [] ######################################## if cmd in ['add','a']: tags = None infile = None sid = None prompt = False view = False query = None argc = 2 while True: if argc >= len(sys.argv): break elif '--source=' in sys.argv[argc]: sid = sys.argv[argc].split('=',1)[1] elif '--file' in sys.argv[argc]: if '=' in sys.argv[argc]: infile = sys.argv[argc].split('=',1)[1] else: infile = True elif '--tags=' in sys.argv[argc]: tags = sys.argv[argc].split('=',1)[1].split(',') elif '--prompt' in sys.argv[argc]: prompt = True elif '--view' in sys.argv[argc]: view = True else: break argc += 1 if argc == (len(sys.argv) - 1): query = make_query_string(sys.argv[argc:]) with cli.initdb(writable=True, create=True) as db: docid = cli.add(db, query, infile=infile, sid=sid, tags=tags, prompt=prompt) if view and docid: nci = import_nci() nci.UI(cmd=['search', 'id:'+str(docid)]) ######################################## elif cmd in ['import','i']: tags = [] argc = 2 while True: if argc >= len(sys.argv): break elif '--tags=' in sys.argv[argc]: tags = sys.argv[argc].split('=',1)[1].split(',') elif '--overwrite' in sys.argv[argc]: overwrite = True else: break argc += 1 try: bibfile = sys.argv[argc] except IndexError: print >>sys.stderr, "Must specify bibtex file to import." sys.exit(1) if not os.path.exists(bibfile): print >>sys.stderr, "File not found: %s" % bibfile sys.exit(1) with cli.initdb(writable=True, create=True) as db: cli.importbib(db, bibfile, tags=tags) ######################################## elif cmd in ['update']: argc = 2 query = make_query_string(sys.argv[argc:]) with cli.initdb(writable=True) as db: for doc in db.search(query): try: print >>sys.stderr, "Updating %s..." % doc.docid, doc.update_from_bibtex() doc.sync() print >>sys.stderr, "done." except: print >>sys.stderr, "\n" raise ######################################## elif cmd in ['delete']: prompt = True argc = 2 while True: if argc >= len(sys.argv): break elif '--noprompt' in sys.argv[argc]: prompt = False else: break argc += 1 query = make_query_string(sys.argv[argc:]) with cli.initdb(writable=True) as db: count = db.count(query) if count == 0: print >>sys.stderr, "No documents found for query." sys.exit(1) for doc in db.search(query): if prompt: resp = raw_input("Type 'yes' to delete document id:%d: " % doc.docid) if resp != 'yes': continue print >>sys.stderr, "deleting id:%d..." % doc.docid, doc.purge() print >>sys.stderr, "done." ######################################## elif cmd in ['search','s']: oformat = 'summary' limit = 0 argc = 2 while True: if argc >= len(sys.argv): break if '--output=' in sys.argv[argc]: oformat = sys.argv[argc].split('=')[1] elif '--limit=' in sys.argv[argc]: limit = int(sys.argv[argc].split('=')[1]) else: break argc += 1 if oformat not in ['summary','bibtex','tags','sources','keys','files']: print >>sys.stderr, "Unknown output format." sys.exit(1) query = make_query_string(sys.argv[argc:]) set_stdout_codec() with cli.initdb() as db: cli.search(db, query, oformat=oformat, limit=limit) ######################################## elif cmd in ['bibtex','bib','b']: argc = 2 query = make_query_string(sys.argv[argc:]) set_stdout_codec() with cli.initdb() as db: cli.search(db, query, oformat='bibtex') ######################################## elif cmd in ['nci','view','show','select']: nci = import_nci() if cmd == 'nci': args = sys.argv[2:] else: query = make_query_string(sys.argv[2:], require=False) args = ['search', query] nci.UI(cmd=args) ######################################## elif cmd in ['tag','t']: add_tags = [] remove_tags = [] argc = 2 for arg in sys.argv[argc:]: if argc >= len(sys.argv): break if arg == '--': argc += 1 continue if arg[0] == '+': add_tags.append(arg[1:]) elif arg[0] == '-': remove_tags.append(arg[1:]) else: break argc += 1 if not add_tags and not remove_tags: print >>sys.stderr, "Must specify tags to add or remove." sys.exit(1) if '' in add_tags: print >>sys.stderr, "Null tags not allowed." sys.exit(1) query = make_query_string(sys.argv[argc:]) with cli.initdb(writable=True) as db: for doc in db.search(query): doc.add_tags(add_tags) doc.remove_tags(remove_tags) doc.sync() ######################################## elif cmd in ['dumpterms']: prefix = None argc = 2 while True: if argc >= len(sys.argv): break if '--prefix=' in sys.argv[argc]: prefix = sys.argv[argc].split('=')[1] else: break argc += 1 query = make_query_string(sys.argv[argc:], require=True) with cli.initdb() as db: if query == '*': for term in db.term_iter(prefix): print term else: for doc in db.search(query): for term in doc.term_iter(prefix): print term ######################################## elif cmd in ['maxid']: docid = 0 with cli.initdb() as db: for doc in db.search('*'): docid = max(docid, doc.docid) print 'id:%d' % docid ######################################## elif cmd in ['count']: query = make_query_string(sys.argv[2:], require=False) with cli.initdb() as db: print db.count(query) ######################################## elif cmd in ['export']: outdir = sys.argv[2] query = make_query_string(sys.argv[3:]) set_stdout_codec() with cli.initdb() as db: cli.export(db, outdir, query) ######################################## elif cmd in ['restore']: with cli.initdb(writable=True, create=True, force=True) as db: db.restore(log=True) ######################################## elif cmd in ['sources']: sources = Sources() w = 0 for source in sources: w = max(len(source.name), w) format = '%'+str(w)+'s: %s[%s]' for source in sources: name = source.name desc = '' try: desc += '%s ' % source.description except AttributeError: pass try: desc += '(%s) ' % source.url except AttributeError: pass if source.is_builtin: path = 'builtin' else: path = source.path print format % (name, desc, path) ######################################## elif cmd in ['source2bib', 's2b', 'source2url', 's2u', 'source2file', 's2f']: outraw = False argc = 2 for arg in sys.argv[argc:]: if argc >= len(sys.argv): break elif sys.argv[argc] == '--raw': outraw = True else: break argc += 1 try: sss = sys.argv[argc:] except IndexError: print >>sys.stderr, "Must specify source to retrieve." sys.exit(1) if cmd in ['source2file', 's2f']: if len(sss) > 1: print >>sys.stderr, "source2file can only retrieve file for single source." sys.exit(1) sources = Sources() for ss in sss: try: item = sources.match_source(ss) except SourceError as e: print >>sys.stderr, e sys.exit(1) if cmd in ['source2url', 's2u']: print item.url continue elif cmd in ['source2bib', 's2b']: try: bibtex = item.fetch_bibtex() except Exception as e: print >>sys.stderr, "Could not retrieve bibtex: %s" % e sys.exit(1) if outraw: print bibtex else: try: print Bibtex(bibtex)[0].as_string() except: print >>sys.stderr, "Failed to parse retrieved bibtex data." print >>sys.stderr, "Use --raw option to view raw retrieved data." sys.exit(1) elif cmd in ['source2file', 's2f']: try: name, data = item.fetch_file() print data except Exception as e: print >>sys.stderr, "Could not retrieve file: %s" % e sys.exit(1) ######################################## elif cmd in ['scandoc','sd']: try: infile = sys.argv[2] except IndexError: print >>sys.stderr, "Must specify document to scan." sys.exit(1) try: items = Sources().scan_file(infile) except ParseError as e: print >>sys.stderr, "Parse error: %s" % e print >>sys.stderr, "Is file '%s' a PDF?" % infile sys.exit(1) for item in items: print item ######################################## elif cmd in ['version','--version','-v']: import version print 'xapers', version.__version__ ######################################## elif cmd in ['help','h','--help','-h']: if len(sys.argv) > 2: if sys.argv[2] == 'search': usage_search() else: usage() ######################################## else: if cmd: print >>sys.stderr, "Unknown command '%s'." % cmd else: print >>sys.stderr, "Command not specified." print >>sys.stderr, "See \"help\" for more information." sys.exit(1) xapers-0.6/lib/xapers/bibtex.py000066400000000000000000000121501245427370400165670ustar00rootroot00000000000000import os import sys import io import json import pybtex from pybtex.core import Entry, Person from pybtex.bibtex.utils import split_name_list from pybtex.database.input import bibtex as inparser from pybtex.database.output import bibtex as outparser def clean_bib_string(string): for char in ['{', '}']: string = string.replace(char,'') return string ################################################## class BibtexError(Exception): """Base class for Xapers bibtex exceptions.""" def __init__(self, msg): self.msg = msg def __str__(self): return self.msg ################################################## class Bibtex(): """Represents a bibtex database.""" # http://www.bibtex.org/Format/ def __init__(self, bibtex): parser = inparser.Parser(encoding='utf-8') if os.path.exists(bibtex): bibdata = parser.parse_file(bibtex) else: # StringIO requires unicode input # http://nedbatchelder.com/text/unipain.html assert type(bibtex) is unicode, "Bibtex strings must be unicode" with io.StringIO(bibtex) as stream: bibdata = parser.parse_stream(stream) self.keys = bibdata.entries.keys() self.entries = bibdata.entries.values() self.index = -1 self.max = len(self.entries) def __getitem__(self, index): key = self.keys[index] entry = self.entries[index] return Bibentry(key, entry) def __iter__(self): return self def __len__(self): return self.max def next(self): self.index = self.index + 1 if self.index == self.max: raise StopIteration return self[self.index] ################################################## class Bibentry(): """Represents an individual entry in a bibtex database""" def __init__(self, key, entry): self.key = key self.entry = entry def get_authors(self): """Return a list of authors.""" authors = [] if 'author' in self.entry.persons: for p in self.entry.persons['author']: authors.append(clean_bib_string(unicode(p))) return authors def get_fields(self): """Return a dict of non-author fields.""" bibfields = self.entry.fields # entry.fields is actually already a dict, but we want to # clean the strings first fields = {} for field in bibfields: fields[field] = unicode(clean_bib_string(bibfields[field])) return fields def set_file(self, path): # FIXME: what's the REAL proper format for this self.entry.fields['file'] = ':%s:%s' % (path, 'pdf') def get_file(self): """Returns file path if file field exists. Expects either single path string or Mendeley/Jabref format.""" try: parsed = self.entry.fields['file'].split(':') if len(parsed) > 1: return parsed[1] else: return parsed[0] except KeyError: return None except IndexError: return None def _entry2db(self): db = pybtex.database.BibliographyData() db.add_entry(self.key, self.entry) return db def as_string(self): """Return entry as formatted bibtex string.""" writer = outparser.Writer() with io.StringIO() as stream: writer.write_stream(self._entry2db(), stream) string = stream.getvalue() string = string.strip() return string def to_file(self, path): """Write entry bibtex to file.""" writer = outparser.Writer(encoding='utf-8') writer.write_file(self._entry2db(), path) ################################################## def data2bib(data, key, type='article'): """Convert a python dict into a Bibentry object.""" if not data: return # need to remove authors field from data authors = None if 'authors' in data: authors = data['authors'] if isinstance(authors, str): authors = split_name_list(authors) if len(authors) == 1: authors = authors[0].split(',') del data['authors'] entry = Entry(type, fields=data) if authors: for p in authors: entry.add_person(Person(p), 'author') return Bibentry(key, entry).as_string() def json2bib(jsonstring, key, type='article'): """Convert a json string into a Bibentry object.""" if not json: return data = json.loads(jsonstring) # need to remove authors field from data authors = None if 'author' in data: authors = data['author'] del data['author'] if 'issued' in data: data['year'] = str(data['issued']['date-parts'][0][0]) del data['issued'] # delete other problematic fields if 'editor' in data: del data['editor'] entry = Entry(type, fields=data) if authors: for author in authors: entry.add_person(Person(first=author['given'], last=author['family']), 'author') return Bibentry(key, entry).as_string() xapers-0.6/lib/xapers/cli.py000066400000000000000000000327561245427370400160770ustar00rootroot00000000000000""" This file is part of xapers. Xapers 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. Xapers 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 notmuch. If not, see . Copyright 2012, 2013 Jameson Rollins """ import os import sys import sets import shutil import readline import database from documents import Document from source import Sources, SourceError from parser import ParseError from bibtex import Bibtex, BibtexError ############################################################ def initdb(writable=False, create=False, force=False): xroot = os.getenv('XAPERS_ROOT', os.path.expanduser(os.path.join('~','.xapers','docs'))) try: return database.Database(xroot, writable=writable, create=create, force=force) except database.DatabaseUninitializedError as e: print >>sys.stderr, e print >>sys.stderr, "Import a document to initialize." sys.exit(1) except database.DatabaseInitializationError as e: print >>sys.stderr, e print >>sys.stderr, "Either clear the directory and add new files, or use 'retore' to restore from existing data." sys.exit(1) except database.DatabaseError as e: print >>sys.stderr, e sys.exit(1) ############################################################ # readline completion class class Completer: def __init__(self, words): self.words = words def terms(self, prefix, index): matching_words = [ w for w in self.words if w.startswith(prefix) ] try: return matching_words[index] except IndexError: return None def prompt_for_file(infile): if infile: print >>sys.stderr, 'file: %s' % infile else: readline.set_startup_hook() readline.parse_and_bind('') readline.set_completer() infile = raw_input('file: ') if infile == '': infile = None return infile def prompt_for_source(db, sources): if sources: readline.set_startup_hook(lambda: readline.insert_text(sources[0])) elif db: sources = list(db.term_iter('source')) readline.parse_and_bind("tab: complete") completer = Completer(sources) readline.set_completer(completer.terms) readline.set_completer_delims(' ') source = raw_input('source: ') if source == '': source = None return source def prompt_for_tags(db, tags): # always prompt for tags, and append to initial if tags: print >>sys.stderr, 'initial tags: %s' % ' '.join(tags) else: tags = [] if db: itags = list(db.term_iter('tag')) else: itags = None readline.set_startup_hook() readline.parse_and_bind("tab: complete") completer = Completer(itags) readline.set_completer(completer.terms) readline.set_completer_delims(' ') while True: tag = raw_input('tag: ') if tag and tag != '': tags.append(tag.strip()) else: break return tags ############################################################ def print_doc_summary(doc): docid = doc.docid title = doc.get_title() if not title: title = '' tags = doc.get_tags() sources = doc.get_sids() key = doc.get_key() if not key: key = '' print "id:%d [%s] {%s} (%s) \"%s\"" % ( docid, ' '.join(sources), key, ' '.join(tags), title, ) ############################################################ def add(db, query_string, infile=None, sid=None, tags=None, prompt=False): doc = None bibtex = None sources = Sources() doc_sid = sid source = None file_data = None if infile and infile is not True: infile = os.path.expanduser(infile) ################################## # if query provided, find single doc to update if query_string: if db.count(query_string) != 1: print >>sys.stderr, "Search '%s' did not match a single document." % query_string print >>sys.stderr, "Aborting." sys.exit(1) for doc in db.search(query_string): break ################################## # do fancy option prompting if prompt: doc_sids = [] if doc_sid: doc_sids = [doc_sid] # scan the file for source info if infile is not True: infile = prompt_for_file(infile) print >>sys.stderr, "Scanning document for source identifiers..." try: ss = sources.scan_file(infile) except ParseError as e: print >>sys.stderr, "\n" print >>sys.stderr, "Parse error: %s" % e sys.exit(1) if len(ss) == 0: print >>sys.stderr, "0 source ids found." else: if len(ss) == 1: print >>sys.stderr, "1 source id found:" else: print >>sys.stderr, "%d source ids found:" % (len(ss)) for sid in ss: print >>sys.stderr, " %s" % (sid) doc_sids += [s.sid for s in ss] doc_sid = prompt_for_source(db, doc_sids) tags = prompt_for_tags(db, tags) if not query_string and not infile and not doc_sid: print >>sys.stderr, "Must specify file or source to import, or query to update existing document." sys.exit(1) ################################## # process source and get bibtex # check if source is a file, in which case interpret it as bibtex if doc_sid and os.path.exists(doc_sid): bibtex = doc_sid elif doc_sid: # get source object for sid string try: source = sources.match_source(doc_sid) except SourceError as e: print >>sys.stderr, e sys.exit(1) # check that the source doesn't match an existing doc sdoc = db.doc_for_source(source.sid) if sdoc: if doc and sdoc != doc: print >>sys.stderr, "A different document already exists for source '%s'." % (doc_sid) print >>sys.stderr, "Aborting." sys.exit(1) print >>sys.stderr, "Source '%s' found in database. Updating existing document..." % (doc_sid) doc = sdoc try: print >>sys.stderr, "Retrieving bibtex...", bibtex = source.fetch_bibtex() print >>sys.stderr, "done." except SourceError as e: print >>sys.stderr, "\n" print >>sys.stderr, "Could not retrieve bibtex: %s" % e sys.exit(1) if infile is True: try: print >>sys.stderr, "Retrieving file...", file_name, file_data = source.fetch_file() print >>sys.stderr, "done." except SourceError as e: print >>sys.stderr, "\n" print >>sys.stderr, "Could not retrieve file: %s" % e sys.exit(1) elif infile is True: print >>sys.stderr, "Must specify source with retrieve file option." sys.exit(1) if infile and not file_data: with open(infile, 'r') as f: file_data = f.read() file_name = os.path.basename(infile) ################################## # if we still don't have a doc, create a new one if not doc: doc = Document(db) ################################## # add stuff to the doc if bibtex: try: print >>sys.stderr, "Adding bibtex...", doc.add_bibtex(bibtex) print >>sys.stderr, "done." except BibtexError as e: print >>sys.stderr, "\n" print >>sys.stderr, e print >>sys.stderr, "Bibtex must be a plain text file with a single bibtex entry." sys.exit(1) except: print >>sys.stderr, "\n" raise # add source sid if it hasn't been added yet if source and not doc.get_sids(): doc.add_sid(source.sid) if infile: try: print >>sys.stderr, "Adding file...", doc.add_file_data(file_name, file_data) print >>sys.stderr, "done." except ParseError as e: print >>sys.stderr, "\n" print >>sys.stderr, "Parse error: %s" % e sys.exit(1) except: print >>sys.stderr, "\n" raise if tags: try: print >>sys.stderr, "Adding tags...", doc.add_tags(tags) print >>sys.stderr, "done." except: print >>sys.stderr, "\n" raise ################################## # sync the doc to db and disk try: print >>sys.stderr, "Syncing document...", doc.sync() print >>sys.stderr, "done.\n", except: print >>sys.stderr, "\n" raise print_doc_summary(doc) return doc.docid ############################################ def importbib(db, bibfile, tags=[], overwrite=False): errors = [] sources = Sources() for entry in sorted(Bibtex(bibfile), key=lambda entry: entry.key): print >>sys.stderr, entry.key try: docs = [] # check for doc with this bibkey bdoc = db.doc_for_bib(entry.key) if bdoc: docs.append(bdoc) # check for known sids for source in sources.scan_bibentry(entry): sdoc = db.doc_for_source(source.sid) # FIXME: why can't we match docs in list? if sdoc and sdoc.docid not in [doc.docid for doc in docs]: docs.append(sdoc) if len(docs) == 0: doc = Document(db) elif len(docs) > 0: if len(docs) > 1: print >>sys.stderr, " Multiple distinct docs found for entry. Using first found." doc = docs[0] print >>sys.stderr, " Updating id:%d..." % (doc.docid) doc.add_bibentry(entry) filepath = entry.get_file() if filepath: print >>sys.stderr, " Adding file: %s" % filepath doc.add_file(filepath) doc.add_tags(tags) doc.sync() except BibtexError as e: print >>sys.stderr, " Error processing entry %s: %s" % (entry.key, e) print >>sys.stderr errors.append(entry.key) if errors: print >>sys.stderr print >>sys.stderr, "Failed to import %d" % (len(errors)), if len(errors) == 1: print >>sys.stderr, "entry", else: print >>sys.stderr, "entries", print >>sys.stderr, "from bibtex:" for error in errors: print >>sys.stderr, " %s" % (error) sys.exit(1) else: sys.exit(0) ############################################ def search(db, query_string, oformat='summary', limit=None): if query_string == '*' and oformat in ['tags','sources','keys']: if oformat == 'tags': for tag in db.term_iter('tag'): print tag elif oformat == 'sources': for source in db.get_sids(): print source elif oformat == 'keys': for key in db.term_iter('key'): print key return otags = set([]) osources = set([]) okeys = set([]) for doc in db.search(query_string, limit=limit): if oformat in ['summary']: print_doc_summary(doc) continue elif oformat in ['file','files']: for path in doc.get_fullpaths(): print "%s" % (path) continue elif oformat == 'bibtex': bibtex = doc.get_bibtex() if not bibtex: print >>sys.stderr, "No bibtex for doc id:%d." % doc.docid else: print bibtex print continue if oformat == 'tags': otags = otags | set(doc.get_tags()) elif oformat == 'sources': osources = osources | set(doc.get_sids()) elif oformat == 'keys': key = doc.get_key() if key: print key if oformat == 'tags': for tag in otags: print tag elif oformat == 'sources': for source in osources: print source ############################################ def export(db, outdir, query_string): try: os.makedirs(outdir) except: pass import pipes for doc in db.search(query_string): title = doc.get_title() origpaths = doc.get_fullpaths() nfiles = len(origpaths) for path in origpaths: if not title: name = os.path.basename(os.path.splitext(path)[0]) else: name = '%s' % (title.replace(' ','_')) ind = 0 if nfiles > 1: name += '.%s' % ind ind += 1 name += '.pdf' outpath = os.path.join(outdir,name) print outpath shutil.copyfile(path, outpath.encode('utf-8')) xapers-0.6/lib/xapers/database.py000066400000000000000000000275501245427370400170700ustar00rootroot00000000000000""" This file is part of xapers. Xapers 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. Xapers 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 notmuch. If not, see . Copyright 2012, 2013 Jameson Rollins """ import os import sys import xapian from source import Sources from documents import Documents, Document # FIXME: add db schema documentation ################################################## class DatabaseError(Exception): """Base class for Xapers database exceptions.""" def __init__(self, msg): self.msg = msg def __str__(self): return self.msg class DatabaseUninitializedError(DatabaseError): pass class DatabaseInitializationError(DatabaseError): pass class DatabaseLockError(DatabaseError): pass ################################################## class Database(): """Represents a Xapers database""" # http://xapian.org/docs/omega/termprefixes.html BOOLEAN_PREFIX_INTERNAL = { # FIXME: use this for doi? #'url': 'U', 'file': 'P', # FIXME: use this for doc mime type 'type': 'T', } BOOLEAN_PREFIX_EXTERNAL = { 'id': 'Q', 'key': 'XBIB|', 'source': 'XSOURCE|', 'tag': 'K', 'year': 'Y', 'y': 'Y', } PROBABILISTIC_PREFIX = { 'title': 'S', 't': 'S', 'author': 'A', 'a': 'A', } # http://xapian.org/docs/facets NUMBER_VALUE_FACET = { 'year': 0, 'y': 0, } # FIXME: need to set the following value fields: # publication date # added date # modified date # FIXME: need database version def _find_prefix(self, name): if name in self.BOOLEAN_PREFIX_INTERNAL: return self.BOOLEAN_PREFIX_INTERNAL[name] if name in self.BOOLEAN_PREFIX_EXTERNAL: return self.BOOLEAN_PREFIX_EXTERNAL[name] if name in self.PROBABILISTIC_PREFIX: return self.PROBABILISTIC_PREFIX[name] def _find_facet(self, name): if name in self.NUMBER_VALUE_FACET: return self.NUMBER_VALUE_FACET[name] def _make_source_prefix(self, source): return 'X%s|' % (source.upper()) ######################################## def __init__(self, root, writable=False, create=False, force=False): # xapers root self.root = os.path.abspath(os.path.expanduser(root)) # xapers db directory xapers_path = os.path.join(self.root, '.xapers') # xapes directory initialization if not os.path.exists(xapers_path): if create: if os.path.exists(self.root): if os.listdir(self.root) and not force: raise DatabaseInitializationError('Uninitialized Xapers root directory exists but is not empty.') os.makedirs(xapers_path) else: if os.path.exists(self.root): raise DatabaseInitializationError("Xapers directory '%s' does not contain a database." % (self.root)) else: raise DatabaseUninitializedError("Xapers directory '%s' not found." % (self.root)) # the Xapian db xapian_path = os.path.join(xapers_path, 'xapian') if writable: try: self.xapian = xapian.WritableDatabase(xapian_path, xapian.DB_CREATE_OR_OPEN) except xapian.DatabaseLockError: raise DatabaseLockError("Xapers database locked.") else: self.xapian = xapian.Database(xapian_path) stemmer = xapian.Stem("english") # The Xapian TermGenerator # http://trac.xapian.org/wiki/FAQ/TermGenerator self.term_gen = xapian.TermGenerator() self.term_gen.set_stemmer(stemmer) # The Xapian QueryParser self.query_parser = xapian.QueryParser() self.query_parser.set_database(self.xapian) self.query_parser.set_stemmer(stemmer) self.query_parser.set_stemming_strategy(xapian.QueryParser.STEM_SOME) self.query_parser.set_default_op(xapian.Query.OP_AND) # add boolean internal prefixes for name, prefix in self.BOOLEAN_PREFIX_EXTERNAL.iteritems(): self.query_parser.add_boolean_prefix(name, prefix) # add probabalistic prefixes for name, prefix in self.PROBABILISTIC_PREFIX.iteritems(): self.query_parser.add_prefix(name, prefix) # add value facets for name, facet in self.NUMBER_VALUE_FACET.iteritems(): self.query_parser.add_valuerangeprocessor( xapian.NumberValueRangeProcessor(facet, name+':') ) # register known source prefixes # FIXME: can we do this by just finding all XSOURCE terms in # db? Would elliminate dependence on source modules at # search time. for source in Sources(): name = source.name self.query_parser.add_boolean_prefix(name, self._make_source_prefix(name)) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.xapian.close() def reopen(self): self.xapian.reopen() def __contains__(self, docid): try: self.xapian.get_document(docid) return True except xapian.DocNotFoundError: return False def __getitem__(self, docid): if type(docid) not in [int, long]: raise TypeError("docid must be an int") xapian_doc = self.xapian.get_document(docid) return Document(self, xapian_doc) ######################################## # generate a new doc id, based on the last availabe doc id def _generate_docid(self): return self.xapian.get_lastdocid() + 1 ######################################## # return a list of terms for prefix def _term_iter(self, prefix=None): term_iter = iter(self.xapian) if prefix: plen = len(prefix) term = term_iter.skip_to(prefix) if not term.term.startswith(prefix): return yield term.term[plen:] for term in term_iter: if prefix: if not term.term.startswith(prefix): break yield term.term[plen:] else: yield term.term def term_iter(self, name=None): """Iterator over all terms in the database. If a prefix is provided, will iterate over only the prefixed terms, and the prefix will be removed from the returned terms. """ prefix = None if name: prefix = self._find_prefix(name) if not prefix: prefix = name return self._term_iter(prefix) def get_sids(self): """Get all sources in database.""" sids = [] # FIXME: do this more efficiently for source in self.term_iter('source'): for oid in self._term_iter(self._make_source_prefix(source)): sids.append('%s:%s' % (source, oid)) return sids ######################################## # search for documents based on query string def _search(self, query_string, limit=None): enquire = xapian.Enquire(self.xapian) if query_string == "*": query = xapian.Query.MatchAll else: # parse the query string to produce a Xapian::Query object. query = self.query_parser.parse_query(query_string) if os.getenv('XAPERS_DEBUG_QUERY'): print >>sys.stderr, "query string:", query_string print >>sys.stderr, "final query:", query # FIXME: need to catch Xapian::Error when using enquire enquire.set_query(query) # set order of returned docs as newest first # FIXME: make this user specifiable enquire.set_docid_order(xapian.Enquire.DESCENDING) if limit: mset = enquire.get_mset(0, limit) else: mset = enquire.get_mset(0, self.xapian.get_doccount()) return mset def search(self, query_string, limit=0): """Search for documents in the database.""" mset = self._search(query_string, limit) return Documents(self, mset) def count(self, query_string): """Count documents matching search terms.""" return self._search(query_string).get_matches_estimated() def _doc_for_term(self, term): enquire = xapian.Enquire(self.xapian) query = xapian.Query(term) enquire.set_query(query) mset = enquire.get_mset(0, 2) # FIXME: need to throw an exception if more than one match found if mset: return Document(self, mset[0].document) else: return None def doc_for_path(self, path): """Return document for specified path.""" term = self._find_prefix('file') + path return self._doc_for_term(term) def doc_for_source(self, sid): """Return document for source id string.""" source, oid = sid.split(':', 1) term = self._make_source_prefix(source) + oid return self._doc_for_term(term) def doc_for_bib(self, bibkey): """Return document for bibtex key.""" term = self._find_prefix('key') + bibkey return self._doc_for_term(term) ######################################## def replace_document(self, docid, doc): """Replace (sync) document to database.""" self.xapian.replace_document(docid, doc) def delete_document(self, docid): """Delete document from database.""" self.xapian.delete_document(docid) ######################################## def restore(self, log=False): """Restore a database from an existing root.""" docdirs = os.listdir(self.root) docdirs.sort() for ddir in docdirs: if ddir == '.xapers': continue docdir = os.path.join(self.root, ddir) if not os.path.isdir(docdir): # skip things that aren't directories continue if log: print >>sys.stderr, docdir # if we can't convert the directory name into an integer, # assume it's not relevant to us and continue try: docid = int(ddir) except ValueError: continue docfiles = os.listdir(docdir) if not docfiles: # skip empty directories continue if log: print >>sys.stderr, ' docid:', docid try: doc = self[docid] except xapian.DocNotFoundError: doc = Document(self, docid=docid) for dfile in docfiles: dpath = os.path.join(docdir, dfile) if dfile == 'bibtex': if log: print >>sys.stderr, ' adding bibtex' doc.add_bibtex(dpath) elif os.path.splitext(dpath)[1] == '.pdf': if log: print >>sys.stderr, ' adding file:', dfile doc.add_file(dpath) elif dfile == 'tags': if log: print >>sys.stderr, ' adding tags' with open(dpath, 'r') as f: tags = f.read().strip().split('\n') doc.add_tags(tags) doc.sync() xapers-0.6/lib/xapers/documents.py000066400000000000000000000355721245427370400173300ustar00rootroot00000000000000""" This file is part of xapers. Xapers 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. Xapers 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 notmuch. If not, see . Copyright 2012, 2013 Jameson Rollins """ import os import shutil import xapian from parser import parse_data from source import Sources from bibtex import Bibtex ################################################## class DocumentError(Exception): """Base class for Xapers document exceptions.""" def __init__(self, msg): self.msg = msg def __str__(self): return self.msg ################################################## class Documents(): """Represents a set of Xapers documents given a Xapian mset.""" def __init__(self, db, mset): self.db = db self.mset = mset self.index = -1 self.max = len(mset) def __getitem__(self, index): m = self.mset[index] doc = Document(self.db, m.document) doc.matchp = m.percent return doc def __iter__(self): return self def __len__(self): return self.max def next(self): self.index = self.index + 1 if self.index == self.max: raise StopIteration return self[self.index] ################################################## class Document(): """Represents a single Xapers document.""" def __init__(self, db, xapian_doc=None, docid=None): # Xapers db self.db = db # if Xapian doc provided, initiate for that document if xapian_doc: self.xapian_doc = xapian_doc self.docid = xapian_doc.get_docid() # else, create a new empty document # document won't be added to database until sync is called else: self.xapian_doc = xapian.Document() # use specified docid if provided if docid: if docid in self.db: raise DocumentError('Document already exists for id %d.' % docid) self.docid = docid else: self.docid = self.db._generate_docid() self._add_term(self.db._find_prefix('id'), self.docid) # specify a directory in the Xapers root for document data self.docdir = os.path.join(self.db.root, '%010d' % self.docid) self.bibentry = None self._infiles = {} def get_docid(self): """Return document id of document.""" return self.docid def _make_docdir(self): if os.path.exists(self.docdir): if not os.path.isdir(self.docdir): raise DocumentError('File exists at intended docdir location: %s' % self.docdir) else: os.makedirs(self.docdir) def _write_files(self): for name, data in self._infiles.iteritems(): path = os.path.join(self.docdir, name) with open(path, 'w') as f: f.write(data) def _write_bibfile(self): bibpath = self.get_bibpath() # reload bibtex only if we have new files paths = self.get_fullpaths() if paths: self._load_bib() if self.bibentry: # we put only the first file in the bibtex # FIXME: does jabref/mendeley spec allow for multiple files? if paths and not self.bibentry.get_file(): self.bibentry.set_file(paths[0]) self.bibentry.to_file(bibpath) def _write_tagfile(self): with open(os.path.join(self.docdir, 'tags'), 'w') as f: for tag in self.get_tags(): f.write(tag) f.write('\n') def _rm_docdir(self): if os.path.exists(self.docdir) and os.path.isdir(self.docdir): shutil.rmtree(self.docdir) def sync(self): """Sync document to database.""" # FIXME: add value for modification time # FIXME: catch db not writable errors try: self._make_docdir() self._write_files() self._write_bibfile() self._write_tagfile() self.db.replace_document(self.docid, self.xapian_doc) except: self._rm_docdir() raise def purge(self): """Purge document from database and root.""" # FIXME: catch db not writable errors try: self.db.delete_document(self.docid) except xapian.DocNotFoundError: pass self._rm_docdir() self.docid = None ######################################## # internal stuff # add an individual prefix'd term for the document def _add_term(self, prefix, value): term = '%s%s' % (prefix, value) self.xapian_doc.add_term(term) # remove an individual prefix'd term for the document def _remove_term(self, prefix, value): term = '%s%s' % (prefix, value) try: self.xapian_doc.remove_term(term) except xapian.InvalidArgumentError: pass # Parse 'text' and add a term to 'message' for each parsed # word. Each term will be added both prefixed (if prefix is not # None) and non-prefixed. # http://xapian.org/docs/bindings/python/ # http://xapian.org/docs/quickstart.html # http://www.flax.co.uk/blog/2009/04/02/xapian-search-architecture/ def _gen_terms(self, prefix, text): term_gen = self.db.term_gen term_gen.set_document(self.xapian_doc) if prefix: term_gen.index_text(text, 1, prefix) term_gen.index_text(text) # return a list of terms for prefix def _term_iter(self, prefix=None): term_iter = iter(self.xapian_doc) if prefix: plen = len(prefix) term = term_iter.skip_to(prefix) if not term.term.startswith(prefix): return yield term.term[plen:] for term in term_iter: if prefix: if not term.term.startswith(prefix): break yield term.term[plen:] else: yield term.term def term_iter(self, name=None): """Iterator over all terms in the document. If a prefix is provided, will iterate over only the prefixed terms, and the prefix will be removed from the returned terms. """ prefix = None if name: prefix = self.db._find_prefix(name) if not prefix: prefix = name return self._term_iter(prefix) # set the data object for the document def _set_data(self, text): self.xapian_doc.set_data(text) def get_data(self): """Get data object for document.""" return self.xapian_doc.get_data() ######################################## # files def add_file_data(self, name, data): """Add a file data to document. 'name' is the name of the file, 'data is the file data. File will not copied in to docdir until sync(). """ # FIXME: set mime type term # parse the file data into text text = parse_data(data) # generate terms from the text self._gen_terms(None, text) # set data to be text sample # FIXME: is this the right thing to put in the data? summary = text[0:997] + '...' self._set_data(summary) # FIXME: should files be renamed to something generic (0.pdf)? prefix = self.db._find_prefix('file') self._add_term(prefix, name) # add it to the cache to be written at sync() self._infiles[name] = data def add_file(self, infile): """Add a file to document. Added file will have the same name. File will not copied in to docdir until sync(). """ with open(infile, 'r') as f: data = f.read() name = os.path.basename(infile) self.add_file_data(name, data) def get_files(self): """Return files associated with document.""" return list(self.term_iter('file')) def get_fullpaths(self): """Return fullpaths of files associated with document.""" list = [] for path in self.get_files(): # FIXME: this is a hack for old path specifications that # included the docdir path = os.path.basename(path) list.append(os.path.join(self.docdir, path)) return list ######################################## # SOURCES def _purge_sources_prefix(self, source): # purge all terms for a given source prefix prefix = self.db._make_source_prefix(source) for i in self._term_iter(prefix): self._remove_term(prefix, i) self._remove_term(self.db._find_prefix('source'), source) def add_sid(self, sid): """Add source sid to document.""" source, oid = sid.split(':', 1) source = source.lower() # remove any existing terms for this source self._purge_sources_prefix(source) # add a term for the source self._add_term(self.db._find_prefix('source'), source) # add a term for the sid, with source as prefix self._add_term(self.db._make_source_prefix(source), oid) def get_sids(self): """Return a list of sids for document.""" sids = [] for source in self.term_iter('source'): for oid in self._term_iter(self.db._make_source_prefix(source)): sids.append('%s:%s' % (source, oid)) return sids # TAGS def add_tags(self, tags): """Add tags from list to document.""" prefix = self.db._find_prefix('tag') for tag in tags: self._add_term(prefix, tag) def get_tags(self): """Return a list of tags associated with document.""" return list(self.term_iter('tag')) def remove_tags(self, tags): """Remove tags from a document.""" prefix = self.db._find_prefix('tag') for tag in tags: self._remove_term(prefix, tag) # TITLE def _set_title(self, title): pt = self.db._find_prefix('title') for term in self._term_iter(pt): self._remove_term(pt, term) # FIXME: what's the clean way to get these prefixes? for term in self._term_iter('ZS'): self._remove_term('ZS', term) self._gen_terms(pt, title) # AUTHOR def _set_authors(self, authors): pa = self.db._find_prefix('author') for term in self._term_iter(pa): self._remove_term(pa, term) # FIXME: what's the clean way to get these prefixes? for term in self._term_iter('ZA'): self._remove_term('ZA', term) self._gen_terms(pa, authors) # YEAR def _set_year(self, year): # FIXME: what to do if year is not an int? try: year = int(year) except ValueError: pass prefix = self.db._find_prefix('year') for term in self._term_iter(prefix): self._remove_term(prefix, year) self._add_term(prefix, year) facet = self.db._find_facet('year') self.xapian_doc.add_value(facet, xapian.sortable_serialise(year)) ######################################## # bibtex def get_bibpath(self): """Return path to document bibtex file.""" return os.path.join(self.docdir, 'bibtex') def _set_bibkey(self, key): prefix = self.db._find_prefix('key') for term in self._term_iter(prefix): self._remove_term(prefix, term) self._add_term(prefix, key) def _index_bibentry(self, bibentry): authors = bibentry.get_authors() fields = bibentry.get_fields() if 'title' in fields: self._set_title(fields['title']) if 'year' in fields: self._set_year(fields['year']) if authors: # authors should be a list, so we make a single text string # FIXME: better way to do this? self._set_authors(' '.join(authors)) # add any sources in the bibtex for source in Sources().scan_bibentry(bibentry): self.add_sid(source.sid) # FIXME: index 'keywords' field as regular terms self._set_bibkey(bibentry.key) def add_bibentry(self, bibentry): """Add bibentry object.""" self.bibentry = bibentry self._index_bibentry(self.bibentry) def add_bibtex(self, bibtex): """Add bibtex to document, as string or file path.""" self.add_bibentry(Bibtex(bibtex)[0]) def _load_bib(self): if self.bibentry: return bibpath = self.get_bibpath() if os.path.exists(bibpath): self.bibentry = Bibtex(bibpath)[0] def get_bibtex(self): """Get the bib for document as a bibtex string.""" bibpath = self.get_bibpath() if os.path.exists(bibpath): with open(bibpath, 'r') as f: bibtex = f.read().decode('utf-8') return bibtex.strip() def get_bibdata(self): self._load_bib() if self.bibentry: data = self.bibentry.get_fields() data['authors'] = self.bibentry.get_authors() return data def update_from_bibtex(self): """Update document metadata from document bibtex.""" self._load_bib() self._index_bibentry(self.bibentry) ######################################## def get_key(self): self._load_bib() if not self.bibentry: return return self.bibentry.key def get_title(self): """Get the title from document bibtex.""" self._load_bib() if not self.bibentry: return fields = self.bibentry.get_fields() if 'title' in fields: return fields['title'] def get_year(self): """Get the title from document bibtex.""" self._load_bib() if not self.bibentry: return fields = self.bibentry.get_fields() if 'year' in fields: return fields['year'] def get_urls(self): """Get all URLs associated with document.""" sources = Sources() urls = [] # get urls associated with known sources for sid in self.get_sids(): urls.append(sources[sid].url) # get urls from bibtex self._load_bib() if self.bibentry: fields = self.bibentry.get_fields() if 'url' in fields: urls.append(fields['url']) if 'adsurl' in fields: urls.append(fields['adsurl']) return urls xapers-0.6/lib/xapers/nci/000077500000000000000000000000001245427370400155125ustar00rootroot00000000000000xapers-0.6/lib/xapers/nci/__init__.py000066400000000000000000000013111245427370400176170ustar00rootroot00000000000000""" This file is part of xapers. Xapers 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. Xapers 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 notmuch. If not, see . Copyright 2012 Jameson Rollins """ from ui import UI xapers-0.6/lib/xapers/nci/bibview.py000066400000000000000000000014141245427370400175130ustar00rootroot00000000000000import urwid from ..cli import initdb ############################################################ class Bibview(urwid.WidgetWrap): def __init__(self, ui, query): self.ui = ui self.ui.set_header([urwid.Text("bibtex: " + query)]) string = '' with initdb() as db: if db.count(query) == 0: self.ui.set_status('No documents found.') else: for doc in db.search(query, limit=20): bibtex = doc.get_bibtex() if bibtex: string = string + bibtex + '\n' self.box = urwid.Filler(urwid.Text(string)) w = self.box self.__super.__init__(w) def keypress(self, size, key): self.ui.keypress(key) xapers-0.6/lib/xapers/nci/defaults/000077500000000000000000000000001245427370400173215ustar00rootroot00000000000000xapers-0.6/lib/xapers/nci/defaults/bindings000066400000000000000000000004431245427370400210420ustar00rootroot00000000000000[global] ?: help s: promptSearch S: promptSearch A: promptAdd q: killBuffer Q: quit [search] n: nextEntry p: prevEntry] enter: viewFile u: viewURL b: viewBibtex +: addTags -: removeTags u: update a: archive meta i: copyID meta f: copyPath meta u: copyURL meta b: copyBibtex f: filterSearch xapers-0.6/lib/xapers/nci/help.py000066400000000000000000000025071245427370400170200ustar00rootroot00000000000000import urwid ############################################################ class Help(urwid.WidgetWrap): def __init__(self, ui, target=None): self.ui = ui self.target = target if self.target: tname = self.target.__class__.__name__ self.ui.set_header([urwid.Text("help: " + tname)]) else: self.ui.set_header([urwid.Text("help")]) pile = [] if self.target and hasattr(self.target, 'keys'): pile.append(urwid.Text('%s commands:' % (tname))) pile.append(urwid.Text('')) for key, cmd in self.target.keys.iteritems(): pile.append(self.row('target', cmd, key)) pile.append(urwid.Text('')) pile.append(urwid.Text('')) pile.append(urwid.Text('Global commands:')) pile.append(urwid.Text('')) for key, cmd in self.ui.keys.iteritems(): pile.append(self.row('ui', cmd, key)) w = urwid.Filler(urwid.Pile(pile)) self.__super.__init__(w) def row(self, c, cmd, key): hstring = eval('str(self.%s.%s.__doc__)' % (c, cmd)) return urwid.Columns([('fixed', 10, urwid.Text(key)), urwid.Text(hstring), ]) def keypress(self, size, key): self.ui.keypress(key) xapers-0.6/lib/xapers/nci/search.py000066400000000000000000000311741245427370400173370ustar00rootroot00000000000000import os import urwid import subprocess import collections from ..cli import initdb from ..database import DatabaseLockError ############################################################ def xclip(text, isfile=False): """Copy text or file contents into X clipboard.""" f = None if isfile: f = open(text, 'r') sin = f else: sin = subprocess.PIPE p = subprocess.Popen(["xclip", "-i"], stdin=sin) p.communicate(text) if f: f.close() ############################################################ class DocItem(urwid.WidgetWrap): FIELDS = ['title', 'authors', 'journal', 'year', 'source', #'tags', 'file', #'summary', ] def __init__(self, doc, doc_ind, total_docs): self.doc = doc self.docid = self.doc.docid c1width = 10 field_data = dict.fromkeys(self.FIELDS, '') field_data['tags'] = ' '.join(self.doc.get_tags()) bibdata = self.doc.get_bibdata() if bibdata: for field, value in bibdata.iteritems(): if 'title' == field: field_data[field] = value elif 'authors' == field: field_data[field] = ' and '.join(value[:4]) if len(value) > 4: field_data[field] += ' et al.' elif 'year' == field: field_data[field] = value if field_data['journal'] == '': if 'journal' == field: field_data['journal'] = value elif 'container-title' == field: field_data['journal'] = value elif 'arxiv' == field: field_data['journal'] = 'arXiv.org' elif 'dcc' == field: field_data['journal'] = 'LIGO DCC' urls = self.doc.get_urls() if urls: field_data['source'] = urls[0] summary = self.doc.get_data() if not summary: summary = 'NO FILE' field_data['summary'] = summary files = self.doc.get_files() if files: field_data['file'] = os.path.basename(files[0]) def gen_field_row(field, value): if field in ['journal', 'year', 'source']: color = 'journal' elif field in ['file']: color = 'field' else: color = field return urwid.Columns([ ('fixed', c1width, urwid.Text(('field', field + ':'))), urwid.Text((color, value)), ]) self.tag_field = urwid.Text(field_data['tags']) header = urwid.AttrMap(urwid.Columns([ ('fixed', c1width, urwid.Text('id:%d' % (self.docid))), urwid.AttrMap(self.tag_field, 'tags'), urwid.Text('%s%% match (%s/%s)' % (doc.matchp, doc_ind, total_docs), align='right'), ]), 'head') pile = [urwid.AttrMap(urwid.Divider(' '), '', ''), header ] + [gen_field_row(field, field_data[field]) for field in self.FIELDS] w = urwid.AttrMap(urwid.AttrMap(urwid.Pile(pile), 'field'), '', {'head': 'head focus', 'field': 'field focus', 'tags': 'tags focus', 'title': 'title focus', 'authors': 'authors focus', 'journal': 'journal focus', }, ) self.__super.__init__(w) def selectable(self): return True def keypress(self, size, key): return key ############################################################ class DocWalker(urwid.ListWalker): def __init__(self, docs): self.docs = docs self.ndocs = len(docs) self.focus = 0 self.items = {} def __getitem__(self, pos): if pos < 0: raise IndexError if pos not in self.items: self.items[pos] = DocItem(self.docs[pos], pos+1, self.ndocs) return self.items[pos] def set_focus(self, focus): if focus == -1: focus = self.ndocs - 1 self.focus = focus self._modified() def next_position(self, pos): return pos + 1 def prev_position(self, pos): return pos - 1 ############################################################ class Search(urwid.WidgetWrap): palette = [ ('head', 'dark blue, bold', ''), ('head focus', 'white, bold', 'dark blue'), ('field', 'light gray', ''), ('field focus', '', 'dark gray', '', '', 'g19'), ('tags', 'dark green', ''), ('tags focus', 'light green', 'dark blue'), ('title', 'yellow', ''), ('title focus', 'yellow', 'dark gray', '', 'yellow', 'g19'), ('authors', 'light cyan', ''), ('authors focus', 'light cyan', 'dark gray', '', 'light cyan', 'g19'), ('journal', 'dark magenta', '',), ('journal focus', 'dark magenta', 'dark gray', '', 'dark magenta', 'g19'), ] keys = collections.OrderedDict([ ('n', "nextEntry"), ('down', "nextEntry"), ('p', "prevEntry"), ('up', "prevEntry"), ('<', "firstEntry"), ('>', "lastEntry"), ('=', "refresh"), ('l', "filterSearch"), ('enter', "viewFile"), ('u', "viewURL"), ('b', "viewBibtex"), ('+', "addTags"), ('-', "removeTags"), ('a', "archive"), ('meta i', "copyID"), ('meta f', "copyPath"), ('meta u', "copyURL"), ('meta b', "copyBibtex"), ]) def __init__(self, ui, query=None): self.ui = ui self.query = query count = self.ui.db.count(query) if count == 0: self.ui.set_status('No documents found.') docs = [] else: docs = [doc for doc in self.ui.db.search(query)] if count == 1: cstring = "%d result" % (count) else: cstring = "%d results" % (count) self.ui.set_header([urwid.Columns([ urwid.Text("search: \"%s\"" % (self.query)), urwid.Text(cstring, align='right'), ])]) self.lenitems = count self.docwalker = DocWalker(docs) self.listbox = urwid.ListBox(self.docwalker) w = self.listbox self.__super.__init__(w) def keypress(self, size, key): if key in self.keys: cmd = "self.%s()" % (self.keys[key]) eval(cmd) else: self.ui.keypress(key) ########## def refresh(self): """refresh current search results""" entry, pos = self.listbox.get_focus() self.ui.newbuffer(['search', self.query]) self.ui.killBuffer() def filterSearch(self): """filter current search with additional terms""" prompt = 'filter search: ' urwid.connect_signal(self.ui.prompt(prompt), 'done', self._filterSearch_done) def _filterSearch_done(self, newquery): self.ui.view.set_focus('body') urwid.disconnect_signal(self.ui, self.ui.prompt, 'done', self._filterSearch_done) if not newquery: self.ui.set_status() return self.ui.newbuffer(['search', self.query, newquery]) def nextEntry(self): """next entry""" entry, pos = self.listbox.get_focus() if not entry: return if pos + 1 >= self.lenitems: return self.listbox.set_focus(pos + 1) def prevEntry(self): """previous entry""" entry, pos = self.listbox.get_focus() if not entry: return if pos == 0: return self.listbox.set_focus(pos - 1) def lastEntry(self): """last entry""" self.listbox.set_focus(-1) def firstEntry(self): """first entry""" self.listbox.set_focus(0) def viewFile(self): """open document file""" entry = self.listbox.get_focus()[0] if not entry: return path = entry.doc.get_fullpaths() if not path: self.ui.set_status('No file for document id:%d.' % entry.docid) return path = path[0] if not os.path.exists(path): self.ui.set_status('ERROR: id:%d: file not found.' % entry.docid) return self.ui.set_status('opening file: %s...' % path) subprocess.Popen(['xdg-open', path], stdin=self.ui.devnull, stdout=self.ui.devnull, stderr=self.ui.devnull) def viewURL(self): """open document URL in browser""" entry = self.listbox.get_focus()[0] if not entry: return urls = entry.doc.get_urls() if not urls: self.ui.set_status('ERROR: id:%d: no URLs found.' % entry.docid) return # FIXME: open all instead of just first? url = urls[0] self.ui.set_status('opening url: %s...' % url) subprocess.call(['xdg-open', url], stdin=self.ui.devnull, stdout=self.ui.devnull, stderr=self.ui.devnull) def viewBibtex(self): """view document bibtex""" entry = self.listbox.get_focus()[0] if not entry: return self.ui.newbuffer(['bibview', 'id:' + str(entry.docid)]) def copyID(self): """copy document ID to clipboard""" entry = self.listbox.get_focus()[0] if not entry: return docid = "id:%d" % entry.docid xclip(docid) self.ui.set_status('yanked docid: %s' % docid) def copyPath(self): """copy document file path to clipboard""" entry = self.listbox.get_focus()[0] if not entry: return path = entry.doc.get_fullpaths()[0] if not path: self.ui.set_status('ERROR: id:%d: file path not found.' % entry.docid) return xclip(path) self.ui.set_status('yanked path: %s' % path) def copyURL(self): """copy document URL to clipboard""" entry = self.listbox.get_focus()[0] if not entry: return urls = entry.doc.get_urls() if not urls: self.ui.set_status('ERROR: id:%d: URL not found.' % entry.docid) return # FIXME: copy all instead of just first? url = urls[0] xclip(url) self.ui.set_status('yanked url: %s' % url) def copyBibtex(self): """copy document bibtex to clipboard""" entry = self.listbox.get_focus()[0] if not entry: return bibtex = entry.doc.get_bibpath() if not bibtex: self.ui.set_status('ERROR: id:%d: bibtex not found.' % entry.docid) return xclip(bibtex, isfile=True) self.ui.set_status('yanked bibtex: %s' % bibtex) def addTags(self): """add tags to document (space separated)""" self.promptTag('+') def removeTags(self): """remove tags from document (space separated)""" self.promptTag('-') def promptTag(self, sign): entry = self.listbox.get_focus()[0] if not entry: return if sign is '+': # FIXME: autocomplete to existing tags prompt = 'add tags: ' elif sign is '-': # FIXME: autocomplete to doc tags only prompt = 'remove tags: ' urwid.connect_signal(self.ui.prompt(prompt), 'done', self._promptTag_done, sign) def _promptTag_done(self, tag_string, sign): self.ui.view.set_focus('body') urwid.disconnect_signal(self, self.ui.prompt, 'done', self._promptTag_done) if not tag_string: self.ui.set_status('No tags set.') return entry = self.listbox.get_focus()[0] try: with initdb(writable=True) as db: doc = db[entry.docid] tags = tag_string.split() if sign is '+': doc.add_tags(tags) msg = "Added tags: %s" % (tag_string) elif sign is '-': doc.remove_tags(tags) msg = "Removed tags: %s" % (tag_string) doc.sync() tags = doc.get_tags() entry.tag_field.set_text(' '.join(tags)) except DatabaseLockError as e: msg = e.msg self.ui.db.reopen() self.ui.set_status(msg) def archive(self): """archive document (remove 'new' tag) and advance""" self._promptTag_done('new', '-') self.nextEntry() xapers-0.6/lib/xapers/nci/ui.py000066400000000000000000000111561245427370400165050ustar00rootroot00000000000000""" This file is part of xapers. Xapers 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. Xapers 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 notmuch. If not, see . Copyright 2012, 2013 Jameson Rollins """ import os import sys import urwid import collections from ..cli import initdb from search import Search from bibview import Bibview from help import Help ############################################################ class UI(): palette = [ ('header', 'white', 'dark blue'), ('footer', 'white', 'dark blue'), ('prompt', 'black', 'light green'), ] keys = collections.OrderedDict([ ('s', "promptSearch"), ('q', "killBuffer"), ('Q', "quit"), ('?', "help"), ]) def __init__(self, cmd=None): self.db = initdb() self.header_string = "Xapers" self.status_string = "s: search, q: kill buffer, Q: quit Xapers, ?: help and additional commands" self.view = urwid.Frame(urwid.SolidFill()) self.set_header() self.set_status() self.devnull = open('/dev/null', 'rw') if not cmd: cmd = ['search', '*'] if cmd[0] == 'search': query = ' '.join(cmd[1:]) self.buffer = Search(self, query) elif cmd[0] == 'bibview': query = ' '.join(cmd[1:]) self.buffer = Bibview(self, query) elif cmd[0] == 'help': target = None if len(cmd) > 1: target = cmd[1] if isinstance(target, str): target = None self.buffer = Help(self, target) else: self.buffer = Help(self) self.set_status("Unknown command '%s'." % (cmd[0])) self.merge_palette(self.buffer) self.view.body = urwid.AttrMap(self.buffer, 'body') self.mainloop = urwid.MainLoop( self.view, self.palette, unhandled_input=self.keypress, handle_mouse=False, ) self.mainloop.screen.set_terminal_properties(colors=88) self.mainloop.run() ########## def merge_palette(self, buffer): if hasattr(buffer, 'palette'): self.palette = list(set(self.palette) | set(buffer.palette)) def set_header(self, widget=[]): header = urwid.Columns([('pack', urwid.Text('Xapers '))] + widget) self.view.set_header(urwid.AttrMap(header, 'header')) def set_status(self, text=None): if text: self.status_string = '%s' % (text) self.view.set_footer(urwid.AttrMap(urwid.Text(self.status_string), 'footer')) def newbuffer(self, cmd): UI(cmd=cmd) self.set_status() def prompt(self, string): prompt = PromptEdit(string) self.view.set_footer(urwid.AttrMap(prompt, 'prompt')) self.view.set_focus('footer') return prompt ########## def promptSearch(self): """search database""" prompt = 'search: ' urwid.connect_signal(self.prompt(prompt), 'done', self._promptSearch_done) def _promptSearch_done(self, query): self.view.set_focus('body') urwid.disconnect_signal(self, self.prompt, 'done', self._promptSearch_done) if not query: self.set_status() return self.newbuffer(['search', query]) def killBuffer(self): """kill current buffer (quit if last buffer)""" raise urwid.ExitMainLoop() def quit(self): """quit Xapers""" sys.exit() def help(self): """help""" self.newbuffer(['help', self.buffer]) def keypress(self, key): if key in self.keys: cmd = "self.%s()" % (self.keys[key]) eval(cmd) ############################################################ class PromptEdit(urwid.Edit): __metaclass__ = urwid.signals.MetaSignals signals = ['done'] def keypress(self, size, key): if key == 'enter': urwid.emit_signal(self, 'done', self.get_edit_text()) return elif key == 'esc': urwid.emit_signal(self, 'done', None) return urwid.Edit.keypress(self, size, key) xapers-0.6/lib/xapers/parser.py000066400000000000000000000026051245427370400166120ustar00rootroot00000000000000import os ################################################## class ParseError(Exception): """Base class for Xapers parser exceptions.""" def __init__(self, msg): self.msg = msg def __str__(self): return self.msg ################################################## class ParserBase(): """Base class for Xapers document parsering.""" def __init__(self, path): self.path = os.path.expanduser(path) def extract(self): pass ################################################## def parse_data(data): # FIXME: determine mime type mimetype = 'pdf' from xapers.parsers.pdf import extract try: text = extract(data) except Exception, e: raise ParseError("Could not parse file: %s" % e) return text def parse_file(path): # FIXME: determine mime type mimetype = 'pdf' try: mod = __import__('xapers.parsers.' + mimetype, fromlist=['Parser']) pmod = getattr(mod, 'Parser') except ImportError: raise ParseError("Unknown parser '%s'." % mimetype) if not os.path.exists(path): raise ParseError("File '%s' not found." % path) if not os.path.isfile(path): raise ParseError("File '%s' is not a regular file." % path) try: text = pmod(path).extract() except Exception, e: raise ParseError("Could not parse file: %s" % e) return text xapers-0.6/lib/xapers/parsers/000077500000000000000000000000001245427370400164205ustar00rootroot00000000000000xapers-0.6/lib/xapers/parsers/__init__.py000066400000000000000000000000001245427370400205170ustar00rootroot00000000000000xapers-0.6/lib/xapers/parsers/pdf.py000066400000000000000000000011351245427370400175430ustar00rootroot00000000000000from ..parser import ParserBase import subprocess def extract(data): cmd = ['pdftotext', '-', '-'] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=open('/dev/null','w'), ) (stdout, stderr) = proc.communicate(input=data) proc.wait() return stdout class Parser(ParserBase): def extract(self): cmd = ['pdftotext', self.path, '-'] text = subprocess.check_output(cmd, stderr=open('/dev/null','w')) return text xapers-0.6/lib/xapers/source.py000066400000000000000000000160151245427370400166160ustar00rootroot00000000000000import os import re import pkgutil from urlparse import urlparse import sources from parser import parse_file ################################################## class SourceError(Exception): pass class SourceAttributeError(SourceError): def __init__(self, source, msg): self.source = source self.msg = msg def __str__(self): return "Source '%s' does not include a %s." % (self.source.name, self.msg) ################################################## class Source(object): """Xapers class representing an online document source. The Source object is build from a source nickname (name) and possibly user-defined source module. """ def __init__(self, name, module): self.name = name self.module = module def __repr__(self): return '%s(%s, %s)' % (self.__class__, self.name, self.module) def __str__(self): return self.name def __getitem__(self, id): return SourceItem(self, id) @property def path(self): return self.module.__file__ @property def is_builtin(self): bpath = os.path.dirname(sources.__file__) spath = os.path.dirname(self.path) return os.path.commonprefix([bpath, spath]) == bpath @property def description(self): try: return self.module.description except AttributeError: raise SourceAttributeError(self, "'description' property") @property def url(self): try: return self.module.url except AttributeError: raise SourceAttributeError(self, "'url' property") @property def url_regex(self): try: return self.module.url_regex except AttributeError: raise SourceAttributeError(self, "'url_regex' property") @property def scan_regex(self): try: return self.module.scan_regex except AttributeError: raise SourceAttributeError(self, "'scan_regex' property") def fetch_bibtex(self, id): try: return self.module.fetch_bibtex(id) except AttributeError: raise SourceAttributeError(self, "fetch_bibtex() function") def fetch_file(self, id): try: return self.module.fetch_file(id) except AttributeError: raise SourceAttributeError(self, "fetch_file() function") class SourceItem(Source): """Xapers class representing an item from an online source. """ def __init__(self, source, id): super(SourceItem, self).__init__(source.name, source.module) self.id = id self.sid = '%s:%s' % (self.name, self.id) def __repr__(self): s = super(SourceItem, self).__repr__() return '%s(%s, %s)' % (self.__class__, s, self.id) def __hash__(self): return hash(self.sid) def __eq__(self, other): if isinstance(other, self.__class__): return self.sid == other.sid return NotImplemented def __ne__(self, other): return not self.__eq__(other) def __str__(self): return self.sid @property def url(self): try: return self.module.url_format % self.id except AttributeError: raise SourceAttributeError(self, "'url_format' property") def fetch_bibtex(self): return super(SourceItem, self).fetch_bibtex(self.id) def fetch_file(self): return super(SourceItem, self).fetch_file(self.id) ################################################## class Sources(object): def __init__(self): self.sourcespath = sources.__path__ extra = os.getenv('XAPERS_SOURCE_PATH', None) if extra: for path in extra.split(':'): if path: sourcespath.insert(0, path) else: self.sourcespath.insert(0, os.path.expanduser(os.path.join('~','.xapers','sources'))) self._sources = {} for (loader, name, ispkg) in pkgutil.walk_packages(self.sourcespath): if ispkg: continue #self._modules[name] = loader.find_module(name).load_module(name) module = loader.find_module(name).load_module(name) self._sources[name] = Source(name, module) def __repr__(self): return '%s(%s)' % (self.__class__, self.sourcespath) def get_source(self, name, id=None): try: source = self._sources[name] except KeyError: raise SourceError("unknown source: %s" % name) if id: return source[id] else: return source def __contains__(self, source): return source in self._sources def __getitem__(self, sid): name = None id = None try: vals = sid.split(':') except ValueError: raise SourceError("could not parse sid string") name = vals[0] if len(vals) > 1: id = vals[1] return self.get_source(name, id) def __iter__(self): return self._sources.itervalues() def match_source(self, string): """Return Source object from URL or source identifier string. """ o = urlparse(string) # if the scheme is http, look for source match if o.scheme in ['http', 'https']: for source in self: try: match = re.match(source.url_regex, string) except SourceAttributeError: # FIXME: warning? continue if match: return source[match.group(1)] elif o.scheme != '' and o.path != '': return self.get_source(o.scheme, o.path) raise SourceError('String matches no known source.') def scan_file(self, file): """Scan document file for source identifiers Source 'scan_regex' attributes are used. Returns a list of SourceItem objects. """ text = parse_file(file) items = set() for source in self: try: regex = re.compile(source.scan_regex) except SourceAttributeError: # FIXME: warning? continue matches = regex.findall(text) if not matches: continue for match in matches: items.add(source[match]) return list(items) def scan_bibentry(self, bibentry): """Scan bibentry for source identifiers. Bibentry keys are searched for source names, and bibentry values are assumed to be individual identifier strings. Returns a list of SourceItem objects. """ fields = bibentry.get_fields() items = set() for field, value in fields.iteritems(): field = field.lower() if field in self: items.add(self.get_source(field, value)) # FIXME: how do we get around special exception for this? if 'eprint' in fields: items.add(self.get_source('arxiv', fields['eprint'])) return list(items) xapers-0.6/lib/xapers/sources/000077500000000000000000000000001245427370400164245ustar00rootroot00000000000000xapers-0.6/lib/xapers/sources/__init__.py000066400000000000000000000000001245427370400205230ustar00rootroot00000000000000xapers-0.6/lib/xapers/sources/arxiv.py000066400000000000000000000041261245427370400201320ustar00rootroot00000000000000import urllib from HTMLParser import HTMLParser from xapers.bibtex import data2bib description = "Open access e-print service" url = 'http://arxiv.org/' url_format = 'http://arxiv.org/abs/%s' url_regex = 'http://arxiv.org/(?:abs|pdf|format)/([^/]*)' # html parser override to override handler methods class MyHTMLParser(HTMLParser): def __init__(self): HTMLParser.__init__(self) self.lefthead = False self.title = None self.author = [] self.year = None self.sid = None def handle_starttag(self, tag, attrs): title = False author = False date = False sid = False if self.lefthead: return if tag != 'meta': return for attr in attrs: if attr[0] == 'name': if attr[1] == 'citation_title': title = True if attr[1] == 'citation_author': author = True if attr[1] == 'citation_date': date = True if attr[1] == 'citation_arxiv_id': sid = True if attr[0] == 'content': if title: self.title = attr[1] if author: self.author.append(attr[1]) if date: self.year = attr[1].split('/')[0] if sid: self.sid = attr[1] def handle_endtag(self, tag): if tag == 'head': self.lefthead = True def fetch_bibtex(id): url = url_format % id f = urllib.urlopen(url) html = f.read() f.close() parser = MyHTMLParser() parser.feed(html) data = { 'arxiv': id, 'title': parser.title, 'authors': parser.author, 'year': parser.year, 'eprint': id, 'url': url_format % id, } return data2bib(data, 'arxiv:%s' % id) def fetch_file(id): url = 'http://arxiv.org/pdf/%s' % id f = urllib.urlopen(url) data = f.read() f.close() name = '%s.pdf' % id return name, data xapers-0.6/lib/xapers/sources/cryptoeprint.py000066400000000000000000000025001245427370400215350ustar00rootroot00000000000000import urllib from HTMLParser import HTMLParser description = "Cryptology ePrint Archive" url = "https://eprint.iacr.org/" url_format = 'https://eprint.iacr.org/%s' url_regex = 'https?://eprint.iacr.org/(\d{4,}/\d{3,})' # don't know what a scan_regex looks like for IACR eprints. i don't # think there is one, because i think the submission process happens # after the pdf is formalized. # custom definitions for IACR eprints: bibtex_url = 'https://eprint.iacr.org/eprint-bin/cite.pl?entry=%s' pdf_url = 'https://eprint.iacr.org/%s.pdf' # html parser override to override handler methods class IACRParser(HTMLParser): def __init__(self): HTMLParser.__init__(self) self.pre = False self.data = None def handle_starttag(self, tag, attrs): if (tag == 'pre'): self.pre = True def handle_endtag(self, tag): if (tag == 'pre'): self.pre = False def handle_data(self, data): if (self.pre): self.data = data def fetch_bibtex(id): url = bibtex_url % id f = urllib.urlopen(url) html = f.read() f.close() p = IACRParser() p.feed(html) return unicode(p.data) def fetch_file(id): url = pdf_url % id f = urllib.urlopen(url) pdf = f.read() f.close() return (id.split('/').pop() + '.pdf', pdf) xapers-0.6/lib/xapers/sources/dcc.py000066400000000000000000000046041245427370400175330ustar00rootroot00000000000000import sys import pycurl import cStringIO import tempfile from xapers.bibtex import data2bib description = "LIGO Document Control Center" url = 'https://dcc.ligo.org/' url_format = 'https://dcc.ligo.org/%s' url_regex = 'https://dcc.ligo.org/(?:LIGO-)?([^/]*)' def dccRetrieveXML(docid): url = 'https://dcc.ligo.org/Shibboleth.sso/Login?target=https%3A%2F%2Fdcc.ligo.org%2Fcgi-bin%2Fprivate%2FDocDB%2FShowDocument?docid=' + docid + '%26outformat=xml&entityID=https%3A%2F%2Flogin.ligo.org%2Fidp%2Fshibboleth' curl = pycurl.Curl() cookies = tempfile.NamedTemporaryFile() curl.setopt(pycurl.URL, url) curl.setopt(pycurl.UNRESTRICTED_AUTH, 1) curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_GSSNEGOTIATE) curl.setopt(pycurl.COOKIEJAR, cookies.name) curl.setopt(pycurl.USERPWD, ':') curl.setopt(pycurl.FOLLOWLOCATION, 1) doc = cStringIO.StringIO() curl.setopt(pycurl.WRITEFUNCTION, doc.write) try: curl.perform() except: import traceback traceback.print_exc(file=sys.stderr) sys.stderr.flush() xml = doc.getvalue() curl.close() cookies.close() doc.close() return xml def dccXMLExtract(xmlstring): from xml.dom.minidom import parse, parseString xml = parseString(xmlstring) etitle = xml.getElementsByTagName("title")[0].firstChild if etitle: title = etitle.data else: title = None alist = xml.getElementsByTagName("author") authors = [] for author in alist: authors.append(author.getElementsByTagName("fullname")[0].firstChild.data) eabstract = xml.getElementsByTagName("abstract")[0].firstChild if eabstract: abstract = eabstract.data else: abstract = None # FIXME: find year/date year = None return title, authors, year, abstract def fetch_bibtex(id): xml = dccRetrieveXML(id) try: title, authors, year, abstract = dccXMLExtract(xml) except: print >>sys.stderr, xml raise data = { 'institution': 'LIGO Laboratory', 'number': id, 'dcc': id, 'url': url_format % id } if title: data['title'] = title if authors: data['authors'] = authors if abstract: data['abstract'] = abstract if year: data['year'] = year key = 'dcc:%s' % id btype = '@techreport' return data2bib(data, key, type=btype) xapers-0.6/lib/xapers/sources/doi.py000066400000000000000000000023171245427370400175540ustar00rootroot00000000000000import urllib2 description = "Digital Object Identifier" url = 'https://dx.doi.org/' # produces URL string when supplied with valid source identifier url_format = 'https://dx.doi.org/%s' id_regex = '(10\.\d{4,}[\w\d\:\.\-\/]+)' # for regex matching a supplied URL. match group 1 should return the # source identifier string url_regex = 'https?://dx.doi.org/(10\.\d{4,}[\w\d\:\.\-\/]+)' # for regex scanning of document text #scan_regex = '[doi|DOI][\s\.\:]{0,2}(10\.\d{4}[\d\:\.\-\/a-z]+)[A-Z\s]' #scan_regex = '\b(10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?!["&\'<>])[[:graph:]])+)\b' #scan_regex = '(doi|DOI)(10[.][0-9]{4,}(?:[.][0-9]+)*[\/\.](?:(?!["&\'<>])[[:graph:]])+)' #scan_regex = '(?:doi|DOI)[\s\.\:]{0,2}(10\.\d{4,}[\w\d\:\.\-\/]+)' scan_regex = '(?:doi|DOI)[\s\.\:]{0,2}' + id_regex # function to fetch a bibtex entry for a given source identifier def fetch_bibtex(id): # http://www.crosscite.org/cn/ url = url_format % id req = urllib2.Request(url) req.add_header('Accept', 'application/x-bibtex') req.add_header('Accept-Charset', 'utf-8') f = urllib2.urlopen(req) # DECODE the returned byte string to get a unicode string bibtex = f.read().decode('utf-8') f.close return bibtex xapers-0.6/lib/xapers/version.py000066400000000000000000000000241245427370400167740ustar00rootroot00000000000000__version__ = '0.6' xapers-0.6/man/000077500000000000000000000000001245427370400134445ustar00rootroot00000000000000xapers-0.6/man/man1/000077500000000000000000000000001245427370400143005ustar00rootroot00000000000000xapers-0.6/man/man1/xapers-adder.1000066400000000000000000000025421245427370400167440ustar00rootroot00000000000000.\" xapers - journal article indexing system .\" .\" Copyright 漏 2013 Jameson Rollins .\" .\" Xapers 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. .\" .\" Xapers 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/ . .\" .\" Author: Jameson Rollins .TH XAPERS 1 .SH NAME xapers-adder \- "gui" to import individual documents into Xapers database .SH SYNOPSIS .B xapers-adder .IR file.pdf .SH DESCRIPTION The specified PDF file is displayed (using \fBxdg-open\fR(1)), then a terminal is opened (\fBx-terminal-emulator\fR(1)) executing the following command: xapers add \-\-file= \-\-tags=new \-\-prompt \-\-view This program is useful to use as your PDF handler in your browser. See \fBxapers\fR(1) for more information. .SH CONTACT Feel free to email the author: Jameson Rollins xapers-0.6/man/man1/xapers.1000066400000000000000000000215711245427370400156720ustar00rootroot00000000000000.\" xapers - journal article indexing system .\" .\" Copyright 漏 2013 Jameson Rollins .\" .\" Xapers 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. .\" .\" Xapers 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/ . .\" .\" Author: Jameson Rollins .TH XAPERS 1 .SH NAME xapers \- personal journal article indexing system . .SH SYNOPSIS .B xapers .IR command " [" args " ...]" . .SH DESCRIPTION Xapers is a personal document indexing system, geared towards academic journal articles. It provides fast search of document text and bibliographic data (synced from online libraries) and simple document and bibtex retrieval. Xapers takes as input document files (as PDF) and source identifiers. Documents are copied into a local document store (~/.xapers/docs by default) and text is extracted from the PDF and fully indexed into a Xapian database. Source identifiers are used to download document bibliographic data from online digital libraries (see \fBSOURCES\fR below), which are then parsed and indexed to prefixed terms in the database. The bibliographic data is also stored as bibtex in the document store for easy retrieval. Documents can be arbitrarily tagged. A curses UI is provided for simple access to documents (see the \fBview\fR command below). Xapers is ultimately a document indexing library, though, so development of alternate user interfaces is encouraged. Underlying Xapers is the wonderful Xapian database/search engine. See http://xapian.org/ for more information. . .SH MAIN COMMANDS The following are the main xapers commands. See \fBSEARCH TERMS\fR below for details of the supported syntax for . . .SS add [options] [] Add a document, or update an existing document. Must specify at least one of \-\-file or \-\-source. If search terms are provided they must match exactly one document and the matching document is updated with the newly provided information. Available options: .RS 4 .TP 4 .BR \-\-source=[|] Source identifier for document. See \fBSOURCES\fR below. This may also be a path to a file that contains a single bibtex entry. .RE .RS 4 .TP 4 .BR \-\-file[=] Document file (as PDF) to add. Text of document will be extracted and indexed. A copy of the file will be placed in the Xapers document store. If provided without path, xapers will attempt to download file from source, assuming source supports file downloads. .RE .RS 4 .TP 4 .BR \-\-tags=[,...] Initial tags to apply to document. Multiple tags can be specified, comma separated. .RE .RS 4 .TP 4 .BR \-\-prompt Prompt user for source/file/tags, if not specified. When prompting for source information input files are automatically scanned for source IDs and found ids are displayed. .RE .RS 4 .TP 4 .BR \-\-view View resulting entry in curses UI when done. See the \fBviewP\fR command below for more info. .RE . .SS import [options] Import an existing bibtex database. Each bibtex entry will be added as a new document. If bibtex key, or any sources found in bibtex, match an existing document, that document is instead updated (this makes the command effectively idempotent). Any "file" fields will be parsed for document files to add. Files can be specified as a single path, or in Mendeley/Jabref format. Available options: .RS 4 .TP 4 .BR \-\-tags=[,...] Tags to apply to all imported documents. Multiple tags can be specified, comma separated. .RE . .SS tag +|- [...] [--] Add/remove tags from documents. '--' can be used to separate tagging operations from search terms. . .SS search [options] Search for documents in the database. Document information is printed to stdout. .RS 4 .TP 4 .BR \-\-output=[summary|bibtex|tags|sources|keys|files] Specify document information to be output: .B summary outputs a single-line summary of the documents (default). .B bibtex outputs bibtex for all documents (if available). .B tags outputs all tags associated with documents. .B sources outputs all sources associated with documents. .B keys outputs all bibtex citation keys associated with documents. .B files outputs the full paths to all files associated with documents. Default is .B summary. .RE .RS 4 .TP 4 .BR \-\-limit=N Limit number of results returned to N. .RE . .SS bibtex Short for "search \-\-output=bibtex ". . .SS count Return a simple count of search results. . .SS view [] .SS show [] View search results in curses search UI. Documents matching search are displayed with their bibliographic information and a short text summary. It allows for manipulating document tags and for retrieved for document files and source URLs for viewing (see .B xdg-open(1) for more info). Initial search terms can be provided, but further searches can be performed from within the UI. While in the UI type "?" for available commands. NOTE: At the moment only the top 20 search results are displayed, due to synchronous loading restrictions. This obviously needs to be fixed. . .SS export Copy PDF files of resulting documents into , named with document titles when available. . .SS delete Delete documents from the database. All document files will purged from the document store. .RS 4 .TP 4 .BR \-\-noprompt Do not prompt to confirm deletion of documents. .RE . .SS restore Restore a database from existing xapers root. . .SH SOURCE COMMANDS These commands provide access to some of the source module methods. See \fBSOURCES\fR below. . .SS sources List available sources. . .SS source2url [...] Parse a source identifier string and print the corresponding source URL. . .SS source2bib [...] Retrieve bibtex from source for a specified URL or source id, and write to stdout. . .SS source2file Retrieve file from source for a specified URL or source id, and write to stdout. . .SS scandoc Scan a document file (PDF) for source IDs, and print and recognized source ids to stdout. . .SH SOURCES Sources are online databases from which document bibliographic data can be retrieved. In Xapers, online libraries are assigned unique prefixes. The online libraries associate unique document identifiers to individual documents. See 'xapers sources' for a list of available online sources. Xapers recognizes document a source identifier, or \fBsid\fR, in two forms: full URL http://dx.doi.org/10.1364/JOSAA.29.002092 sid of form : doi:10.1364/JOSAA.29.002092 URLs are parsed into sources and source ids when recognized, and this information is used to retrieve bibtex from the online library databases. The sources and sids for a given document are stored as prefixed terms in the Xapers database (see below). . .SH SEARCH TERMS Xapers supports a common syntax for search terms. Search can consist of free-form text and quoted phrases. Terms can be combined with standard Boolean operators. All terms are combined with a logical OR by default. Parentheses can be used to group operators, but must be protect from shell interpretation. The string '*' will match all documents in the database. Additionally, the following prefixed terms are understood (where indicate user-supplied values): id: Xapers document ID author: string in authors (also a:) title: string in title (also t:) tag: specific user tag : specific source id (sid) source: specific source key: specific bibtex citation key year: specific publication year (also y:) year:.. publication year range (also y:) year:.. year:.. Publication years must be four-digit integers. See the following for more information on search terms: http://xapian.org/docs/queryparser.html . .SH ENVIRONMENT The following environment variables can be used to control the behavior of xapers: . .SS XAPERS_ROOT Location of the Xapers document store. Defaults to "~/.xapers/docs" if not specified. . .SS XAPERS_SOURCE_PATH Path specification for location of additional custom Xapers source modules. Defaults to "~/.xapers/sources" if not specified. . .SH CONTACT Feel free to email the author: Jameson Rollins xapers-0.6/screenshot.png000066400000000000000000000643471245427370400155720ustar00rootroot00000000000000塒NG  IHDR_9'sBIT坩O IDATx^頋崓$森ngy>瑎欢葊k3@鹅$謳y堀枹H#22楚4,>灛╀DuV @ @ 丩鄘69睡* @烡i/熢紷 $鹱&懻镞O~&-桍z报&|喷-! 仯t (熶貙羈廣Y飨c;(佽想"A @鵰礰J6i<`淙桠ㄓM囍烏隽鐍鉲y膵? 瑼 漶,~倈Ah;衰\?闖o支G搬砇K|B鎙Y裞导f逰)p L挓畜碸婺@<伵If4kI*?掬塘!OV0縢yZC 踭e)k|8咫f閅曶?'=购inBE-邙@闶h2雪#O2J脾屜19sㄒ<黶轺蓀瞱h 嫲湩獲D<涨5鷩%詒-}rfs5縣鬏瓌M#+兖 @囿i涣r袙巽澍Q蝂輂)6+ |鈖h匆fP蝜訴|俭 n藱澀PZ4媹稴鍙 @(&l8 0视釓?L骃/ K霙o峦-麑,&Lc柞xmノ' @&佈敘4l8殓峳u%唭埠mT祿鯂鬻繒襇囘就抆倜讄N5 @xs%7?伻 @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ <柪弌咪树厷,{庫捯7兮輗]窣 丠 'S9d蠘沲3[TK疐洭tU屃釭瘶/鐨磆蒶瀛喭呝"伭x褸 饀 4b聕"鯼埒$U皾N訂盝c罱侄2蕫罪韒`5沟, @ 邢潒儞v愃-w跺8;魸鴨3&蟠QW鉅遊  @84 菃?G幟 $幔谚P3^s灳4瘴5G* Av(嘊E摚.5` @崋栿朽e3鐀hQ 毸镵蟌塾ahC眄:G嬧@2丆肔樒r膘蘘塾盐碡K(a-}0B 並囟Y轟3K榐γLV鷭1-=6Щ}& 3 鯇佴ワ膋昪+E珮}F蚡 @Sy e(g8$箊^曅<狨砈璁|'玪 雇l櫌 @嘌dh筱軲S&瘠 @ @ @ 佺樲暡#敕邁摪焴鷌'鴬糓}衆^U嫰,鞭ī%堳檚3厎kM飃4^埘J舧豉lJ缁3乜鸚宣3*熃鏪旨楃h訨庄愑k窩蟆韉I遭+↑8C-L>袴缂蠭0f'螀"政m+'C^燠^§![Vr>*h藮h曼j=GSc)悒q*r淈Q%埝3_昰避嗃灺GFm4%!琩揠t%s龉Js吸朑瀷)1;Y9^~p楲噀嘩 y,2e任:2f麠(XiB鹷R刬痳錩鑅>嘍 g{-巫<肾^o峻褎熮l" >雵Hs y捏猋S圭膞礲r旟鴔J膭楠礃T咢k3h+玔現)-f+荳褦闽r銖每箴挙烐S渟靭f煭' jsrM81鲿-#Iewη*Yp&垛g╞-a-緫,sN榢Q灎' 潌攙揮榇夾犌9CEyO飺#K9叙jG欿+=獣$がn篝QY屺z祾 衛荆\ >暲_覺9e^籯彅亟T缗宒軪朂'~洎鯮侃-2隘U詹Te,M攜>} 喿h酤膍 谥5+櫍|+TJ媦>禿-岠OF丼屉欳閫K蟥褵羆)陀剃P/k,T$9*瓺M9g&F|M圇 傚嫌W26寇溕鉥幌渖RW2涮j莎>[缔镠∝oEyU+>e鲣J揊哝:稰tC:燑L豮y/蟐詾伦*螻>速2ma1充逛9粘畎鱈x.蛀%皕WJ郭鮕. 珬~1{禠5敊C詩弰4-胋靆〖!{q<-T 濟顋2諻 y51觍陈!卐}ike叮?榧组散脶3熷l-OJ搚擥Oq硓r闻+h8墤p\B牷+E 走M2醣A跋s%}审椛?悟2敹YD睐簺訆gY輰yUB,彥j2浫衊(574K>O.*{/e4侢紉灈7-弢翴\g5[縭覛%G錱Bh9?C蛸Y裱覗軖xBX鮍就Ο@c]a@4邈5[锰ъt%sh∩帑绬?;AOY+枭H-阧2UYN嶅骿罿E遛艘?H5=诨鵏艌縊梵勱<$馥餮逛曒_ ぷl鵫4@ 鋂0[@ @ @ ;佺6【i颎搠其v鏍饚S辢沄/c-昙2+$閩F輞l\d4旼!菋_{Q嬌C誷]ozO]}烘嶇艼_鳣M`m8轿蓾f鵩肠嘒.=W醰跹 ~廌w仓v'腈珑蓜鵟d舜剠鷹5s萢鉥種H酓梵酨iq蛾74琰箣鞊-蠉  鹋\0轏煸韫:~k?!鵶>酬z犪懴4jA L昔K鶼M忢g銃斈䴔7煣餮(苎匮蟍弡8觭V俧熁8/有 齘E┣寬;缺歴1eu措u曣&u氈z7M!鸏罂γ 漘蔋冨b+欳 鰠$@鄵鳼Pq皺 GQ~.鶆*0驟赹.%E*K鷷}F底=C啌>\e噭霭T^:}革AO0蔚琂>愷愜,eз杳箨2な命b怼zfa#嬿鱑LC.=#穈下s *$ dr忁!g眃8俨雅J堬N鸧忩氖.歍玖撏6UX>斃滹,嚿o率_U6=悷ZK|鳭%U釛o謃t21进婣ユ2V尌呈 战v]邑2硍nJ炈┞┪軤X濨^赕S擕Z睌i訤D"軘胰3⑧+唃BY+z%+5l4襨`鴴洪P5析颇CH禆/z(鼆尘+趸紭練纣糌!g訽M#馅}濌Svク艁 q經鈶抵豳锄些<,b^橱uJxB/C`騵嚜3弳"灔q盨嫷M腝榕効簧ke?F臬朼4鮱G#{N>E瞰臬e荭V?5jB.4N献ɑ扆Z驏m/)匆A:zid8+叜)挷[钗[zb9V髲臁簽w#昜4知斸+F佖!伅A犤;碅9J~祫狙;钮弾屒v*EY鎐9蕠魲sB焖壗 ,k鵮F錕L鏲T岒厲'Dd鴓U滴г淸3K PV<#g=j1唙嘞Ng#釋L筘Y婹蜘d4>e`0柕台厊9砐1熌ⅵ蠠R4暬X=资朢$F@/K嗵~滟︿5証F遐 軏:$-=嘋L9{蹃s哗嚼建篙耝郶訳臑[.蹻帑m橧 乚r椴F蝞n @ @ @ 伔& 縝鴺2郫5=腃 並l~⑼%礐捬硖w*s2)朮0B 仹葅囐r8/両@>壚|0坚mh笥巯嘤)欪劌Ci敊O縰羱!铦浝昛璃詫 侀,ㄍ厏汈Q鵻dY楃聗瓗γ殳儬*搓&x攇#項Z怠百_黂@h @ @ @烙 ;国頃溥/峅 @驶@柺A4nJ!  !佀>飌邴愁>杹Oo1K刑h8=8@ 瘃洪邢sm8,y.eA椋n"籸_p3o襂Z@ 饸篛梦奺俭>[=m5枨_?驌q橏蜷@嶮噿.e婱亊|逖怮r鶧 /B`i:趔_蚵n+P蹯髀_4@ 佦 瑞В︰%xfe隉碜Z;K蟢%  伾轪)訽增t,~)鱫d轤鯿熆[賜F褊"导I%穩 |u~凤啭 _齶? @笩厉緞峵伢霑 @ @ @ @竸@y'%檥抃%[./zy聭鎽}$利棻翗姤 l榢H(騞烸 !@鳻鍚憤賠圚蠔C 霹|啱*Ub䌷P苲c K9O秾抍 @棹悟鉏节}停Y'|幐X滙澂K4KE3JΥ6G櫟邖剣@ @鴍&印~苪鴋5*7j>9停>鲶鋓恲聠=榋!2"肹VT<j毷嬏O>-嚊%)2OHXNi屙'[参 侂 &?璬S蝧a3濜I认^!興?kb}鮨鬺k菾F礔戒.|-YCi1a檶4-h悋傃P棁 葸J逍歴2=MS鲞8an5匱2d琗俧 9誇9$资>刽tx挻#V" 郸婵頸!Y喎╗禿X @洪P\u\揑顠雇拙<+淶?錖G瓼癗c龀<看╕'c淎h洢筝 仯哼;错=欎虱>_l隊钨蚚蜿褈z<冰馝幟も-V=蚵4镑)9ⅢY酁f 讋蒫逝Y颗b踴’Q灒漟l)慴 @ 桷齈╗}崶 (Qc泩:薇Oi3嵧(%犙馼4緮$焩4漍BuVI箹_語燎j瑟鯐U僖HO┵5|呰A)衤4*t殻矠K唭 乄'p鬓屺唁4矎蜍m?嵔粭>{╉{a 侘#鹆硤处唢wVP @ @ @ 鵠俐忥岡酛#(M廨插悳關8 @郕6殑%3訒 庣襥,5cV邼 檚詩% -3魜珺 葭鷗駔I?9璆鯏*ゼ0窌>}璔f嵹H貤kVY珣 @I狖济瞐锄臚|朼Y 恿.2j%s夎ㄑO畞匮T鳦 亾洪P0束猝橽^G1帝盕 a9#-^麶0qf8賀︴Ca`逑B8e* @ 乶:槭耨Ya奉i拿T'voQ穕8a坨b! |e輙竿錣冟H癕,嵰b @烥`醍斀锡&b埂笚鯬攲%Q1u4諟E<辕懧C- @ |#ed 髪5f叱魎稺迷迮[/'[D@鑛'G璛L橈跅.J穖PB '诱撜瑫縊诚|_暤.駛 @x%L嚡t6@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ 呔慎輄E曪 5UY%F%w涹/H <烲-鎼=瘏粸?x9诗 鈛善* ?wEU龚i蒶瀛喭呝b遗G/bi~:! @xu~奟璦>c癲図&8礉$庢(颵c=輗f俏JVMhv/耲鏩V@ p怈1Vf檯r嗔v麇 S n箣榼4説彴)駇簑(龙mM唶b仿覻ST罼 OS 洨&霎ボ"h 技縚R嶮-󰬖争剐&E+>矞PG( @()5NTE?崉镋搊!IVbyLO蒨$ι 欳.誣KQ 瓢 #|>⒅3痏]╯/鉻y蓮'旗9问  @/O`o8穴沛l.閧璭 瞴&?澷ㄝ冾=琫/cd |橲@例 3搦类帝!m救$:肣O閄or0u>a/觫q @郶䴘LヾofⅪj^难粝sN4濢岶q绸葿播蒮K檰.桅网栰4Z 遆餣il鎐摐殇X縂蚜Y輗敽1,0b <搥Z)E$柅阼猉羲K-憰9}6+a帷枙螬/矓苧:鼘骤 /D Lu7u䴓*7塤O踥 @ @ @~'皊燇斳o魁&>?釉髍鵺4龞F徤Wm隞廁Ss茺蒸颼?b焳橙_昰ΨX箭繎E发罋閼歓w`4J职 苋P 揙囱9/$ 旗髷筆E蕌.-摲駮梋{褰V魙櫙誌佺灹疥齈!姒'閮+4)i餃>騶)|$CjA鄭Lγ矒珕鱥惊鶸y铍T3繈位9\n瀢缈y 科6 fax赘魬渂鰿す<鈟宅a曌&3夣鈂Zu1娛錼瓅糖]X`Y=. z碢纬XT柺.rH疄}矠l)箐蜤u圤愝揥>}鄨陑 絒r呷J%C3美绔靕擥5徊T z蔔死懭3l挩E`茭Z&篮[9_e萇蝮  (拂俼赉F菨堛k蜮b/z碡糆雽,YE孺 塻葥K汷戊U>屎h麮ЬV觟<磪Z>I04掼 灩嬱`賒蒰栥`蒥x薝荵O9 n喝 唾J鋝1襍z嗞9枧' 尉惮<杋{澹諓*惐篰g呣択y>U/妁l轪氋'$骨軛%4艦珬|9诗 瀆%岭惮A 鹋 \鮳' 驇]脩幖逷}咈瞖j惠藴蝗usf擤鴊K矑?W烝[謗M鏍g/j*柞4呆H幷* KIc6O煭_mD^兢%鞹:4n虸-I輗iTE硗s腾Y5龡25 蹂3怭KB>*瘱騀#8鏻Ya)rT;M噺废 畸鮞鍟w瘧9痓9 IDAT軈香瘺{(#呬籰乖U挣=2鞹aFi\夕鬜炐疯眧寰Smv%j轧苨,*珬厦蟮F]d鸮ňz^U舜V蓀 ;佌籖释縺楱蠆菧 }q咯籤/蹁<賠―yI+3玥タ.伎$歍yi喳!9韮-.鰀L裮R][)捶敓?賿3H源髋6鰕S_@龐f~ 7J=uV漞昋?熫x)kI敼暁}敃0c8P靡g謦遑wgKf 機硧z#74糂k\vN辚+銸efX嫔毥欤 雝綎A揟V_:Jk弦済򱹈锋罀x蟶.F算凵赔QfM溢鰼B0=}-疄M璔颵Cy $軖{b+ 颓cx/祑腳釜\'黸 摳瞮O耊% 乄"0;|%礹伬*伡i塤K 忑淳,B @ @ @`H%~汸%晏/F潐弭j+U7%鸕V阶9寿G@u0"囹Z&濽 佌O磇:z塹0钃;质6灨Zv^赎∮G襵媄{C伍MOT褺竊:+帪5l膤%|媲蹋{<^#+~jsM 伾t(脵~﹍?饼 c湉捸?狱骟>e?鞴惤瓚Χ&创鵃b外{K#跣逸}y1粇昑祍淫&e_ r 蟷鄀‖皌@O老傏瑣兺\8婒s崛'T!/cv=灕[髟T緰^朋シ祥W-V掔◥刽5槼|%闸猍茕渴_c楝岷妹&! 餬洪P5析颇C蚸-_鬚滮5K&祅=镣Tw9祎半嚰m8)q旔r!@鄖&縲騢-釿[砐跠暈$詪h8跫d猍I银+n┸<甓迼z6У$v脓)aKy;]0鮕Q糕3-洎9Q名-j30韙殹瑡^@穡h僙s -#?饙).}td<稕3v .鶔诛漻靑掉爈~臛񽥜);祼琕楘ㄠs/際T7%%侽 v趇Y=$峔+燅L#G単E j >P庴WV(>鵿eKN 癎嗵~dWQ甞忌讧鰰漀叽鳢|傭)@ @ @ @ "P鶕谟_1荻.曜o开/邁地悯T梮^^]巣山_衣v扺映 伝n I樝瞦Dr弾S焥=l/j旐聗G%F扟麉z沦K!@ 棒y囩黧6攊葯yq戳-F锶yF匣臑鐅>没癇' @喙嬗√g~D訃f K覍Z|T定涽))X`箊(踆鷐I-绢J詩螑Gw荭 溯籭璡=1m~iQO遅%isH0朡S=8@ 癐`: j<,^%?9譽弳曓瀏媗Q +鲛筹E篼a,伧猥徳s吉=杇TB鶱C曟酘OO濽@擛鞣R;样纑鍭,w記赼恮-"f稊蛱h飡顛&9昃7RYK[yh}$譄V!@&p藅8U#!峿媍4绾 +y4Q媉蕱i2驧Q哟ッ灺2蘸袴掯ǔ!%暇? 筮;\D7銶&嵓闋`v`sL4卍)詺.稠E匱e抣號I婹e笣Y跄杀妄}9V!@!`摍唁X,=T7铿>!軐夏繀9a邔鷐n#8豍2蝰S撕O񁊿罿.UWC獞卢,e-] K^(S*W蚟a861!s裛! @氨扈|vM?<濒匽<>U9=^泼*.N覥!@利橖蝞5鋧 貈葲/xv@ @ @ 乄#7+;偽7驤惬5@ 乁遢 j K XWk@逕纄焪8E瘳粡e嘤黦7.0NO" |<乶:趔t9K蔻K貵P浫朿_║y0褺 伔 p/樯xg嫻賮燼趝dJ@ 饄嶮囈%>駽弾咷?飝@G @@ ,M噡 C^豈|瑊醊+4@ p;⺶4?礿犀L齔Kyg肦銀$睞 7"0; ;咜雰跒.櫯/妣m攟利藒祗w+弁(~汸7╀癅 伅N里鰸啭;熱珶鷩 @棸戭{~V!@ @ @ @(顳$s$譣濒9u5鏨壓是W螶%淄>"踣b撑/暻b蘍俨4D貱/蔡,栖 篁|[蛠>[萛V檄鮍杁'(駚Y6@浪WD諚嵸r矒減挪R4珩QJ球薃ē{(驿f 螄蜾!缟碖1幰f籝VOY=霟FeU扗嵐D丬?X %p媾薠槎⒕ 唐l戜Zt/j歷帕J雭W2:鰅W瀕睖A徻GU|灎j蜵p沄/觙c葥 ]>c筺螠-鵟xC纆榺轪緂9X洹菸鯯%5跌勠堳d璇E隋廍 Ix#胑貒7〣攈3K杏 QZQ峂|_bt汌賨鸼-邁𾏼gJ<I4鐣熫葄Tx&銽ゥ哷 !鄔螋x裝n隥娞邕(-泧騺宕%跌噝-賡臨嫔%B9*[F諁%3f倏隙!D─P陔蘒澗2砫+}Jzrf熤爕濌颥De烱獅%YF]X@桷A9魕囨湥蜣橗7痮4%塣 謇' 膳gj蒛4%:s颟抨6r_哟{緪g媎^墁翜Y諙|Tp藋ay))夊ㄠ肚'G暯棐俼<鳣浪瑕C%秭 偡b斋圄j}挴X9S猓 W溩K @$i毐鱵mq殴駪辯~泛蟨f諉K馇9丁领2#p6`gu螬DOx篙嫜<跔DY郕%8髡GQ}砓O螕}rN壱oY '.?K螠-YOOF3洙9[叮B狶5[V喝 盄纚8鼦酎权E'H(將-孔|f[裚)帽则3乛缭R^_鞡hQ翏2E奪 狶说L鯰彲曮4澸蒖m 邆<兟建蹺郑偨浆魟6韆8萉SK甞jyV⒉ 煏'S--櫰$v@蛭IR劔i鳖$)棶蕓4忳?9!缒@M鄫4揖~?; @ @ @ 侘 0+E毎汵F MF侘貳捒礶鍨兟'鼥鷱F铜6e%逥晜s%錇虾焔U (9鄞2F@n$`梶s斚5蚏珮Fy覭親eJ16iC7婀|赎欀4猅\违{嫢}  \H狖糃_骑忪龛攃膮漢*蜾鏙虐梅婃餱﹒^塟駃J浈茠ゝ媴 +=邮T!@X$0鵞)9嫀\齟[}$周詒彳悈,絋婻m-埯獽Z焔%j'玗廧99O勉 蠉  @O!蠱噝-S俓喳氏v琎耄,0-1z凡qC枷(s賈萐鶯峽账(c濉M瑃aX4s>)朌b8晣 @蠯uH攳"渠GR%谯m栈5e硾Z*M漏蜵靆捧≦_ 3笊鯭f厪 ` @郆蒉醰橶垓慽c*&鏼2B '皕W蔨觀>兽'!oe趽(龗洌92oheK痬磟U濹霈'S蜄僝曜報Q跌C p-伾蕝<\彭bo=说m刲^渡S暆5雑钭g蜵SKY=颥D>YO&-#e1'兎洙躓 }#'籩hX @竷纃:及d-.L﨔┦﹏慟S1鼀9p @嗝 髅互=@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ =咣癬踀!@>壚/姨_>!z @烝嗲戯粄)U禀魨焘鏇牙`奢纥w ? @烥 'S9d蟢琪烗墨`,6X踹獈;%ml( 蹭gM椚  @_橜ぬb鉍傍'裪7糣&_Z幍佻'>陊瞙犒<5 @6悩弽a邮f潰|蝵鎖猒4殠=[r镔b4t)<鉪镄@C柌眃d賢u1狶 xy蔣3 L龇YH*(|E>rj氺鶱|3搚2k#C碩㤘n @郱噁/?嶝愩!?q$崥巐铗樧湩/&>$螷撻0ㄅd廘V3A藧y錶X @拉鼧礮栊鄀3忎o啫Qu 3e衆N_氻$*Q浻aP1枎@O嘈0Ρ谦]1塉嫱镻〃2;銎R础U飹T挛銑厦N鷅+>嬪傐UggJ証飡Q+z饋 餰 祆^5XyU獩芜隷Poj糏{斏Q︳t閼郸b尼*=W錣褜 @牄uS0糓\>溇昹麐6Z驀 埡%Z;愑岤勑嫳.壟f1c禠烺崒埠諾塟癖杴y/溎*鷲|"虙3<颿苝鷟+}y+]>m,s朼鬱鐞歉->-a93號罨蠪V|r嫘:剆as 鉩y滩u煵/>扏鲏6r蟫菇孲0: 銱蝡$qY鼌?V妁挦Q/珮/綅3 ]朊&s謾{ ]s鸨j藤逰6Z昿N}傾IFE6蜄5凢rQ;;俉>葦幔豅u獃r铳,搁=)ky7熗嶮夞)髕C魹錸镳B4秝鑣薹k/蕾蜻J~逷 棛p給!嵜J匀蝑赠 j妫yrf,>徖儲名3_xlP汊胈]F装uyゞ]浾-,6.~裇闖 辖*鹘(嚏軬 娋劚蕐1N莓蕼箐N蠝P轌kM踎q写鷏\懕@C`r喜鮕n G}Vm*鐴Bj賀_枕聥%f晈鑒|疮倦螳&~駔8V|世襵H狯y毸<楐Uf.飴纬襸錾朡S婀}; 丱%0Ci[g竝7壺齰嘣>蟳蕑他 2P隷緁Y岛9Oh*+>Ρ鍈=筕茷}Jekj,蝗潠毥?Ye2s頺趝賦离樴矕CHV+率f叆瀸詩N鰸嫣赒9誦^R堤灜㈱Z! 伅@@飌>耷逻_暅9_夗鈹漘腞^醊D酆屪' 譱 @8J帙俞Q菸羐L噐_燑+拣 @灷棡y*@ :>@ 伅@帔O磞謔曩拸K+蓑遯.G碟糊j懅筂=Gz鸋刊; # 津r>\嬬玊 餌>雰i濰)貧稥蟟 \ 氩m属冹zx稆某 c 竳锇痺tu{'蚵恁^骽犀忷:2$綃委筇@x伾t>蟡z鷖}l 垨焃 ~0X麹i檓谌挣d鸝'[顊姮胸#hX掓`煌L,[V挆4,PN獹Q7硒@浪XC涚0~d>8奪馫d!鶖cG?_5I鵩b个麇憇氲駺擋 >炉啂寙u楍Zx幷ガ_!撤靧WV汛虺♂e囈#*d芝_($! @)魹逭矫CW耳蘂洄崒>|%U㎝.鞧G疅 J%f逃-晝Hi霮飙枀Jb,滳徑寽9[ U-[B瑫O}蜭C肢<盋^橜7鷠;輓恧軌塬–宆熳3鮛宪{8挧太f陖詋畓b炾黑压X绅橌耽@K,缪0[DDi\g倍墾佦9蟀5U3ジ咠ろ亭PQ螋诈=薣)1豕腁52U覑Y)解3諃 @嗾t印j~1[诣4*(}t4祫崏z-棁賐bl账;'d儠揟6厰㎜彮z! 鬎漽缪(摣玟+>9je棴*U龣塭K}dU:%u又L^Y+煾,  @ 曭,橥@y$?蒠谝xa褔籉轓鹈N% @囡,/闳璒5扖 栴Y~婲螊捶惜" @xk蒿緐c垏 @ 衹⑼F3!z魃j鳢g昪辫m氡*牘'?稆荜珲 伝 ,灱kx眹搒懮L魁$川!竚dh媛 7魟嶵轚|雳@+X瘹6@鵍詥B @婵wS瓕d\硙D赗s皰GK莨藇谀⑶v{K垾憦e.焄Aa9蒕X @!箝餚:u秠.X6&?:J曼鰇秳6x闪再楌沣s衦拘^2晷満O@乕γ)话9蹩闪OW:炦!慳@ #┉鰖V貱猵 @筮;\d锤睲&嬺紱蝆錶訙a赢苊&)k恁趮* /H犤;炭YQ唧5媉歳,疼奩4浴塬([覒藛H?泰廦黂垰*嚋賹^纰玩K%V溽@罈"鸾轹菑:􀔭u鄵B䎬Qn @# 柽Y瞀脽黧6n yqd鯲靐忢捧# \N`i:技* !@^搥N嚄輹驓M  @錧c 飙d3f,暺C@q @oM嘤剿D~0蹽 c |雊-!@笍楞γ;>筽厠螊鈏B8# O| @鴏輙桤9;2.立炠G-蓕LW預.}F#銄R @^怈鱥豗畬w鲩6v-朵}|l蝲箙i飏$ @+86 懠-6/J|鋰& 忷単=B 5 ,M噡 C^豈|坽醊+4@ p濦鱷2署7zG8l n阷蓯g婚(8腼#柨晉6扏 佔'0; ;:/jW篸繑壑裵4hf3$6瘗┭o=Z IDAT蠙娔B 亸!旋濆?~豻x~;熱c犛 @xAKg俎泩/ @虚鹭;薵 @ 瘊夯R^_= !@%餴印%錤R}禱w乓缭諟g%*_=+蓶\7Y踹!a巬.Qe]s >9O秇篂U曄@困M囧蜅鋖)_啉癋r!必岪昋9浔Oz霃姚儼H蘏蛓睧x嫣罵F嵨v@^k:|蘥"9鍁I粢踇mbo‖羏@l#笊yV2詔築C|F:g螙W@xe摶R=肆?忢Т?G)L-梶饙\^M鮴泞 !朑,砧:]瀒耍玽逯劊T!湿g= VN寙蚢MА懢D俚譱難<呙9肶 Z鸊i旂+煗,&:C/`/軎H @/N牄G摕对宷朵鏓3霜~烐鶑伪斯萿1"彰$Qe`H.┦X祂!u葋鯸 F9U廍鍍R硨?Ы"2B所a8_轪鹅n憜o9)iLOMU晢, o詅櫮寋Q}NV!@嗝t印;(噯6s蜵y镄威该Et#儎卣]|麻z尻/压褲誱瑃盦z1膭呇o谘@女'7C餎鸅嫬W%4D擂 趮Z7@輙(M旮謑n兏d墚╓e.+琟貵螤蒣9试櫼~綛仙建W  餪輂)2跠l臋s#K偻[騤燀瑾4,r犨6@▋V萭葪踠)猍杹+疲y碅磔槃 )5,″疥掃蜾 塗#┲9X鎝NswVb碩诮蜍 睉/梚洙l蒕e !@B狖;?曌娶 pGQ鈻遦>抄璇傖Xj鵌Bj)瘽v/ 蓶2E輰哵秨B>箹蔀犟祌灕S摟鷖V:嫶B箊幨M*哊K=井栁TC_ヂ' euzX_+R鐣畗S!J 桛悟僡yf4躒涚廞猽螿啈蘂髕1箺r蝜Y﹔m_YC冬ê**缌@8O骫韊鼗誾(i_U鍗T+献埘鯕碤@_俞9譼 @t:祛J欑 @喑L>袴氄J歘:<挊侩Y5稞.c纬遗bT綠G97惖z%缟𣖠螶虫愠﹢i#囼 @# L趼痺",A则j柩癄(謲<姠痭H磐|r瀕憱s鎖灒狘o};3 <尷d:4:麒C.}煮|灖巏覘C浗翔屼╨箖鏗等 @赖骘,噝:漉蚆-6歕勖J6/鬯般9 >e0KRF2VqI伍(-v@迶@7闘噻*i操逺逄+內y巠3I^/TjyJ 礢9s手 ?喅2&虾Oy籍拖倦Q狳濷瞁(@.$蠱囒e麯禎亊8S蚢 b杷V_g*赟鮻D @O`醍=e~莧佼娨┪廌梶懻=珼樢|匀荎 岵敚%Gi涡顏!@xk?黶硓t)L$G/-煖.S眭uU 彠t時駺 癎郖漞踓{纇竪z^*旯s闗  @鄡 鑤  @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ \A嗲穙騷鱓Q妍BMU=GvI闆懤曓ri#梍X鴈磳 丱%愮摡茬禂煮棡窖廩6V1XU4.1緸y{(鏒室矌嗏jKa8A 痤l贪F蜞,声d>u裁(肎 Jl鱿螀踎2]{RI快`擴鷏X鉽3# 鸲b儑6-賚#0筱窟局_e婆$"\Cz镉鴒T箱B_0抑(蒳这搹規b盋 鸩&焗^P屬)s1E騲T瀴2旯蠀%晑楉叅噄愉!+咅 @喙&觓)瞠/彆e9 犮 Q '薪箨7没匹忯,v?D歔#濿蜌诙德:岞鋜旞決cY3K$<|蝆嵫犍2剮ru:%I`槴d%X @弼^0N}济桫B^2饥鯬JP1;7賉*杝?R涰賿*陳艊N颂&F,  @oA喁廞YN悧烇Ed)> [塭笍蚚彛剔楾q杷匲 l⒅3珵菌J_鳣 :4距 x鳬5韍O铮Q鰏 賩M搋積咧溥% >}$!@繫,%ll媛Q敓 G>∈j_~⊕c2?%砲鲅鼦99U柮-く鑅.叉椂柍@ '伾遈撨"窌开l>z ?禆昑叒黭|j褵O煋d7逧仰罃i 1C 乶:攓蛦辰1_ 枯Yf,空=r樘2yR冀a @  L~颬戜0[某4.B礨跠昢L87{鱲T覇0P>q囿c頗6v@$乶镄垫@狔諕鳩>:2^繘X诂&釶Q s^i)Yěg朚+誷J薧T # @x03麘彁沺o*g @ @ @ @ &鋀 骩喛~慀7釀L@ p#佂O垂C谚v;j9K,!@烙浣胠笵倢儣$  @烡`緒摒諊6殆镸忢g痖髷L}乱4侍н簀僠?  粑M郕Sj@迚纓詅V鍮?M|<铂兯sa?髰VG觓鲎APZu<食 @鄋葸J佼m緼,g藃繉槆癬君⑸鞞|鋁杣穽怪v*!@楞 2N蹛研凭1q歋椙呐$鈌炽浜 眛)勭龔茤0B 乕 |锍雙秖kH燊KP仂/~) 4 @ @ @ @嚅錆\J蝻黣孁椘@ 丮錧 j K X7@迱纄焪8睚Yw死Х冐袠%hf4湠D @鴛輙桤96r柤E棽彔鬛7慮/隔崒7$- @xM荮ag2搦縺g埈灦翥8L{a @S铅CI楓才?掘h(9}⑧@!4/ ya7峼鯧噞岑8 @理d研元<2跸鰇-鍧%j K嶇祾@迗纃0戬j{篸繑Q/线璴7xmB慫蓼抂@:扣w~顼|啹~> 躉鄘_翭禊鮆X @ @ @ \B牸鶔>I畳-<酘笕>帻素`>j墐~泒"斡蹋ΠC,侇/閩 <"d擞泒AIG櫖磹}%汞\空y"I6? @+x鹫頺. M鼡#[6:㤘滽4K洊g斝3>僖4s_桿@雷!03G郇Q錙絈嬿蟟醣'OL6旇猎%,饜槓gT]sz懝t鹕2Φ u酸f y狮杝汥y{錾朏纭2拋3 @啵&?m姚@9蠀峅堯9螥魮悷紹N?娚8: !0您*曩栀謵晫j峼蒥鴾9*[矄襜2欈Vc屬G拰:貔9 @棹V;(>镄渟擸i偛瀑簆(Qc4[?醛6:!筕錾ェ#敜U99O5u融F朶訹$首喊頗v@瑕Ci[5欎.欅<撬韭I 3识$澠扃v灂繣:紅GW斻@哼;错=欎虱>/h隊钨蚚蜿=郯bi"莂堮⺻旧枩对瘉YL殪岹Ct驹4蠟f %U撉嫵~嬇耳=毼企(&K 挒5梈t偞煛(J茯{蚲帴枱Qt耯赌麳$眣u覤簹}J叀栆 辟'Xr-FzJh摩堤Acj沆%壉寶vj滇颔颂: 伝G$an#(陷觯z沿粄轶織诰V!@x?<婬k~g @ @ @利愡p牮鴁K亮榼%(譔桔奅扡37坮蛊%@ p/M聦獮檦.o@'稖矲Q'X盚4s栮-V%魜珺 葭鷗f>q葡RBC)/Lr羐5*Z(潖f.銁鯐l鳣 O`蚍R2,锄臚1sSPX 雍Y暸朡棍 薐f&睄妱@ 3洪蠪:)`S鴌&做#k允〝編%虠叿j厑? z|(箅蠧  @oA牄稧簉||諰悃論$蘰捦[鋋9,坨鈷$ @嚯瑕胢 G俶Z絧R章@逇狸]){M鶰膔Cq/怼(U鮇幁芥~b濉~U豔a %鸾悜%+6謽}弦纵^ So絛焞{}叏曁&蘷祴襪 @嗌聹鬱5k屣屿3遅e璌 @^俞+ 碄 @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @@C嗲穙騷鱓Q妍BMU=GvI闆懤躘0驿 簆 @慇濷s葹1请g9G檺 ^6YYl傍+牍&2眲~鋋V宣\$4 |-y2 $菱瀯迩洆﹔x繒VNVYO }鸎a&Q"H卢dL耒悻r鋲 "愮 丒蒉∥m蛴pa+}Bm5夵 cb榷(鷲沒z頀糱)K對)31WWO眊吅ん8刼Y%2y|r-s璜R~U濹~蜊缃* @@ +䎱儣>;J/41汪摶鹕mu雹竔Q;愐M>鞦>a躬,+諆忰^j7錣﹉3b闾 @`兝u喅涂饗僙r険2eC}蓷y!9_SG>yM磏9NyIe-k9j狦,pZ啟嬆爆W?萤'鎒粽<9鰘#Z9{V敊5k沐S<5!@嬗醔 7_鐛.裼蟞海|n鯐蟙E> 佅 苞悟2帹诙諷茽鏥1@纁<璁斍4驣U烕庒s玂香嬎涥 鹗灩w\@&侂g輭鼰'o勤麇gU3鮞幤瓛齷叛盌e\賿搊[<擁$M鄨sC犐斗鋥斻浘鶱o=売嶧贔丟鼼y俄吓-汙@7竊:|8肛麕饅^AR~eU賿6-炆v挿碞K)'譜蟘\稊f苡s雜絊.瘣1恬姡幺]E蚺螂蘥3KД;廵//k媳=/ 癅`2蕄e蛎gJ.驎意漨oI/$嘐5乀籡樀4!E#哵 >攜样Z嵲Qb蟿E賤j殜!盏濟v3嫟3=y,[鹅=-穦綣=9y牨~俽*,>幚d:斞mer y蘂t9疳+& 鮦"摌?栱揖鈢筄Y郊嗁錺J劊愐3h鍾U檃蹣KK顢J6洆疿Vz)鰜刽橬晢鲔d6濃j)Cz齇Y菪驍( @洪0禊-O良yA]辉閑2g磱,獬畱痽咑澄#/茵S~&%w-(篥k;鮶3O锺 G衔";;橖蕖稅w矱蔥<跎枽S )(|隙蒫捖~蚗3羫牘r璴善@喑|梫骺嚘糎泧G=輟八$)/*9太峞;吧ロΧ熣蚨囗纆uzU蓓品/Q陾碄@ t{嚽砞1趉几掃鼝詪a!蠣x侥怒迿n哗砝;婚ro 煸济趘阚仐Pyn鮇Z  併鹱 6恹9開=c @鄇縃楼籖.L>@ %酗緰?礼}疢净巎>舄%He螡銽賿6,桍茧r4vw晲 >嬍o:【〦%丆:z^r豫;灃b蜸=Y攑@#俺w(䏝0氦<猣藶靯1螙教!室猺;硹鹕 oJ ]e箷蟄蠔珰菟s9 >{T墏 餌圇鹁踋<惧\1[.m笊J帾?m 畎\如yw鏫9)+>w$? 楲噕旍鷕o{XV蚎S:勪9J藱sY=s驎.r-觭蓵/珤妵Z |蕜櫰奺匫(昄^蝣毧(;]|Qd>錙璐z賅辤D蛕稼[涓9琸冻/-.,弚P儋"鰑厼j甞娔@嘟t焪徬 i{揰:κ(+絶m玥i鸎fV/G-殫1鞹悯N废r.頇囥撋敳㘎n0;讴┎'襀s/杋u6?72伈s-煱6怟z%欆棌51w:娡-dOC!頴豵泳萰盳j@鄀鼅蟫穡ǒ冨K覥幰 5穦嶂字|駯碜唿厕taK坼}Z搄n佗3F 轶髭Q鞬XSr爫6%C1鉅>9vE已婧9*鴶I(1栁'徻sT禿枲箲簰\砿'){獖e狄>濫牄E摻嗍+牻PN_ (:3/蕎叇]啉狽嫣TO&答u>WU=[颟櫼(煓:絵慜H庲缇肩埰(廴~U濹鲧曩畘3(1!蹽牷gY^1髬Dqe渊丵扱2廣ゲ楋rtEi!-霝1杣K銠*垠>V藬$昑-諍fU=耽湎饍q0鐪'[琤燃虑魟G|r鮱薭_+|rT稊醨u迅X+g{XuSh髎5嗣@鄭t印戒楂>詿摇刵s敺鳷r2J^扽#>>鹅"176)3珡i0焼嗺烧{1!9鱍櫩嫔X绱蝱>9*[onF5鴺冓Kh嫷悳9[FH庞敂誈祌T覹>僗 >侇當o咿z]>潼'oE帷9[踅# +^榡蔺Rb謊 @D`vW蔥碯頒紖蚯<溺愺c暨] 秥7R駽阑栾J鶀~縪曭u1伹S}|艐 襺j_僾1C缷鑯镳E%#  @6珻?c"AIEND瓸`xapers-0.6/setup.py000077500000000000000000000010641245427370400144070ustar00rootroot00000000000000#!/usr/bin/env python from distutils.core import setup execfile('lib/xapers/version.py') setup( name = 'xapers', version = __version__, description = 'Xapian article indexing system.', author = 'Jameson Rollins', author_email = 'jrollins@finestructure.net', url = '', package_dir = {'': 'lib'}, packages = [ 'xapers', 'xapers.parsers', 'xapers.sources', 'xapers.nci', ], scripts = ['bin/xapers'], requires = [ 'xapian', 'pybtex', 'urwid' ], ) xapers-0.6/test/000077500000000000000000000000001245427370400136505ustar00rootroot00000000000000xapers-0.6/test/all000077500000000000000000000373201245427370400143530ustar00rootroot00000000000000#!/usr/bin/env bash test_description='basic command line usage.' . ./test-lib.sh ################################################################ # FIXME: update with source already in db # FIXME: add with prompting ################################################################ test_expect_code 1 'fail search without database' \ 'xapers search tag:foo' test_expect_code 1 'fail to add without file or source' \ 'xapers add --tags=new' test_begin_subtest 'add file without source' xapers add \ --file=$DOC_DIR/1.pdf \ --tags=new,foo >OUTPUT cat <EXPECTED id:1 [] {} (foo new) "" EOF test_expect_equal_file OUTPUT EXPECTED test_expect_success 'new docdir exists' \ 'test -d $XAPERS_ROOT/0000000001' test_begin_subtest 'tag file exists' cat <EXPECTED foo new EOF test_expect_equal_file "$XAPERS_ROOT"/0000000001/tags EXPECTED test_expect_code 1 'fail to add non-bibtex file as source' \ 'xapers add \ --source=$DOC_DIR/1.pdf' test_begin_subtest 'add bib without file' xapers add \ --source=$DOC_DIR/2.bib \ --tags=new,bar >OUTPUT cat <EXPECTED id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'bib file exists and is correct' cat <EXPECTED @article{Good_Bad_Up_Down_Left_Right_et_al._2012, author = "Good, Bob and Bad, Sam and Up, Steve and Down, Joseph and Left, Aidan and Right, Kate and et al.", title = "Multicolor cavity sadness", volume = "29", url = "http://dx.doi.org/10.9999/FOO.1", DOI = "10.9999/FOO.1", number = "10", journal = "Journal of the Color Feelings", publisher = "Optical Society of America", year = "2012", month = "Sep", pages = "2092" } EOF test_expect_equal_file "$XAPERS_ROOT"/0000000002/bibtex EXPECTED test_begin_subtest 'add with file and bib' xapers add \ --file=$DOC_DIR/3.pdf \ --source=$DOC_DIR/3.bib \ --tags=qux >OUTPUT cat <EXPECTED id:3 [] {fake:1234} (qux) "When the liver meats the pavement" EOF test_expect_equal_file OUTPUT EXPECTED test_expect_code 1 'fail to add non-existant file' \ 'xapers add --file=foo.pdf' test_expect_code 1 'fail to add non-existant source' \ 'xapers add --source=foo.bib' test_expect_code 1 'fail to add non-bibtex file as source' \ 'xapers add --source=$DOC_DIR/3.pdf' test_expect_code 1 'fail to add source doc already associated with different doc' \ 'xapers add --source=doi:10.9999/FOO.1 id:1' test_begin_subtest 'update doc with bib' xapers add --source=$DOC_DIR/1.bib id:1 xapers search id:1 >OUTPUT cat <EXPECTED id:1 [arxiv:1234] {arxiv:1234} (foo new) "Creation of the Universe" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'update with different bib overwrites previous' xapers add --source=$DOC_DIR/1a.bib id:1 xapers search id:1 >OUTPUT cat <EXPECTED id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'previous source no longer in db' xapers search arxiv:1234 >OUTPUT cat <EXPECTED EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'update doc with file' xapers add \ --file=$DOC_DIR/2\ file.pdf \ doi:10.9999/FOO.1 >OUTPUT cat <EXPECTED id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'add bib without file' xapers add \ --source=$DOC_DIR/4.bib \ --tags=new cat <EXPECTED id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'add file without source' xapers add \ --file=$DOC_DIR/5.pdf \ --tags=new cat <EXPECTED id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED ################################################################ test_begin_subtest 'count all' output=`xapers count` test_expect_equal "$output" 5 test_begin_subtest 'count all (*)' output=`xapers count '*'` test_expect_equal "$output" 5 test_begin_subtest 'count search' output=`xapers count tag:new` test_expect_equal "$output" 4 test_expect_code 1 'fail search without query' \ 'xapers search' test_begin_subtest 'search all' xapers search '*' >OUTPUT cat <EXPECTED id:5 [] {} (new) "" id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:3 [] {fake:1234} (qux) "When the liver meats the pavement" id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search all pipe' xapers search '*' | cat >OUTPUT cat <EXPECTED id:5 [] {} (new) "" id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:3 [] {fake:1234} (qux) "When the liver meats the pavement" id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search all --limit' xapers search --limit=3 '*' >OUTPUT cat <EXPECTED id:5 [] {} (new) "" id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:3 [] {fake:1234} (qux) "When the liver meats the pavement" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search text' xapers search --output=summary lorem >OUTPUT cat <EXPECTED id:5 [] {} (new) "" id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search prefix title:' xapers search title:cavity >OUTPUT cat <EXPECTED id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search prefix author:' xapers search author:cruise >OUTPUT cat <EXPECTED id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search year' xapers search y:1869 >OUTPUT cat <EXPECTED id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search year (none)' xapers search year:1868 >OUTPUT cat <EXPECTED EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search year range' xapers search y:1980..2011 >OUTPUT cat <EXPECTED id:3 [] {fake:1234} (qux) "When the liver meats the pavement" id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search year open start' xapers search year:..1990 >OUTPUT cat <EXPECTED id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:3 [] {fake:1234} (qux) "When the liver meats the pavement" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search year open end' xapers search year:1900.. >OUTPUT cat <EXPECTED id:3 [] {fake:1234} (qux) "When the liver meats the pavement" id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search prefix id:' xapers search id:3 >OUTPUT cat <EXPECTED id:3 [] {fake:1234} (qux) "When the liver meats the pavement" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search prefix :' xapers search doi:10.9999/FOO.1 >OUTPUT cat <EXPECTED id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search prefix bib:' test_subtest_known_broken xapers search key:Good_Bad_Up_Down_Left_Right_et_al._2012 >OUTPUT cat <EXPECTED id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search different prefix bib:' xapers search key:fake:1234 >OUTPUT cat <EXPECTED id:3 [] {fake:1234} (qux) "When the liver meats the pavement" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search prefix tag:' xapers search tag:new >OUTPUT cat <EXPECTED id:5 [] {} (new) "" id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search --output=tags' xapers search --output=tags tag:foo | sort >OUTPUT cat <EXPECTED foo new EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search --output=tags all' xapers search --output=tags '*' | sort >OUTPUT cat <EXPECTED bar foo new qux EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search --output=sources' xapers search --output=sources tag:bar >OUTPUT cat <EXPECTED doi:10.9999/FOO.1 EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search --output=sources all' xapers search --output=sources '*' >OUTPUT cat <EXPECTED arxiv:1235 doi:10.9999/FOO.1 doi:10.9999/FOO.2 EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search --output=keys' xapers search --output=keys tag:bar >OUTPUT cat <EXPECTED Good_Bad_Up_Down_Left_Right_et_al._2012 EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search --output=keys all' xapers search --output=keys '*' >OUTPUT cat <EXPECTED 30929234 Good_Bad_Up_Down_Left_Right_et_al._2012 arxiv:1235 fake:1234 EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search --output=files' xapers search --output=files '*' | sed "s|$XAPERS_ROOT|XAPERS_ROOT|" >OUTPUT cat <EXPECTED XAPERS_ROOT/0000000005/5.pdf XAPERS_ROOT/0000000003/3.pdf XAPERS_ROOT/0000000002/2 file.pdf XAPERS_ROOT/0000000001/1.pdf EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search --output=bibtex single' xapers search --output=bibtex tag:foo | sed "s|$XAPERS_ROOT|XAPERS_ROOT|" >OUTPUT cat <EXPECTED @article{arxiv:1235, author = "Dole, Bob and Cruise, Tim", title = "Creation of the 纬-verses", year = "2011", eprint = "1235", file = ":XAPERS_ROOT/0000000001/1.pdf:pdf" } EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'bibtex multiple' xapers bibtex tag:new | sed "s|$XAPERS_ROOT|XAPERS_ROOT|" >OUTPUT cat <EXPECTED @article{30929234, author = "Me and You and We Know, Everyone", title = "The Circle and the Square: Forbidden Love", url = "http://dx.doi.org/10.9999/FOO.2", DOI = "10.9999/FOO.2", journal = "Shaply Letters", year = "1869" } @article{Good_Bad_Up_Down_Left_Right_et_al._2012, author = "Good, Bob and Bad, Sam and Up, Steve and Down, Joseph and Left, Aidan and Right, Kate and et al.", title = "Multicolor cavity sadness", volume = "29", url = "http://dx.doi.org/10.9999/FOO.1", DOI = "10.9999/FOO.1", number = "10", journal = "Journal of the Color Feelings", publisher = "Optical Society of America", year = "2012", month = "Sep", pages = "2092", file = ":XAPERS_ROOT/0000000002/2 file.pdf:pdf" } @article{arxiv:1235, author = "Dole, Bob and Cruise, Tim", title = "Creation of the 纬-verses", year = "2011", eprint = "1235", file = ":XAPERS_ROOT/0000000001/1.pdf:pdf" } EOF test_expect_equal_file OUTPUT EXPECTED ################################################################ test_expect_code 1 'fail tag without operation' \ 'xapers tag tag:foo' test_expect_code 1 'fail tag without search' \ 'xapers tag +baz' test_begin_subtest 'add tag' xapers tag +baz -- tag:foo xapers search tag:baz >OUTPUT cat <EXPECTED id:1 [arxiv:1235] {arxiv:1235} (baz foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'check tags added to tag file' cat <EXPECTED baz foo new EOF test_expect_equal_file "$XAPERS_ROOT"/0000000001/tags EXPECTED test_begin_subtest 'remove tag' xapers tag -baz -- tag:baz xapers search tag:baz >OUTPUT cat <EXPECTED EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'check tags removed from tag file' cat <EXPECTED foo new EOF test_expect_equal_file "$XAPERS_ROOT"/0000000001/tags EXPECTED test_begin_subtest 'add and remove tags' xapers tag -foo +zzz -- tag:foo and tag:zzz xapers search tag:foo and tag:zzz >OUTPUT cat <EXPECTED EOF test_expect_equal_file OUTPUT EXPECTED ################################################################ rm -rf "$TMP_DIRECTORY"/export test_expect_code 1 'fail export no query' \ 'xapers export $TMP_DIRECTORY/export' test_begin_subtest 'export all' xapers export "$TMP_DIRECTORY"/export '*' find "$TMP_DIRECTORY"/export -mindepth 1 | sed "s|$TMP_DIRECTORY|TMP_DIRECTORY|" | sort >OUTPUT cat <EXPECTED TMP_DIRECTORY/export/5.pdf TMP_DIRECTORY/export/When_the_liver_meats_the_pavement.pdf TMP_DIRECTORY/export/Multicolor_cavity_sadness.pdf TMP_DIRECTORY/export/Creation_of_the_纬-verses.pdf EOF test_expect_equal_file OUTPUT EXPECTED rm -rf "$TMP_DIRECTORY"/export test_begin_subtest 'export query' xapers export "$TMP_DIRECTORY"/export lorem find "$TMP_DIRECTORY"/export -mindepth 1 | sed "s|$TMP_DIRECTORY|TMP_DIRECTORY|" | sort >OUTPUT cat <EXPECTED TMP_DIRECTORY/export/5.pdf TMP_DIRECTORY/export/Creation_of_the_纬-verses.pdf EOF test_expect_equal_file OUTPUT EXPECTED test_expect_success 'restore to existing db' \ "xapers restore" test_begin_subtest 'database intact after restore' xapers search '*' >OUTPUT cat <EXPECTED id:5 [] {} (new) "" id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:3 [] {fake:1234} (qux) "When the liver meats the pavement" id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_expect_code 1 'fail delete with no query' \ "xapers delete" # purge the db from the root rm -rf $XAPERS_ROOT/.xapers test_expect_success 'restore purged db' \ "xapers restore" test_begin_subtest 'database intact after restore' xapers search '*' >OUTPUT cat <EXPECTED id:5 [] {} (new) "" id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:3 [] {fake:1234} (qux) "When the liver meats the pavement" id:2 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (bar new) "Multicolor cavity sadness" id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'delete single document noprompt' echo 'yes' | xapers delete id:2 xapers search '*' >OUTPUT cat <EXPECTED id:5 [] {} (new) "" id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:3 [] {fake:1234} (qux) "When the liver meats the pavement" id:1 [arxiv:1235] {arxiv:1235} (foo new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'delete document search w/ prompt' xapers delete --noprompt lorem xapers search lorem >OUTPUT cat <EXPECTED EOF test_expect_equal_file OUTPUT EXPECTED test_expect_code 1 'check for deleted docdirs' " test -d $XAPERS_ROOT/0000000001 \ || test -d $XAPERS_ROOT/0000000002 \ || test -d $XAPERS_ROOT/0000000005 " ################################################################ test_done xapers-0.6/test/basic000077500000000000000000000045201245427370400146600ustar00rootroot00000000000000#!/usr/bin/env bash # # Copyright (c) 2005 Junio C Hamano # test_description='the test framework itself.' . ./test-lib.sh ################################################################ test_expect_success 'success is reported like this' ' : ' test_set_prereq HAVEIT haveit=no test_expect_success HAVEIT 'test runs if prerequisite is satisfied' ' test_have_prereq HAVEIT && haveit=yes ' clean=no test_expect_success 'tests clean up after themselves' ' test_when_finished clean=yes ' cleaner=no test_expect_code 1 'tests clean up even after a failure' ' test_when_finished cleaner=yes && (exit 1) ' if test $clean$cleaner != yesyes then say "bug in test framework: cleanup commands do not work reliably" exit 1 fi test_expect_code 2 'failure to clean up causes the test to fail' ' test_when_finished "(exit 2)" ' # Ensure that all tests are being run test_begin_subtest 'Ensure that all available tests will be run by xapers-test' eval $(sed -n -e '/^TESTS="$/,/^"$/p' $TEST_DIRECTORY/xapers-test) eval $(sed -n -e '/^TESTS_NET="$/,/^"$/p' $TEST_DIRECTORY/xapers-test) tests_in_suite=$(for i in $TESTS $TESTS_NET; do echo $i; done | sort) available=$(find "$TEST_DIRECTORY" -maxdepth 1 -type f -perm /111 \ ! -name '*~' \ ! -name test-aggregate-results \ ! -name test-verbose \ ! -name xapers-test \ | sed 's,.*/,,' | sort) test_expect_equal "$tests_in_suite" "$available" EXPECTED=$TEST_DIRECTORY/test.expected-output suppress_diff_date() { sed -e 's/\(.*\-\-\- test-verbose\.4\.\expected\).*/\1/' \ -e 's/\(.*\+\+\+ test-verbose\.4\.\output\).*/\1/' } test_begin_subtest "Ensure that test output is suppressed unless the test fails" output=$(cd $TEST_DIRECTORY; ./test-verbose 2>&1 | suppress_diff_date) expected=$(cat $EXPECTED/test-verbose-no | suppress_diff_date) test_expect_equal "$output" "$expected" test_begin_subtest "Ensure that -v does not suppress test output" output=$(cd $TEST_DIRECTORY; ./test-verbose -v 2>&1 | suppress_diff_date) expected=$(cat $EXPECTED/test-verbose-yes | suppress_diff_date) # Do not include the results of test-verbose in totals rm $TEST_DIRECTORY/test-results/test-verbose-* rm -r $TEST_DIRECTORY/tmp.test-verbose test_expect_equal "$output" "$expected" ################################################################ test_done xapers-0.6/test/docs/000077500000000000000000000000001245427370400146005ustar00rootroot00000000000000xapers-0.6/test/docs/1.bib000066400000000000000000000002261245427370400154160ustar00rootroot00000000000000@article{ arxiv:1234, author = "Dole, Bob and Cruise, Toom", title = "Creation of the Universe", year = "2012", eprint = "1234" } xapers-0.6/test/docs/1.pdf000066400000000000000000000342671245427370400154470ustar00rootroot00000000000000%PDF-1.5 %性咆 3 0 obj << /Length 596 /Filter /FlateDecode >> stream x趍S=! 睚+禿g鞃貽煌瀹趣H&2)0+盎>@;W垏>溢藐計 )珆渍裴T葄W挡)v(c馣|[L濡a.9.vq衍 ocj偘fS/ 犓o9| &T濡mC`\i鰺朚鸬厝£=漨斩=S侧儭掇 k庡-攐悆鳥 插T菠024)'G紝爹zI 忇 琰C陿jwmk薥;君A6a渦亅H荌/顠繀晨岘赍暏 !=祹#!⿻k檪]I>'Ct鷀/~⑾H4飦烵噢 !fK,R4=d椈(!瀉V仩gH#Z遉"k6蓼an:u4y靕aQ鬷)+R轻-钖& .璄幍鎸╱L:z互騣w箪G 稈怦l挗t Kv0洃漡J牏旖狭▔=辭t &7玹诖G_嬬躄5>㈠桘<杓!痁臅8y5繌碪 鞑jM7U7\H品阵a餤 endstream endobj 8 0 obj << /Length1 1866 /Length2 11322 /Length3 0 /Length 12479 /Filter /FlateDecode >> stream x趰P\◢Cpw偦茌% 68凛=@pwww 瓵偦{凛G钌棍氋_w瘱岛*re5&[#悿-貞墠檿 势`e錪feeGあR穚-Fひ;X貍e f2t|晧:沮)貍睳67++潟C[{>浮硡 @ k 9 R壻诠賉槞;締#謽0 b卜06 虯6 j镀 GrA薿铊h乔骡忖耹h憷lko&Hp眕4▊@鑫  m@U茖HP7穚鳮甪k觇bh -孉`囎N`=58@MF燿e,#囡1除泐镎8关胸刂莆靎6榋XJ掤處畮C吧唵动 - 峖  )0|-痫岉-,(戝7,6车萇苈d韓,潿刂祚7榋ML(履蓭El裆 $#色穹 郻eer@ Wcs?塬毁侢T昌!~嗣沃`鶽人酊冭醏 8;伡<鴒Bdc榅;孈f`倪轤 涌痦-\:角`笙撧k{欂偔輣y,姠⑩J潹ō+纼墐滥闻蝌x竂^礤燏S麩ReC嬁scPlj 㑳码摭 缈粋鲲墶黽E圩Vhw.+s逛痱朦MH疑邡O5頍徻衅纶韔冏Nvr| 圩璇IV橷8冱疺蒲饀:D纅l儰+菵俾沿)己范償m,kLl7c狨T乛情緾J峬M䴓;v.n〗健"雓{眘q<豝潸ggX樍稁疜yLm8Qn嬝⒖"䴖垏"鶝,铽N7XT~E酒3麺寏觡<垞髸^孵b/|XL叝q威叝钐;鐉%7;笞 锓奴题_鳽″筐5U醟6繎5箊{圻羄m_ R縡j鱗阮ulU陔珕逵筐5y醟耊伦-v&铧'⺈;氽縙庼kW龂冀A W1忖湱窕@粟丽踛"q乮猐璬:&廍T邯tu鹝憚魰m +%'彑-躔!璹*m瀼cT'w俱L╇'yC踏.肩蒘酉 睸*麚/猺.汁K煍k]轵h疖^襝S劗_ U嶲,㘚8G&z蘏W礄i态2DH霶w愁觎]餓牤0G'=De矜= 猪G揻q;6m赓h輜Uo硝┅&鴋 餣牨e>GJ勤懬聁f湲犬皇滙90*臵(Y笤q"w伙酬鄆qj哂嚚氥$詰亄笲H 酬≦C髒#裰t峎GV殎$-綶f榈.~嚝赇+瓹23g差S∥囒糁丕椘i漿c&6鷲 T 窄 Y釮 D焣2H湲-D皠 椹- 蔋鮦w"靴U歹 迒仄.(}┆孺M侨# %戤 *ds W犒&燌f圈lh3mp顓VE吓刈<&V~鍾35Q4dE離橨羊'9Q\D鷁靏"筕zX蟔拡䜩/絘咭9 苪偺嘜鞵x柆鶬?kU鸁i搠叺[ #p0瑋遑 碀 +鳵嘲b稯舒ЕY 轷e惎発鹇務紀廾菄n栞鍃4+>'葘k?q.(錹=q|1C窍6墢d襓% 1'O鷫U'Q6 [秲邊劲竤^cD&z:砊_篙趺莎黊\鏖 迪q 梔t了M繙煍})d(>:孵 I迾9U鱪mX.38呔呐鏈>燿(椦挂躱琏0񳛟G 没檟D噺磤y2;#Q(浶mS/緻=#襆-灺s滼{鷈LdgQ8"囓潅v樐骋#L崨 征.9焷0蘁 冉 梜然K#5鶘=k拶Hs脧 揢=衙D)×X"C菲懧贗凉/W@躎.鍯唨埊舶甎葇輞{+'赹f兓@铼飮巼8钿4!鯞B籀d敕炑匙摯桼澛癿`4o旭Rx炓礰镖秎fD滺]鎥rY圢付r霻嵃^'X窊H|+圾觙,崨鎠,闍嘱謆琱欴磼Z^瑒IGW<蔒32Uh|i_5獠w綠B渲厬 嬪撿fXJ颂谹陖tB-b胭墯F<鶾Vi%W诧蕧Q烩/P# 7=觔gzIC5騙屒*譻P噵璯|6愀S筧u薨/7|蠆&%岈q櫤e莓霫E9貙js:_朅倝)@輓埜_巤)针鴳$o66-蔳懶覥?7捇>s$I4 彞 !^堬P2{酳!v豤騅0'掟 N赬鈜f萠12兲)s或﹎绫鱚恋B烥1ゆ80腎鴴4銵gE覵穚炠-豲笷>nr仩I晨铔%Y 煩}嵹%持讎鮖jdn漒浆甶{.勏n wQ窂3+阈頳騕穸罻終(攆壦k釲R]捚o泮Z1 7帆褉3(I9F峌nH秌唺牗嘃]嚋+歺窡仪s銎 JP絚陋z嵝)╖[X掬4甧@%D垿翶r"臿F恹嗓畒舲啮a r溒媾昐鱒藏躦*聗m+账圓B),96匯皸堄8>ョ朡醼╙渁捂2YD訞Q湎攅趩痴8oDb|蘕qeT .蘖%=8{s孟H-;F~z!9N瘋獨桇s氯[=液Ln┮拰!1Q/!鮺F:骝#7M"料+V絷葝磿%z櫧h躙 L:荽G"蹭| z戃+yH濳$'#l陯織13/ "圁凬煣 猼哖%篤铪mq'[W泘襜ZLx 帼f O焞抢|mfi!蹫攒戣烁{併赓5慸 '糟6If卛T鼟蛍"{XJ!>,m舚伽來*瑍n阬剩磭h~$7vh7招`聙K薺f鯘$埆2掭,鳉 )跴`阻蘙}紛;_U魘,潸齢_]$ 鏜k 7Y丶rrU*C-鱬靨`湞著)橭葕 Δ值 a澇Γ蓁夃idl洤+鵏)N掽筲濮夦 U]硔蓛M;T 阌|BS柘册:w{賦j朂&XV 絼盕斦鳉g兞帀淾鎈牁5i臥> F&xn殟偼 胲:侧4睦梎E鳮畣oF斣34w欄h浫e>|u{傀Γ榎}~Ex灩逊欕8 b傞K湼|=譍鸈檩脢Of﨓U,鸈fcU蔜q焆&?:籠砱D瀸'彺N滗>j洭殷e挹F勬鎶(+F3侒cBab( ▏臝h塅\&X擊Jo涪Qj4酝 V瓭;乵D 飂F)砟襌~挔3屢6+D綑m_R~悬玠32鑘,f:浗艢誛隱伤U-B跲s&泭D嘗X?-0挵媻4鼊甗u鲀嗩跥蜤挴^r9U &s傶苉殶0Y%> 82=ac袝[\C億Jdu覻\`孊" ^Q甮伬$ p0Y纺敻驄`k6[鎗鵋钾e垽]*坰洆kE盦"詍谆Y2撮貝並曹E >輌b_隥餳煌7%|靶.秾孓r起9dU=舀鞌; f鮗:$"%▓^蠃炈&/Z]6両!籗 賞趗褴{諰b噰菉3绬$媺#求A^a^楼闉 [1毙尌c/0/ m凩N<腍w AH2 CNm夶5􈊥s烈餂@h絆U皊捉W+9L酅=HJ斌舰滺X"'渾優;H襀&y君滘ナQ;,脜D4卷飈畐凂鏑;1p歖 gPY~B嵠桫╯坟萣薎)<<礚儖0U 坞?H~鄗Q;鬍#Wr'3汱勮墓`鷣iMlgl蒼>袍玻n蟑稌緛 !D镕" qU垨樵G旄4W馻y帛i?温S墆_鑒贞/ 9:TO迱蕁v ?鲌蚞尵w魧矋3'G噎$苿(R)38忠wu醌飡Y蕯!^r渑C鱎毯湏 審M,K- $鲖哯凊}(壿龠(>dΠ菫梪餲?9╅V爨l[竄:巼&D%茯擱詬劲枻-cD?擼陡,:"渲98椭愍⑺泞栟s? tz}I/B2 !de⒔╢_&盖顳B沕o擞XIgbY樮3:.峷铛@藻,琀挵9=L 5|zG?隁=趂/J7笁 箋挞誂尟顼*聓軙隂@*p嚫T㑇舎┑彨篢槟S 濞碇移18<#媩鍞h鴵0意!簛S/y-%a1L駶咶剩胰;ob磃孾晾!B匝;2持皭彾O醂 uoUㄜ(5 濜沺d筓瓽闣FC渕講牮ó顉鄄軎磅斉W>帛 蝀=]acQB:|獞鈾弲-怚紞鑐U1韋i嵐~兀F皛挅 "W垓d強擖.r.屻肆w%erC鏼晲:酣@#蚬hYUP尭u]A唠^/谼^冤欀%y0+%K弙2骍-捳鸃蘵蓝忱B1耊NXQ89'v驆iU呶闿鸝u~徟渳$l0q.杒g乡c辭M莵5!C筡V蕹|.vo宿Y濽嬂躓7PS誱詥A璦瀝Q芾{珘壁u*骬s挦8ゅE漃洎8-橙鄳"櫧;坈U0%鬤坿愪 y犳" 扼痑=喛y関礳h郲.!$q努姙% 浑k<♂﹖^绦BYo?)]I巭2M萩茇d9辗%DY8ETIW湕鷃P緙cl'<m(卽FzL愽[綋V莟伝g汑h聉)齴^ w駘鍜>R*讟3揔%诼椥粙D6蛙Bd\YQ訝l倢$獏P;鯗鍴碅愘Df韴糭欐㈡u宎 咭zメ?嗧岐#)$*W曾溫蛣窯x<婴"嚌0て"~勉棚8 TL]]窎&蒦u辱8q弉嵚郵成帡栃G蜽w柸毪4鮁:沒 /x踃:嬹畮 b枕>亚. 潭啾k*coL餡/┩冯Bi5蕨S针~1Z冶 8r蚄be剄瞮;OkZA愘rR旻9偓;嵚胱*艍 =J}遶菱沐p6枒zL混d ;頯碲qXL韮籷 刍嫩痸虛Bq 胀;梾圈螅^锊Q"傎韯O諈殪>z蝐'V灊'_刌 +诼zu峲暒陖梘舐. 嵆朤浙霎2>Z%G?/-慧"V餥9槀胹YA d^嵚'歫穇`K.窗嬮`)钇鐌玈#r(嚕禀,>坒;脺雺3MF置屲5T*倍咪屉 =觅-_D]卮j"0確厘-炐糅G-}]l`萬>t0臜鉡兀L穞窠(訛~c揝+萵2塜脙)⒈x襙4岃L鴌);%研@i泽觱業2l乌8!!魌o+彐 )整&&*鋻w躿 鳛齓絝掮7雜Z埚?3k #椄︽罟@籏摜瑚重_倪;慳鏵J!祼+@*Pv描A篌冯颹y[D$纉 邯89p賜I記h~i=)=D応@6甯"婆K=u暇I' -卲祡侁8 0*a罃U赸趕_$h&採 瞣鰱L$oN2c联炁H鱦麪諽f鮢o :Gs "R糵媲艃窄Cb`c狝PL朼d塺5%時ep笮 b償杣偧撙TJF萅瞄]5蘪L uw腹ud0%z 諁蠿W碹5濑户珕嗤#蒯畁靹)t 鈭$喎q;綝拓K[裔)盬K 憀RKY7耶醄趆韲甑cSe脨袻絭jpe輇u蒂燌丂W1湠甏嚮X tH珢汻脝:吜A邲珮t+v4羃jU鱅S尠肈篬3K懻8#Qe(苅r崔缳]$Y驑z礪ā(匭犠{漞U幫{v摕o窚!$惯H\抨0e孫PT'"兕'3乔壢$<曐錃蘀+穣3-览冁6+\尦鮙t鰲6Ys{況&僄寛劙鴺潮騻"&蠏+UM{7&枒7e. 卢謯萩Z*f骐s钱8D!熜虄膪/`禲s貆r贬K境倭^S獑z^ 苭閝Qr糆祩p5螲y烟涢朘鱸猸f螣类8??燋橫惶 `鯯K卌徰頉噪0襔!蟄\+癧1忑k蔸*竚QB<罐兀虨铿鷤cw鍴F o"{k4C珌椬4艶Lc¢]搳3裿哊e鴩鮎L 藖詚OmhN病=1~爥卂x;击釧s紣襡0# 椧I-з)'m濧絭姬Asd/0劍趦皻$$PA摺犯3妩糒傧\ c -%嚊j倿2揓趦緾4熆 龣+醄~;z饦骞Zン冰7&_^繁o5嵪縃N-柣"褥i3XPN@h4殹-落YyxS鋛P镴%Z艶F'}邂舺w)証 邠黿凢f鲒輁舛j潃02rMX;猚蜘捞$萜< 剺帊58Z'迧^7I 蕪兝錎%覓撎談庰潗St!Q誗脆N忂9泃/討怎W5^9犣Yɡ`陌敮w焪(9FW和繅嘾S丿#镣顢a/燙朡7^m輽|7奣`o*[.."鰃 ~骉\0g{4=d帔$f褦淥*(u伡Z冱舌c$襸蒱8# 牓匯碡I4)Q笩2驪*UU泃C崡@晥oI誘渘!暝鎯馂*M翏.紣缺2m/^匤xo0暶烬绚0/nA示yCx+揚穽+ G;Z貱d 9*g$棌瀈<&@疪<鹼|>&4X#&灔俏乹響氿皸嫄黃 c泜漝_宍歎S穨}bUbm恠<攄q殓&璵|@D夝娜祪鞸;o间UH,Mj;%9髈绖犏,荂N\昄M竘@揈檸鄫3 绕慌8^憦~鞲埮}武鎪氜-V僆鬸崯/翻u+%qゑ6Z枱渄箽衵[:8燆X镽改兜 竧"V竻粀Ov|鋛鱉偿冗辬jXk跣,`蜎鲋伫=2r鯈u嗲竉娜9Z觵!椕T?4< 蚝2;詎趍珣竫B6P>鐋MGcH榏瞿麣.骊蟓%+.尛c主0K郺铢棼2T{_碦p髅+鰢僪着鱈骈鏸寡&瘿颈T簜A廘顶$I#郾輴327jI煞<功h姹s尷擗M]M'鄨餡EH誵'茘绰s?壕竦聱o罼慏Jt=緺|KU謑懛辧禤~4Hx_tw嵊F|崓X ,d;J鈙壮 生vT霋齌栽z$浬鞜筊a忴覷`<禙I頲偢雒榓P姈m:Z > 璎:儨M觼70l辛嶙癜蒷e牝迸(溢晩yDGJ{呴蒫駋棑躕6>5蠳5媏弼8R餧 1|08L7(涡/鶋叏 VF1W偗l椐胅~ 鸨筦.豝.8肪l{剞<]夘坣&) B壮⑹叟,肷戇訾h髃)螐i-=塆 锺9罐'RN:豑嬸'嫈6#3货瘴#ar餿叒A弬瓶墘杌&|P罌賵叿UY]竹?%蚀$ bjW埛N慉u攳q咜fs 佇攪塷2漺J$瀳L锯"/ゐ!罅 閈1l 搗嶽疋oyG﹍3=阘 1KaTF瞯=n?O|@C溧#7老"d$-臎yG墠棑抄 &Y;餮怜I蛯 R^_N蔸H<脁H%攥[蛩碏繂▕豹c'&rQ\悘瞒痟S5畱x4扺舕{= 燑躶艣榱ω=??47玶脼:N-/0 >訣濡/哙 泔.P"wM< 8彖跧pv侯O璿#愌- 蝊6粓秨2諵楨汆;o丛辺 锥犇"Nm魠統0甴 ?T迟飞耚$貄O 钖醏v 1樔5hd邬O骯箒g 梖讈撋O瞣垱輭椔e返+嬷砓69煘38樇v2舳|WxG癉鬦i.H肨 諉d駵灸呔┸q)義藷鲔L5崎;g寖蔼竪71栯:? 辜JF涮U昬鰠-SSS;葎誫c聮eV"},X!u nl!\)ON乼峰 砚鲑g<Z躚#kJ葡V:Y皴霕蕖dVz穝'埯N!莯勛?襛]Z'E7#-炰a靏K澶髮!ペxs疗园钪J4鮿忂9髨 x{`轫 YB{lf(?C4z~熯cwー2A*巷h-仼傢;攌+'<礔!璶S緥懚8_64h椉#葞 n遾S鈭,Tpt晩娷頨笡蘜騚4樍渾 0李洈!1祵镆C?槽40)藬蓰>瓮 喾サ赝 C稵s稊H蛸yVV(Dr%┿諏O馪僊轨秲&G睼媲\G誨"縱y( 忌岋T%钉Mj撜鈚続o8她@<=哝図L導 拉Ο}1I灂苂极%4s碊睩熴哎 {銛泟祄苅勸狢芀A蟢姥:摁h翠54侰0n緅秚睖钔絡]爺c;KBOk2@茼)0郚痖 玮註頉蹝 撿@|醣- '砭z婩c簌v0e2i铀uvz逞N+qvz{誼?tv篴]2驑踊2叻>P⑴v副M)猻3愑_=*謭疲M0跠餮輽蚝e成l竦峀r d蒯$v奆 7u)萴龃 b袜_窴虾F誡 辱U#鱱?鉊;Q5:痴伨綪佅艇&捦)ED燤⒕цA霒蔧y饽崋恄!i?NQ4c0 b酾0籖84鲉o懶 褚JE0渙垓v/Cw紛飀/JhtZ (遈浄f\椁E阆痏-英荄V彛^媘](滰f}JへW牚澔YF_姺庯?┨鼛匋癿遺袤=,1崗#HG; 蹘蓝海睐s琬 ,w嶫馛拒瑀 憌4tq)/]J2A!\蓊B硕4>谡do酮羫騏,%侔dDIN岧費%晼3`z/r'rW瘇m,~8纤{絝蒴砕G熻丱庍 )\剿遞E;]H倠.:t)鯄銠覑@崤懥曻柪9粨秱9麱cO%_A?p.飬#鞡Z-%豾俿妁}龂Ohnt1忠蝁Mk汘╧ 胏 禱缺鐩AB1" 潸j⺗A3'綛黵7幞孚-fj1P=_A摁▅k呰傚{]敐d 終qb莬* 隓E魷蘵Er}羔搜<Q鼟鰓 IC錶,韋DC`塂"h!?]喩繻儡our幫#b-癚&瞓*V粑7儦[2両砬d虦6勭#-X駲'磛焮驀ㄎ 廧h庙蟏臵.`蛟^'京輅F<饰绨'-咊r 灇2./6utZ"艑z.穄權4虀"縩焦ǜypb@邇97A4邳 鯳鸤@ch}a◇ 術l~鬊V_[薬J_>;卬ZxlB鏂`骪紂`up擁韣看D&餌C8Piy@^z桟霆2谝Kf4瓧~a 緰)鎍y贕晩躅X/]6塆?7, 9)氈7u蛛7窨/娕ak錪-~瀮oG踖6x:i:嚌md劆+k祦oJ淡綫匱T煮莋摦勪讱G曶=>G <鏦瓫諪遜IC)身KzTAx唨V;q哔~!*齴)#l'閖s栳 N#峧盆縃醻Ds#u"F嫼^程T.~[_<贀В&伈)炕囗2}p7橾AbJ俾痷孶峯咯暆q互死歅2纰f:摦>0鍺E"哗:k餌诟踨O*巈革愳劭(拤鋂跖u瀆濿驽魃歇+笵峥掜 1鑮7/A3 4Y瘈i户0薶捝0Hs7井K妦Rn$'.TeO7 蠗宓耭 泟繓?=斸視载N譟泯.q燊僷陎hnt⒎尛I懸愯i$6呡仍g$b涘3骉w掶;丮dr-忩-=;MT乱l簺vjrbK蝊舯軟嶛WI/b鍍/g # 嫂J緁mX=?n尝*炵2O弑(A載B个"9婚^6禔8j} ??涻倅鸷R!$秩!摍ra/椏I '鷑j7閅R九鳻8咝秊+}=鏒<#e稚H塲L+疄蓧\庾䴙Q-箔╯h格核鳆犉_姸佗`x僊欴k!簤48 %lT攂拲 煞歼6]S熱< E鰮髰q邱j瘇 e4B藣搩/v3o<释瑞i漆p=|=庮糶J昆z譗J聃趽6雥珩 釛烶>錀 耇焳嗉篦U641閺 潝怗荪,壒爕噮<絷/ ]<8i綼阸頲帧5耟l1賱鹹g мf磎'鈀珹呿 贩1焙X\m?2菡/9甄)=鵵鲠 +ㄢsP櫞O`隮 F:根q粖鍢u]蘊⒍汱蘩#f ?25=赼{r龍KX=H譫W姸;鲥頣捒瓓DQ,虂?z^噻梄Y@R.)噐画:9n栕S'4邭C锵_虛颈罎軁t菹Y灝羂宯0"m拨峓Dx蜎锝鋔bMB淚胴妇兿飣讯坊榩阪_k`勍K+kzΩ斢汈瞿苜9匑盍;SWO喳"衲aH;s颪/廎竛丼帯E姽 描Ф诂延7z丹緘e.=贌Q呋站鞻!p怂穭J8⑶6zO"鍡l閷-"D3隴^LDw/i醗廷S緭g#儎亅|/你闾K^2馮C嚏7/鹞~嗜釈喁O8.m0442aQ;詒m+'73畽/ A玖?錼锆卥Wm莦渀b檸蹰;慉狵-椫烚閌鳶鲻潵+ endstream endobj 11 0 obj << /Producer (pdfTeX-1.40.13) /Creator (TeX) /CreationDate (D:20130413212405-07'00') /ModDate (D:20130413212405-07'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.1415926-2.4-1.40.13 (TeX Live 2012/Debian) kpathsea version 6.1.0) >> endobj 5 0 obj << /Type /ObjStm /N 7 /First 40 /Length 557 /Filter /FlateDecode >> stream x讠S蒼0诫+尕噵DQ +磆捄v0|`dVRaK$蛇wH9^赾$8藍硃F:x( 丠傛 $Rg恏屒>6pnr啻畓[H"X赴]給3沇苜Mi& )'b%;kO屒)(8Y,8o雔i{XQ衁 鴃_zX;蠮+陨H鞽轓+A烀=hb侸Y<"0iPJ裑I隊#榻d$I1閼d鍦隨[(S顴Г|&Τ~n蜤i2埘┾[s8(7}蜒賛j鏥uOq(鏃r恿J8酋ョ訮< у]X endstream endobj 12 0 obj << /Type /XRef /Index [0 13] /Size 13 /W [1 2 1] /Root 10 0 R /Info 11 0 R /ID [<3332CC463BB586FDA86A16786A1D4E0B> <3332CC463BB586FDA86A16786A1D4E0B>] /Length 50 /Filter /FlateDecode >> stream x赾``睦 @殔唁坒b&F 631縛`4飂r endstream endobj startxref 14219 %%EOF xapers-0.6/test/docs/1a.bib000066400000000000000000000002261245427370400155570ustar00rootroot00000000000000@article{ arxiv:1235, author = "Dole, Bob and Cruise, Tim", title = "Creation of the 纬-verses", year = "2011", eprint = "1235" } xapers-0.6/test/docs/2 file.pdf000066400000000000000000000340331245427370400163370ustar00rootroot00000000000000%PDF-1.5 %性咆 3 0 obj << /Length 645 /Filter /FlateDecode >> stream x趨TKo0 诀W(9~$住+v葀vPm5% 水#E9K;塷}鶫赉襻胓RV嚩瓔鉱!雧杖]训MU7}q媉鉀覂妒棝] 鴾薓輯fV%j诎漳R杏ge=關暃f讏鏷嘰6偀rM稖湤歡,技U屣阕b#mx6U觮 骾稓妘絇|X峎懾 <⑥, 矼棓-A3噘l=bl跱6_剚1宩仐R^!:慻;陇&a蒉 q⒎坐v猕>4#%.W$誸=*N颢{c珢檎n5\庆硘.靴4柔s激0~ia$傍牼趸螑峃∣儺殧>*9瀑填N*&襍镝馠功墩箎樅8 Nl\1>睴毁砪覊>2bc羵b{倲!W瀎D@痴7l PV醻w&州^糱h鈂垛G箲倆兂i(俴戜u 賶/-oj囵7Ay煫.F璛櫄|窱碜札U5螙q畢v垌M『胢.阂娠F&鄱"嗠|枍瀞櫑Y9僓5歰軟9獺y铌闽無>H∝並鎞経`/f55擤刣G踏=i!KD+&2蘮|].殗N?B譇&鲥欍疜d得6l歠_韊㧏%>嬗嚈 endstream endobj 8 0 obj << /Length1 1837 /Length2 11129 /Length3 0 /Length 12278 /Filter /FlateDecode >> stream x趰P谝 w0髁輦CpA`pwww n5 !Xpww 8r9鼷*樥瞯輠 j e5f1[#皵瓖3  疇势9X@vdjju垞鴒32&亓bkq0乳&rz峉暗9[8l茳l<麫 潲O牠?@1(鋖m缼仍舛v3sёc@gL`沣鉧3 f v僱 's磅雺 +毉1熹?t傛NNv, kG[3az&+纳 v;竴M(偓)cAC搏贇:箓纮Wl泺氠lcvP擙P埸&肋氨褒楊镬? 6&儗峬@63) P掹棱滏贅瞨磢箑 V W?+つT燱藄4v998B惾蚹%mL膍6N幦'q慷轁醑浀幢u雕楤lLLa鈒仟a眞薐騤Bfvp丂>Nv36g齼^葺擁螳oO;[;楂7儆898兘=眸_勌0;尷f豞蚡涌痣;@豪踪c'阶2钡眗'鹐曌話砸`麷}镞酆<98天\l>^^帻,Z旳惪k﨏(kcj 帑K耴稔#缅铹狖{c{偄黼(tL' 续琦3哓硫6 )g+?輙 k垥瘬祆艉 动籥C道m鈒讲N犠1锄o!嶳7皦2纳攸-家[Al朗稁?3|fl8巨諢.痣:憭6贫&;7溧rG編;罁韚AM纍N6暸浦5*`j雬峳X%0龎x8 ?5R窨`UqX5嗀V"網?钑盆`5/怃|E瘂﨩a5d皞偗諞 疽C㖞⒐粷鱋墨 /鳽浾苦kq@蹲R䲡佻翱凭>r繓f鲝#vn/-&偗: 緰骧/鳭圄/S磷耛⻊3莆痮鏌埯:8>衊y~种X 娶6瓼屇晊{\h妟[3=崇糃圀:b2}uf廓脥X騪嬉$莸鶕鏰K=bhk踥疓儀丈6鋉?&娬鯎!2珛顇=賩i鶾露@w蔘缵;螈+玢芄鯥徽鯒-巻蘮Ts耍<桚d幹堹鋉 攫俗_Kv籵冱4k牻c鶍篤蔃&@缶9O__Akv足褱攪糮3貈敗S(潲@R>s(焿闼izA礗B碻撐r筤╕o";㊣7]i賠/%\ 213Y暾l谳>灋|I厉樖挜櫒pEN(8u怛{$鮹j,弲-1?Aㄟ術鯰猝:n鴆15晁gQN(N襳!筧鯫sZ粖煕宯.c谵!"懀X页Xd鷯 yS9q 4-滅v6堍坠腊osc跬12,E险 Fc渡怒趱F欗鵇['W/+H蹫s浖"\"峲鵙曬瓒-y捤xeX 8SUd鄹綰N!蘮k4嗤1I憐1,F菱TD确砺 誔鑂蠺裸膽n鱏鵽蛙ZS玾LX脂抠wpU皦hr2!芇%籸曟0#髙hG1)Dh^盓#naBM訶考v钼搌,>IM墀M珃5Hl*莢枍L√汽胹,#訷"恳s毹岘h囝T蜹60gsL}2z衶匀!猺蠰 扻集_V%夎RpE!醴嫍17獯酎枿S唇vH旔鷓瑵,A%u 輀+椾o秡>h玶眏鶇*?hr頑7))z壿槍k_ 格.iSp,Z2ǎ6襏曣e鱞psb猪<摚鄍鋰3堝,將|.抰琖湆伯7v温瞲/6u)J覤2鑆祡8阝a3,腞激圃^媦>)愛>戓#藣檯涨\嚪鍺$便"匩箸箢4$Ri畟7鲣ц5mR, S铻劌閎@*>舚F乖佂j渵n歕屵q(钬攙56膩W5,闧(P住剓4錢〖塾9_螟篎нT摞゜~羜 斛H∵忨蚒4g缟p您%3殦搫炸J皴垨8V~眹c朦b箳鶆溫`Zz]话~16烱蜹; "礢骉繅"錳j@A覈)惇[n/E穑 E* tR唎Xz脃.逳.乑U縿 庡痎#獨腕翋2}Nos.p鼕宐楥烲!揜"7鞿!漾朥~yHr蠯垻 iDI=麔6s]匼C忧o钰獺%zsK,崀x╘轻P瘫旯 鎒恩+硗姭Y:`顧峌+S'F*L:S很r瀸?彺N溴?陿,1餲哔F嗗鐙)<(E0踻莜v侹校l( 2棨Vb欹q權L2(I"q拧}誔7 Z秗殇k#1爣緹&ゲ螲8$胐V圓歩_P櫊d72鑔,n:摶虉楂醌忮kлOc揔G%I懘媺Y賑錑|?牓n9O鐾[^'瘹绷dVX選E3抺(鵴缐i 噲r (懽UN錺賎慇x遱=3 ズt垝wK䥺慞~nS崾^/7厶w丳t蕜耦蝌-)转\E喉z纂_{熤拆( 鞸}&禐厪& 汱?E/5苭D/bx碶踐y马U4:逑檋f鯥萲>T-mg睛|汻n横蚬骷櫶巰7绤柮茒楃A tG柇凾d诼v槜煏.J6[:/gj;b(3歚 c^m┟ .-麠w“z哧6疚m廭賝pEX`i臢mE;别N G=#硏璺烟鴚Y*狥韕@縍轥餺㤘sL2孏徐J>攧神i4$D熃肊_LL汜〆6湩偒甌pYE< ;厉 鞁G枝341誐t炮 蚍 2檹I禎%-6>J瞖G\n+C 鸍:堉6訫!N禨广猂検DB,=晗^嬀ΦD垪水$煫L玞{D徃踬簟z妴tv3/硰杠%榢媍F埰熒[.愕堎衹呸h鐝灣5@穛勰搖+3+姇 榪 k榛崓伊基作,dK^p%吏V坦殨崙 JJ轞瀆H轎G8 秣N篞 烦+~^R噠萙"圱糟鯯5軭軆o婸摔崖笤$檯狟X4xA+C_+n`MM2F鯣踗K曘蟍G聤:嫚俸Y]|5_芤{悈蔂/錗LN)偑L屿菍饃険Bl蓦cq枖欚F蕝Ts@.鰦璑@陶莯渉拆癥l>奶巭S岝F?晇&f畃祉cVRB菽?%Ugp1粒鸲tJ訡祬蝼j& 璢$|諈*g瀦{矗; k+鋹坱姤3">噭燙鈎>Αs/E-q1\山咶冢覉忳b F3譂塸P駴粞攞4r持o堭蠽彾.匫9孹u颰=匉↗4牋涴涓UoF陾FC]琖蛾ǒ飡踨陗怅U)媨=./ N[>]汊罓 縅筇%师蹺uUL籝[鉶"~瓰fd)6钾cD掀汆鵴餩K[膮抟鸛&+袏E/ 嫇懂*鑬?鑵榺x镘L琥y0#-莯q:齅-鑵'6`踄Q岒Uu堺春ok>恾ユKd!廹>剼4-br尦'驑鱷觪愡仔.K, W粀5bp旽銌j獰u嘈坎黷U\#xo鬼嶴畛,C筄菋>∪Sp罼e%x媌&皸T0%鳇曫[妰 )1:P蚒破蝃+襻鹗齴 d%携$S孋D&咵3=:**捡剢椧E cU矫ば|hx\塎躍.椺hr于:媵櫨1呷3 a昄9閺睦娬厶x吃;!蠍)尅4啻虡0蓣zg瓗〣 跹鶰MY呁jS喺 <腝爿}猎LU1f*唌宊盿鳁)l汏E◥P懎棍α笵Y嘾狤0-蕑Ns篳鑌硚焩d辭坛1sc涪wt轌剰;( A蕰_ O恙抿 =<焛32密P肙惪:%)οw彯Z墒愬.;f :1jF7Fn|堪蓫棔2D险t杶蠱k陭vd06cmi闞"位<2倖僻n鱍4V+pt8(耤譚无狉〖帶鰦]Z緣.姜^鲖谷偖u皾鍛[A* 蝩G闹n"閸靻斥{ #韬倊M2( m頸p簐%枫濢XV1茛7[鴔発藂⺳X颅3|羘嶃`4歿丕/rP刽4玗 嵩$p浌堥熰:宆鹭塛|(.攞季梁蜳豕/皛尒DrQ逘菓畘苘V&て5眨嶶r蝼峬俎$2m矰苋炌 ^o 鱪 3榟|/皆楼-Ъ燇獻彋楪@5蔷1H+}熷wjH'?╣谈珑?T8r逷淡@li雇淃46魧`;@緓si莳塡蔜ó踾塒0? :ロw圷64.臉瞬飀叙搲织℅檦狎Gq:拜U. ﹩'sTc蒬利p扠@NT饼BnZ>2萈i运~3茕3|奘8>1霻o+怔毳譕諊&f(挼j訰淾3 縖s欘W :埚刬34+I#啄︽钯 F(_+搮:m+躕 mgr茳li敄稦 0郕错p疖茌*DuQ蹗稥l/U褾 ;鶕*^B畕:FlZO 壟模:岍rb盋u泓暈篧撙30蚦MK!BmW&.鲆z8皈e苶苨_餿4觀t咛筙连T/藌V讻{.菜;0M(皔*+貤Y_.[9︿ 7晴昸鑭,〞獴殗唴1さ瑨?鳲娎卬5溳堇-菞襱G;_C裻÷ 垶堘3岤N氼 jF冽觰C鳃黳W7n騉L(廯8舀淭苭塠~$透 蛠e2i D 裭 92n:睇甑c?藝銟{碓驶烹js睐`芾z猄鈅aC[E軙Z說Yv自)  啐# X苂Z矉eo訍曜.﹝L 檅咃邫Y-'液絎6,4E佈涚2砿 E"+僠誛V礪槨!匭>om2軪U庻1vF臔撁w.PR蹾捵 骇,y鉙$H6{v赏 瘼 既毕鋡吂~F瞤焉?郙 3"2r豆蠄栢g劈x?sC捃瀟峊A苐鴪>OQX天Uz┼缻dB卐}[頩&蝘瓕>Γf1d构@s闖D6$< 宿哗彽a6`豿L.俇持(m瞽穚W攋`炵C]{\暅镄-≤#惨^4 *dz靬k貒縇`q滅廮\贴蝑\缠#0堃`tD隂噪2襔停萕\)礇鯙Lc郗b梞讦%%腠髂k.砂锭廡0釀 稜怊p7F炒H`駓=咂[媖黬hRqz&偬噎婙竄啀e1恀铳I>#.堶<耠劺vk鸄")@5S噖阷进u矲揕:饻膧08j縴BlTtM釨3筀(I怗寞1閖恒-绷皟嵽1椸2|髻k暺a砧渡Y葚魗94IA阺褩蜄┄t n<僨5嗟k侣糯P邺B恥n 蒩"h e.刃ZMlHy*&嬁"鮯Y淋氎#樄'Q昪oH o聋H∶Y憓硤Ⅱ1]縌褷 橹烏穳秺"/譂埃逦r.'僡砗),"帡碣靸9GTB攻嵁絾 2Ea謺娊縿P> I G佉0r疄&%'嘈2抗忘'aл(6舝: 蝄-H岺鵣催?o樤欸辰傾(鈒嚌橣b<~ 連 t毨w$2楾Gc<:DQb蝓缪粯#鰶@|? WY韽夣;鋜樏橁G扆硒d眓/x恍僃䥽Z菴佺螘r鈚a`v2茾&\蝸e魪砲羸巜姁鲮┢{¢哜+r+怮cmy癍"i*\掍Kj  约 75铉紤=zo:藶囶硋/=櫫蟚畺p齧 +i,徑e~赩拖9跴鞐悁磹$: 瞐巚蔰w|tc扴殭b叏蕱,x厜粟润莑R4e陼sg巣^筵n>蔵'dY刳NI畖>f] 讟赏尦G"鵰巓7EpqTv%┠v$,0+秗諨M媉埦.'洁OB弛籫媗圑*帜誧)#%姷. 7纰J{ómgn 覸;W 膡薳岱掶(欚{4Nw=匓Yz伒=#齹薼綛钔SX#苂抂辽铜w 贞及PY苏并箵e堝$蟓F-iy绮燿庿焜.鄆谣:|韗 8湠Js?頸$y2増k.*:6!;埡S鷐羜嵆3>摭C軪L酂敊5 湞啌vN]/> %@{淕虜ha^熎貾他踤狵J駧( 鷔5鏧紛 衎貪店嘰t鸵⿸ R頰扯)嶵.鮳薒^{E;N\.記v)a&捄鉁l梶.&4"2 踗猗{.'dZ 棦悛2毘{=塍饄Q敎婾 K)s\疶l".澪M煁< ^眧+?L瞧9j5&U>7L. {曊 lw滧〢]!5=:g7U偔E@T趵襺対峕^(煙V=6x[<蹨篓畆Wr暗勿'Щ3阐]轞w礫喫夡%床$W拑wU''鲽"0W 娦C%z 7谷脁x 鮘皷,$娤F頣猷嬆乪'蚭咓jC蟏鞨pE峻<痴+0'捏r胱A"lZ0v捅0or 蘮sZ柉'舾h5+エ VUm痈橽k驖*玚 S7@)振 Y' 梭0趮WL熪衋"軗矠珏:iY攋`N+z諓0;e裢笇MK<妾埱YY~ 2g敧▆}赩嫦 鹣叧$#パ\组悅:a丕袘ovxBi[n谻*V訳W褤擽J皓>K'仰睻EZHE淌:|U2Ca蓣d罒熬N洛陠禝f藊j亻O2W⺶3畟囤l@{&撑玣:雺專4d鮭-Z 焈婷 襾+H酤藃繃3Vь)籄橜琿5]燈郏'-=0晄鹱U鉈d︿劜纒4%,LI}(xswb穔T%榳繗L憔!ⅶ攻*鉳6阯搗u)O蜚,}灑gK幨D&榃{pS斴桢*鮾尕8&捗S厼@ⅲP賸+腲薤-;瀥仵鮆朸+稴/m皉 {殐jz咝Lk阖妔6 樏坥G/2萄}&潔F百f嘾膿] 筐S/ ;8≈7Wt2毮7B鉫{6ó,k梞経秹: Y晁眙oA+樧"K∧p:{k眺.h睕<;亘q,芸{靀猖産儕,T凈R(檴┲郗唨揟宜桴硄@h嶨萧}~sG^_黧抚 奭及~0矬u駥每苾򝷛 B囷(恨O_蓶.侪a99~蒒DL3y贺t$#&痔鼺銐*陯X<堖?Χ苴百O鶩噧衇袾d矑Ymv]迬渾 jy<蚱凙b鬓t圛俸;$粒'3?r澯袔檰+語4_$鮊邆).弪珃 m容e品飞鵦;纟F飀./ 窟>V耍膵略*5喕 F_+綿掉稆川>3q YP鞌 b靮旂n厜4=鞝.虀驱涽6yYT緳P(驉硲够{B-辋萝 凲f桟メ于2sah=秅?嫋輇t}嚝`B=r麐篼,C罙.!"惕鯄-G脤灍檓S昰@+耮]Xr =且蜃穻龙彌!簚羪L鞈鞋邕Kp?湇懪 盔ozK嚐憫\(瀠駾灇髠{:猘)?k=)|:F嵄 簷^G]裥h=4穤*蒄bQ%KЁ*U>e\傻庮rl(蹀勂砬鼷(涿懅3肺搪=p~At邔K!蠻蚭龑兩蒞Ox杒耳榣3a硯鯊 玵`wXU⿰鱹cp巪颕鱥p峙aw謬! t85%1:u-释;掫嵖 蟀9{慾n` e鍌#W}S琨H&+?TK"\cjP-~謇y≒0唚卌%籬彭+6E3詚i嗰闔%暼遜洖璱P8モ帶 弝倰貍胩论R檧~&8懥w蒨孟 帊qV传怊g犊.Yo*G蜡時$PJ*1泝壺y?,`a觳飋y綊秝oe跌蒛箢w煙v?鱡 韑[$y蔂魒歊l+2祊S蕍-"x鱀榊=芋 〗匛姩垨湉|轮昫&涐5>婌b栻姲;pP鉭;慞嵕*険䲡m苞"sザ" 鬒J 蠲h糰辸?雨岷儸綱8(=狄酝袕<駳;蛘掕妅眺3YD馊~J垘`奖T鰰7蕖晻A'!*%0咇~裼:妘Z;T-堉u钋U裚4遊r耲*8(6@W>镊(+!z蘒僛艔珗瀐t蝲я幥嘬鍪庩DX$[$6蕲㈱冴6鲶$殍尮蠆e;ⅳ(5p嵊<R+ [| 穬L臏绍B?魜?zL縭2e歉藬鴫郔Q,9:楛Pl|醾io曊鰹{L赘9枦歀gL鲇犠笅1邓斷n庵貎|M,9bl圇)赈&wo)/E莪'Jr鷽`o報9偊!)> 鄱軱hk&}煼孜m蚎ワ笗. 9籲GT櫊A湿爻[/bi+律%嬃A姫r$侐紦8+/慽澮%匞倢峘旬烎菚V苕m燊>紱Da>躋羉`駟K蘭{'\髞>? U瞨 涄龖彊t~V恸яj!E剹Q吣祷/{cxq)龙虜v诨嬣8悩 朓猜躼W鮺[4_Q濈N裆0箈ZF 絞$歡8=Cj巶4SU-腫4g賆? p&酝蛻g团秆qv怌)塿籈{媧9E洋`焾■ ?啙52.D&)!X- D ,Lt?壖6惖燯阗?丹[ZV;~Cm媊饎炧Q<,:恎φ(9E皣^}8w伪廖w;丒D%稼餳 瑚虘\姶d"(竹=歹+&v狦 u4*`鑧 c0迉g^筶d/绋E匥b隧W俊!瘽6鍞銼E $c驶鋂鄨6T R湅<9楄嬗#_fV匦E񠜝.#嫤嵋C匽$玔韄茖嫟- 9.嬀820抡7<W鉸矝9旷vd气纀杘戰澦k魤Ie勣#'熓i瘍腘楋嫜7<螓4[腬闙b`q\匬旜m瘖k &$诫蒑 佁呯o~w/.P-崙;娳,a筓餠g:煨n砩C,斲$ /鯙%\摶X墐倡[3橄9'涂涮褪T2>W\9DU鰵~$┃熨邉y\ 旾袠糾?hb樈烷{竴y.$墙跟沗9厖8D岥c<暉臌ASyi蘷/ sq憏/D 藎/佥x仜.o位蘲(w霓嘛狶窢u榌 Q疝敹缞3z5_绯犿6 }遉沍巎形 F`$tSlㄥ叉`0;示g0圴5染狛G襕鄑>y:薣h?7o葩≈枾OkV剛! E.>\魩<劑$a|輭嶤朾悬;.K&务睒7-練|泮諄'O乃|"萶億帲翻旱4憇镼唘U岕軴烅6瓭彙_Eg題d嫧ha鉘鐖`9寒51O=/}lH =匈[d摀 贜i测9U嗘G-几轓讅w雓啃2赈g3簭G9茑牸堡雺 ln0凗孏鼓 f毆ШΩ:N浌廒*-&*84哊(pkㄗv0_骰K鍶PF#h,>/j鈹?=+綧(擾5,|筸^B!FUl=脘U;餾6k笵歴+&麾Kfy尥#5鼣c+饫"&褴樒wZGc>艆具_諂?鄩 endstream endobj 11 0 obj << /Producer (pdfTeX-1.40.13) /Creator (TeX) /CreationDate (D:20130413212529-07'00') /ModDate (D:20130413212529-07'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.1415926-2.4-1.40.13 (TeX Live 2012/Debian) kpathsea version 6.1.0) >> endobj 5 0 obj << /Type /ObjStm /N 7 /First 40 /Length 553 /Filter /FlateDecode >> stream x讠SMo0 禁W鸶aHhI,A&Y阿k2 攘u4跕b{3~敎鎐;頯A$遻扖 攩X腁C,園1磮厾孎>7p炴&繧]u﹃,X0>礄堡f[︺ !9$%歰⒎#q嚭柜3冄p芺JN污:{2)閠鴏:8覛'椕-Rz(雝*_"欝KN挲賩陠月賏2$\ H).uP( 蚺○L2FH=K|桴蹋6閷禖囼d腪]=v稖L$bW麻频^=名b&8g+v錇P}Lㄐ煯荥~妋.谬农盰[6]蓥1軸鋨国簔2yX0N⺌歔坺镭徣^71$qHsrk3?'殌摯鵩始 S]0茧襗欇V@郤g鯧恞?帨H0(抑鸵'溾>#务棙+f樥鹽奫4s,皒o Sa;躢5謺訓a;<囡淆+f%%W#}n 晅x砑闇T8灯忺邖筳 恝_仝螘仅{z4SmWX谳2-祅M攪r迼[ k顎沰$謞蠸螊顐 endstream endobj 12 0 obj << /Type /XRef /Index [0 13] /Size 13 /W [1 2 1] /Root 10 0 R /Info 11 0 R /ID [<0C860A0B723005BB60821FB8FF0F2E89> <0C860A0B723005BB60821FB8FF0F2E89>] /Length 50 /Filter /FlateDecode >> stream x赾``睦 @殔$ D1#觕堎娃3a}u endstream endobj startxref 14063 %%EOF xapers-0.6/test/docs/2.bib000066400000000000000000000006171245427370400154230ustar00rootroot00000000000000@article{Good_Bad_Up_Down_Left_Right_et_al._2012, title={Multicolor cavity sadness}, volume={29}, url={http://dx.doi.org/10.9999/FOO.1}, DOI={10.9999/FOO.1}, number={10}, journal={Journal of the Color Feelings}, publisher={Optical Society of America}, author={Good, Bob and Bad, Sam and Up, Steve and Down, Joseph and Left, Aidan and Right, Kate and et al.}, year={2012}, month={Sep}, pages={2092}} xapers-0.6/test/docs/3.bib000066400000000000000000000002311245427370400154140ustar00rootroot00000000000000@article{ fake:1234, author = "Reed, Lou and Bj枚rk", title = "When the liver meats the pavement", year = "1980", journal = "fake" } xapers-0.6/test/docs/3.pdf000066400000000000000000000342101245427370400154350ustar00rootroot00000000000000%PDF-1.5 %性咆 3 0 obj << /Length 547 /Filter /FlateDecode >> stream x趍S=徾0 蒹+<蔃鞿x-zC嚔]娡KT豶獜%E遐橠=拸彜糸x,籐蕆旰:;緁薞6Y叩e賟蓗妎a]U^4璎呇 #陴╈迲z6F7u6娿蘋砂昴幇篝~栩猣崪您@!6幈i'鳖'^堝椪帢f}w聩 Dk泈双劯氮/%,8噜磑鼈t ~傷鮲V髀煅娘帱蝀|Cx.:~8t濾導亃綜衝踓蓞蹢,/赼(噅:抾/敲_r1 endstream endobj 8 0 obj << /Length1 1852 /Length2 11327 /Length3 0 /Length 12485 /Filter /FlateDecode >> stream x趰T涆-孲軏C(.「Cqm佲囙泡w+P"E 艥Bqwx圭艮憬1捥%s鱖鸎h㏕5X磐霯@2vgVN6A:'儍泹儍 晼V靗孞 rt跘 恡;?郅対熴旍 '7揙悡_悆琅麿牆 @守lPb(谹@Nù抳鲱巂 K珑e繾鵜﨤堐力挶%撒yESc啙)潇㥮 B栁析傡靚nnl贫Nlv",7俺%@rt P6 爄 v產g铎f<l捆 堄s x^!PA V+瘅88←;"0湎dcSS;[{c;b0踿*2妉蜳g1撵廆c'荤|cWc皪鄙s罒d恼葡栫d秝vbs埸!扆氱]枂業谫趥 蜰'v>o;鸰'k 眘儀 塘3?D樄爻kA. y┛C濵,@^~ AM-犠t潼命塾尬`, 6=爖:粋螏. o线峆99f`Sg  A龡 2 ?# 秀x=N窘n/3;垗/粠并鈁婵銚惏/囬s欜做包: 抭北宛6鄹疖.蜗S< 5蒍 3皨z鍧崯ba笙6倽d繮悪*刭则)<盂! U;'w 晸冦|象fj齶8=熣.畜8捯S;?鎺嫍`扈h鞄蜀躛\技O午5A靗;奈9,`n鐖夠嘏0齾鞉 ~惶縃喇x雯䦆">祸?i/zf1歃 瀏Wa|Τ鼦黋ē唤弩鱫某 |7鳿o鸸V!鐂e縌=:惠繈=>?~s?Wjy!薹鸐隗脜葆7鳿笺o鸸Rн3侒o饄媇~兿吇 玌L]煰?圌箯䞍集A (un莆魎怳mP鹵8胫橉頄N*#渃囁-&rcUf缞悭x倚炫 i 眣辖杬湫諨刀;瘂脁鯄6過鰪顗柞懀惐j妋{=8xi鸞梅纕*墟:`鉣货蔅膪FBf对东犦桵睩kE辂O渔檇}'z呬蘆䝼 5}q9厸3﨑蠈杲蚞洎皇s筝c閾&桽1 .9钊潷腘岈gI褗鉎树8mv<稊寨wO 鑘垑蒯皴#Zl嫨恱矌V狉-&N,* ':^u4郪鱩N澢2A韀uK偆:JHo(9lNfX翓爬}4岵䜣橐~C3親毤ッo"持wI搄h:(?}⑧1婧涣d|n歛走=阣fKl汥坌 糞輾#吰L.嬡谺奌M潝櫈勐轄6`vk1Л :~橵贀xUE瓳&N!rl蘉\柃ㄋK賝k俁烁 a1 !鵪:_蜦?/`b+承惱BW儿4肒j,~邮勮u(翡+洃fY!尉sh#榘0H侇%转=铱碣=疠 o&vR絧nF厝噞)騎骳瑶'Э唙5瑆靲嗲233宿YO__![わ\n#9煞!箭>蓛G07乨K(燅兂iF!n "宍澄r8BE θ婲Z亦 !7z蝄'3e)讉屁l霯6俎z H;T麒0幞荰[朏d⒁9%c葌蠵S浴雰陻<祳釚覴a[餇虝匥u掀罧揢fg毵膭笺;[誶F(F,煣懡鶎%V8嵴=6r4崃鈩\﹟\縈 E崹vz$!t2-栊斣黴 zw謓 Q礣簵MpY喋,搬k鲵'6簫'2姸I綳蒑6羚7X肺.絇xU闺芢,Y浊抡9瘜勯0'友蕰獭沬叩D綢藅>D=晢o9q敽Z玳6愍^磳蜆肚錣涓嘎t砥??欭闘顐鍺?橆酯穾珏呖3[bb叹秼63 讼W篣5 a茋頥9#$姬n( 锓睜暏T`$涴氠M0〩p芶詇iZ/俑v跦 aoGh%3饮魅睓G殨bP痉汐辤G&ㄘ 8汮祣蟏缛堥帻鬘湣弤7!孲!潳 锩旔0DT>&f拓'?B筷=鉝C9蛳辭掊U17樛.kG蝓鉹H膂; >嚆3~_聝|*攗S9湉 轌*尣[NJ鮅5D/卑o 欕2F{W&Bp潡oM絑&趍叱V4氊蟀孳愼抾蕫>誯鎄隮to啚傲8)v鸀鵊c|G勪朑a桟}_笸I 輜鵠R蔶}揀姼磐o鶞斟愀 浀 荾g='弯鰟|槺妨 8駉鴘$潧o藄枑塏[9餍 3D蒰随逄## 7餎 嚘b2缯:^3≠? 囌鸙;n#iw%澬IK缅鴭6歜a蝄悖K. ゑ笞 萄 TU(巩 "跻.樜7j 綊-U5膌XO諓蝒q W8s徬W偨0j俵陿J賠疌聣+㳠6鋭秿'喖[E掯ゲx闅0愋俉鳣::聙醑飣毾桍エ顬w 啮V9煈彂鷲轞瀆D買G4穼梓杲7l ~g_W鼫&谞:X邈Gn銞{}T骐 ;坢檛:炡30旽 哋e槴脚 )i:Θ[湁?xs茔闔傺[g惔[7獖乡奫zN舶 d糏(〦裊I穋汖X戭弢扞l{|瑤?d&紣q%宇P喛9啀琌.?鎈7 "/<u贁酸慞W坼:B趮洖稖篒焺U%絻 Ν.瘃桷衯_僯d鵪嬩摨:8蕴毁[/Q"(塛2睞T隵\12過G夏A/dB}瑩褿鏀枧#訣$蒢W 紀邍)J43弗鲅舅*0aM謭骛逻憚鲔u仌|迗)嫢F磲橶鮪.r-W~8骜O 喰掑鉇#&Fy婙#6骟Z鹼|烊抮U辘Z岫e蘃C]狂f 郅涷*醿dM减;^瓜矉.娓2k匽瀽外鮊"噿9夌弰;nr斈鐯 79埥稵 蝽薰Ρya燺I8法鬳 :v& 鈻 ㈤笘検i s9M姐勷\hx\%)$睢'HR岁眮鉱^Q囵廲,礚錣Y]0r唰暙蘹嫈{a蠍)C4巌筈啹.:SE^#隍JU鍸+A|_怗LV幒埠U2m)a龍 9K 形萏P0%乌#W 鲕敚盌c喭%佃骾G:3+竎_鞖羬?冭>|f&&歊Jf恓EM褢齮X瀾G旂#}Ff=鼒9=+⌒髄P9}畕d臚^侤x閇醛0VV=[軾椫砟djq!雒侵谲礑R`ix/(╥+k8_ 娣o*鏹L PS,-达耯zi&鰮Q齷鲹ヨ紴m敖(> 厓羗[晕n4駞嫴>l輖 a煻NA誟緄P3v_o餮o g鳤G爪覾jX/視 b9嵯jY 0鼴B蒘榎F簳.[ZR剭軕N鋞}裎頾#鰭焤 媞$Z8..q.2詝n l燌^芓牳3馍V輔l裝諝咖峌q鲼峬仝 1o稦^厦毶 ^k狋n 3oxB,=恿Y聢H}36隬5s菧GH3堑 6L+暼騪E82"{/;疝攜窍"5)pK墧%=*oVt僧焤,嫣禪禂S ーn8鑸鞠1f\訕0s.郛砤$ 竭/獙骻邐櫐p謝t傢gir讶<蒠K&$翴9Q!$蠊i艶*#^醘﹫y薻c芑$餝=4k控霞&曉o沊崲HWK_髆+輟蹂鬤鴾鷀笖檎販2氐=!M鸞悿貶c砘%65w1鸈煕妥降翉晍隑墴-嬛Ba楼 g臀^瘈轇Z膚R!窱yH诡)襍kX_嘤抁嵾k9墡T缥遦z鮊竝(Eh畮K?儛.NZg躣<鯢MG踌D鱺喀/槕生a]蟦北S*縯=讋煣喕寋+U3煳)9逺1(8>8篢禋燎奦2pH 礎V&IB.-+t翅%翿|)]w劌 ]8l vT@4韒 蠺_龝,毛e艟醪泧媶惖}摹畁Il廯波鎃墰~ね蛠e陔ri 竽 褱搀9Wr袌鮳X砘毜錍癗q=QH遢抲倒偍餈 d婔^}视C>碪註c唌泍U撢柁枵?J縰|5梮喨Y燗硪焙莿9N鴑2愕厱鳵"=4訢B誏╦H`2唀 '硝麞4&∈v峑噶/p翵#@锓鋤 贶k封\淌乫{]ad鳳/魩匕蚱晻7军ご$"*稷=~骃剋2拲椺挖-翑3rH 3O淃w俥默榦蹀Q6窣鵇蝚t!㩳4灙斋鬊][蓱艑*墉D捰Z}繞薴膙y娽軙坖Dt2柦sY 彳`A<&@闥珇詯跰黣昷盖"/n*.L讟0P訄'韨閚'/誠C呫8'鵦Ё尘3误冱5覒 盆:"怩{'辸e欒Q+/傧r$樓钝.A讦Pゥ⒀腠憌$O萹鼎髡 6a鈝鹱G瞭H醎 xR嫩Jj齢z荬纥H簯』R唻c諢^秕^崌rp[R園4愋ns隫*亻lFv\6kaX閈.eу8'7藺氄Q[匴p徑纐饘0鍝庤d{0^摪4&既:_os7泺髌""V2CM5翉))eN砹嵾`歄Q"?遦荅&聈軁-靰59炸t=凹赎'>5甪雕駩B齑E槿惻巕聜犠k拐 W溧HS$`,m柧Uj欚{驛闗&灯靃騙黟,C轠k髚汁0&&肖妪:6 芗 茌 槱}9"箔=哱$e!.d轚O侔V<擯3F捵.q 皮U乺"沖瀛1vo患緼8填扶滉D攔=n3*nZ)RB宒償盔嗺琇樇朾桌桑黈凕uD閥霄 y"V稲f啚z+f|mnCdF.=% 掬t豣ORY8SI~y绾\G?3骶q借4kS [倊骳頖V盵D譫諻羊朿u褸g镳M欹m醸+斺mr彺鷳糯(糙s嬤鹺穔j螐I峏S臟 @,*曒躩椬﹖朌"篁潄8蒘澒轋芪鐎斴.頍渴鎧:F晟3钩颔痻aL玆婋啐礜nⅹ=櫗櫛_NY駟4$ 唲坦s虐h>Z/浭蕞Pi鑴铑炸曮T%芞"".78生d?〩崞讱DIG&9廩こ(/4 x摽酳懫泑眃ru侩炥)吻 %盰侧<餰)kO`#x混)g罜閩:%s'G据鳮2烺陪珟蔟\選犭鸛X偟椚z%;X&<!變3黫虒冟!甩!鵳硜9 D荋兩\_R2橎妲覵[a$y /機b拕摹820M3E!$紬璘2=&圤5)(3鳮R.鶅'ⅹ浍,'艴!%8,鏫踛 <濱卛峡厡&S3靬~jr}7赠h 銙舫2<秳l俞N濂躪&]吐  |;"亣b1n妵涸優钇鉓墺贾詉U 歃屵烀]0啅>昢0j榎ij!~TVp被8ed%[C窂M柛c-[8傽,?# 鈸彫9%兕亟鳘姖s Hこ哝#籆Gj &/v@霣o襉僩i篔鬈.胨瓷h装LzZ踭Lc瀚淉)e鈂fj=盌G ]xNU琏 H1濓蹊秉顕3Kk)2謚鄻l塍籟峁q7矼R'削|嘒鍰颿糄"萴"p彝v衹妘硿三瑭>e4暈铺#1膘 |J垟R禡!莎5提5:/;Z呻1auX<奦璭4吏4詯浥芅蛚暛)%螘︹)81h拿X镼a╠9#Q鏉値|よ[9/俄^絨Pc&逷g.(H"]郋戍elJd褝@w显=q!腲9r7鱹氟纬{F霹A9畟嘍J'(6qk鎸m OQz婠z蚱醭hY睮甕胄怐G内瀚砀b嶟沣翾{鍢濟盎O葇簍u摱脓ㄤ"謃迹C癏.髗f蝚豊:磍3氳幨31m類3諪%'厂 唾鵟nT%ow哃 H3 詛1渳И锽;0A 茣J脺种#r厥5?衍I鋗>媾Wbд{\N7竘O卒詧fA珆;29诫祡#+n:欲l鈾N埆欺@D墿葻晐v痡d滸霈CR>DRT|w玿錡-*5揷}/龙 *6僺寭o焾_`夸鯬3v蕫伡偸1矼~01r姅5锛{階n;3鏀剐骮 訶\枩"犡  蹘漑4凳0y?熶屿鐥烘Z!td\XZ-濌ws璈擲濠Eね ;RF:h堪岖0沅 j!襎汧鬋>c>*x^颤/:6薋te#Y叱笠F)c頛R薶 )c{鰮.茷Ei逻e塽<鲧獇q 纲O葱0>攸)慯眬謼!J悵莪趟Wq求(鶜,q祣.猽ī馼4盆}.趯z莨屓稡鷚︶庯Vp -_熕(觹騢筢儰槊u窕2K/攪靯堒F1 琢鶹)諈挥泖n嫣~鰠鈔蘭v檒ry稸9m3鼪q瘔 PQR. M蝺1?鬦R諑﨤#7 +z丄ごG$[M闓麎p颜lg釕哶鑑伇RG酽 5;袩65zet貵d弔R五仢癸54諝J 蜊')+>徆>&"蟓L4冐i:U8x怕:A8S7?<"欦╕ W讈=G淬dY內6鑪駊暬鱋╇囇躞`碕討%I/x'犱x坷胢糣:&8dkP-$賑茆9?+猴Wk谸~,慍V囼RpJMtz!6b7荅 豒f巐燢* 啨wГ>詊蠛N!-(2呍鵕彼w枣f蓋l画火*獾/5W* 徟禆M异 Iu鉩捏7侀M t绐H撖;3糓撆 峐b j5桡/ uZ勭0J騷厕咈sgx:钸#Zd 孓N謷69=*畯(秵 恇")斂i粛E劲=絬=欳焃唭V堬/)ts峰5鬨瘯訮&[y贸8t键,5騆;+RW炥,樉p寇n)q癵7錂ЦI,;6*rC棛Xb豒缔)!:?: q&P汉颫渁! ?2d.)z!禳掋/?2宛3鶹4諧_-鼝我jb0S湮5匕~.(W|3鹜鼋袈 h蕼鈳!諠5%\穩J(匌薊ˋ熉+-y繈l諊S ,i/臧蹩g祾募k騗~匴S)x;| 僧蜷o詭瑩 5h9噀胴g撻寡f%鸒鈌娮n-瘼Vば裣 4^>.吾.3@憹z硺宷Z@闞:'殅T剫鰌)夎抁颽傄犷 ^蓅rfE簞s崡倳4xN浟敯S$'-课|愨"閪*,揩K鉳恥躺崑O黇).!s?3鹛<斩軗荱鍞.憜於敦4裮k剥璌0㏄饤B臥Z鈁%4d蓊監桏 RY耈镗\%G#V z阥06E1 5婭圩皿撈繘唷&叹頯v釵Z"A餫q e堶Q-蹲#VD 蒩A- '毻[!)*熼魼艜吀銰鱺{驹狃臢巬釥}M 暶.m縰<躊"澨豾齫'鰜vP餛xb鴓jB/Q:篗`~肭寐<.潵Zo朥\# ジ1R宧輼/G勯 鞟排^鐰"嚡-萼 u誯N!E^驸鮛駤F甒 塛>f蹶_9诐>If:鴍E袅銐l8湟!+黉#3%RV;媼v殩.^粟鍍x F眻柩烛鱪漦3r>鱓o .(i2r韭ze+*'^騰-Tn 鷪M方嶫詞觘T壓嬅x蔎Q:炌鞆%z !;└鼁,紭鬂$痷桍途畅nrg'f酬挛s肽&泟跄#Y鑅卡;憨kf绤g廞矚絮薜蹧==aa嬢-驒s湧嚾雎踴whG宀5>:讫亼x'a摤`%L0烷籍q$,滓Lxz訇嗮ln_ -琈綋NM2ㄩ`b湶X楿3+旳婌乑讟饛}~p 愳净懞6D2诌兣9哅卿q藎Wo!W+S杌玧穮Oo8佸4~咬闷U铝灰1橈 (48趶γ彵秝皠鹻顴揬迀M7鞧sz漏5潉?'锢%墛&?-虊淔鬢U輀Z6褸E锑狇 唝4=顐2*延 o#臘RoA臨d荠M在^N6潂8掅b鵅鷢a^-n N儳筜i)簊\#]侚T膤 觴島z鯯 蹢煉sM{j~管C猅mC魕H+B,嫓<hD︷ 3牰ZEΘqP吸<胑~P,T="睵鑀玆憛k8绥惞iWT 陻唭洮罾葱蚚毑TS?8(佼蔰鶀W 魩 緩v旟刅Ej菩 钴}說燃4凲i#-瞸擫蝉鼑O. 偘CjtH:.$> endobj 5 0 obj << /Type /ObjStm /N 7 /First 40 /Length 553 /Filter /FlateDecode >> stream x讠SKo汙倔+尕2妙虏+Y憀G║瞬$掑羀牪畳哔篷=隼j^7f8繞i垇4D" AX:V辺灬銂c梚n<溦UoA+WΛ弇f:bs+觟泙 抭5遺o G6觐'D1`"JN .:[6旚>|4o=l-h胏侹?+匤縙蓑衽扽讳>純%4->艆悲@JI4A﹟幄D$!伤E*鞵鈨o@灨y@o灩m},鋂壌篿鳹篽&-,qk緾8[$gu臢豷脏*p-w愊卷誦乜xo含-浘n嚳篐鋣^嚏/蓷倜妐诧蛹僷樅Qw#1蠮A'.s{)p6_M*帷Mb}#f澾鷗_f*<\麈2蝠鍎 #"m.} 0.q峅樷+f樥嘋;4s,皒o Sa?q坪2豟c诓掎/l泵忴蠫RRzu皆棥PQ亲蕞諬誏游弗{47#t攎综 玳IaLy鳿铤ⅲ臊'辰游陸騊物瀹 穪圹萗J#瑂o\N租 endstream endobj 12 0 obj << /Type /XRef /Index [0 13] /Size 13 /W [1 2 1] /Root 10 0 R /Info 11 0 R /ID [ ] /Length 50 /Filter /FlateDecode >> stream x赾``睦 @殔唁8坒b&F堎嶗20毲0h[ endstream endobj startxref 14172 %%EOF xapers-0.6/test/docs/4.bib000066400000000000000000000005721245427370400154250ustar00rootroot00000000000000@article{30929234, title={The Circle and the Square: Forbidden Love}, url={http://dx.doi.org/10.9999/FOO.2}, DOI={10.9999/FOO.2}, journal={Shaply Letters}, author={Me and You and We Know, Everyone}, year={1869}} @article{30929, title={Circle are Squares}, url={http://dx.doi.org/10.9999/FOO.3}, DOI={10.9999/FOO.3}, journal={Sharp Letters}, author={Me and You}, year={1869}} xapers-0.6/test/docs/4.pdf000066400000000000000000000326451245427370400154500ustar00rootroot00000000000000%PDF-1.5 %性咆 3 0 obj << /Length 498 /Filter /FlateDecode >> stream x趍S睅0 蒹e囔F矓膋wC嘯戥锠貙獴杛杢達Jtz  )驊||d/痋U湻絉.虍:( y瑔辆肨7欤'G柆争少{2琈沵\N壈Ni!系n0!P D  D忯酆戄灲'罵徇~嵮D>?噊U脧璕=~e+鍋喇挮O纟q烴>槺O惯慪= $棢%F&砽B褬肢筫e惄箽H愡)D_(铒-zvY<殉5恑!僬玑ゼ;2#T襛)l檕y=9n漋蕁袆H呱[TA癭.囹騵B松=77 荌X;錇=焝\) Dag慰($肕巭Y|Y卿 "&3妗袹樮4桚u猚羀kFl7+B嗮噟.丯綋櫇 ,阀F栛c;a匆Z6摣纑;r,:嬓>"鰱\綷q@fs9j逃隭奏U僖"C(\4F $s魄 6]磪@2F髨飝跧U5-W捜劢 ?蜩B endstream endobj 8 0 obj << /Length1 1791 /Length2 10683 /Length3 0 /Length 11804 /Filter /FlateDecode >> stream x趰Tj-L#輂Cwwww 1 C梩w4堃"潅H棓劆 9~痷餁禈貽秣菹 #瓗>К崑H馕伤#愖糟痧餾耩稹32轁@浹岪07 D旆y棹dS?舏篅jN^~瘣弴G?.01l袖ü@@n鑼.P匚摭┩~X琘饥⒙d滱05痕儨:Z.謄惢昤懓ww噴qs{yyq澼竆`vR/盎=@倅俵h滱1鉈g貎蔺搽回簕a 罁 l 偢=ex@l@0繱s惊@ 傸W囡餽颃S铒? !&]湣@b;贘\钷 逆廆爴浰S>vZ=99$ >鼪灈5 uw鉹;鼳扄2OК睉wqvA茌極 Y?鱛7q駛 l?H豿@ !`W惇逻!O&mv w 彴 y[踫Q蘩 甥圌堿 皚" 蹅灳旋軃 ;圜汇://l賮!V2僱翺{Lx灤 帙泱/'y俑@渱 髜拐te峵煶砒煖湅7缽揰郎'  ?谬Ol<T呚Dtv♂*XVw衦y2莉Myy瑹?蠑?QH擅砷O7藷 v蝙;郔頞[¢虼 }鷎5A6`酹煻Cb玺1傒斃 盎谍_-<晈C@:.n?'/鵀鐾隈=q{韩?]牕u枈k?鰩OP劻>- 塞Oe腹 .頞)'z[7*$ 鄸敏'p+龐D躗 ~服?H)/z姶腑AO鑙{嶠'吠o 竚僌}韣僌屐儌n餺P理|毸74橌繍鱥 萶餴 莮妢zs?$_椷x<鄓 >M >M牿|*帼|:O忂嘤酁簃k扉黶煠 y儸褩鎉罗_咨Rxq顋挏a躽炁书腧概FMg瓆粩M砻トr!矻笏颎{jdG猲鐫紼厕訬'g狻梢矋僒h敎2箕縗龒坩徽 ]=D皍 斀+V#鎤twk呍1+9 鉒兯g嫭蜴H镻9贬{闾^\梧L>舀%常捏楖o%芴V鸸鯋1揜!^鄰O1韊☉,-[噸gwMl:я:肀h閉闽O蜋'臱菻 Uc阒;TR溗iSP驇卡瓄珵 穂筿h禎0Dwzo }鱌Y^癊曩7褸#┼ 櫚v靻镢I諰嚟W荲 涆D-┶粋沮絥 轤5=:{㈡;骞颗鍰x|n濍2越;醘鉒f熚剀"筗讘^爛罭K軜G錻)贜丩O燓:О澘Yc芈琣kn韃鐮鷘脏- 脢G仝闎懽x*▔ 7憶氖7逐#7闸X"娤濿鳚M|8;2+徜戉訔昧詒J#0巫N8)J0m'|+W颼C﹦鲨 X 獦W鋁'丝s}廆"斟纁為#w饋鵈鄜x魗0妯8檹瘿螀{襐1痹c7j轥}:鴢腶7=k3麃坷@ g9>駛屰;痢W!#Gp7⑹!鯝q&蟜Y%甭m+扎徭I4╙賐/)路$紭y 輦咸坁俉昢踒莔荡s`D槭危霅豭胊呡%恸15ヨY諞鵹+憠O认j;oMr5y弸F鷈wB9i讴匷u唥B]/m旙v毮{:瑙5黤L7g?S鳮煷nq聙窭,k鍯);y闯3仺墟ⅹ0J1Pd嘫5 瀿摫稖鲲6玣r動X颐)v錊煐#7 S孥(⒛9俓t鲂j绒禈b%籣&廭鑮洿}U鹹遾呱饢葪堵9&}'蚮K硚頍4w9臨瑇;-冘墘q蘿寤:\,丫旉_跼藦k-`擴i2喴YE楥e/ 夔(濼饌歷堞W`x誺J4腆俱-ARs鏔罨傯)w誈Yo恶哅/v4z垽*A鷓敃鈠紽p噯A掖No5棯戴V 凷Io[&SYホ虽煭fY u掏w&^e崊撈v喌疞袤c?7e稛:拾粞z絰{舡觢鄁磢 咭z箸$j O皿湳N飖Y崋郯潖;*蟥瘃%欈芽U煫%艋e匶P晫7#$.扄 4鍙e蔱& ;╚懀揇峝馿jF邨3b>f `<}`I% 0Ac?m 瓪辀鱊鳒x ;帶<蠌%b迴豛4S惸圃.p]剗鯉窟!R&氎沅:蕼鑋'煨;櫅,穚簥蕋L瓋仔d;逘耳莤i评R E譽圶扼%E:Qk庞+剣U窼KZ2鯑UH;?T√6-p(Q鶲皚 C$▕异@1IC掉1?浶2U#$Q 2]93"HtM:謒>蹏y勛綌褎拇z|zn(M帲+Bu睽蘯G 忙 ロ嬘 y;/蘐猛 ’Y蝮睂鋬/ .橎毋喩︶ \鎦4+ 夓p签b蛙銝W2∵f fv堗團*招泵0c荊*_珀T埿d韝圲u尤$帜Q2<%匬g晽秂茔]_D腎m&BUI隭╛渰 6壜;U手/劣曖=巳0K砼e瞎莰wJ>":c 嫢#f K;滁~!A紼N揯抨Y5Z~71d沴聍/%銟绕#妌MZSq蜯譄+N父Vn0#fN_K関G2礢d {嘴o鲄dawGE8士u}喂旫+T5- 饰ui玎d\6w$/芖婢t蹭肈M%q镴s剗X砖磨9冺G鷑S彚駩;侨/B2*%"(燤覺=S 羬C|` 唆迉t尚*!(忁乤M氟I%覾痱>M)鵓u覘硐偶ru晸j 鹜=g'mTP !堙醞鴛申缕 t罀魋踋U薹斠铨蹋"b,q蔈栓(pc虰1肏d慪X§E 颼梹胱搦%k;E粀|鸘?"j佱仈岛_hAe3P 萏頀岍95木臸qU-讓C 蟨亩>髗 巐[&酩0)*"K稾銙b蟔b石dg 7pZ/1 誚kz~N爝武6 矲韤=膄2$ o;,f硵愰2隑sUn~愭聡%壁Y亞UO9屨媰@榄櫎趍庌u^m耟疠xs哼a爑#$殳王MQ运J∶mP7is>g9囅䴙P-钇蕜5,嫤齮%99/v,:銭聒 o3|8$9X潽W[灀XK鲚Δ(墲犱醩s飰qd镟清|臔旰辝w瓊髖叡按敩桀蕼2歯卩$襋5 :%骂翞%Eg=跊>?悩g -淀讛=湮L蠅鼘(驷jPV,*7蕸3碂[c眇. $4Rpcv蹋ul諙/暦鳠囦岵+愑蠯c關&q④绞 w錺滝:鍣'パ&Z齲 諗鵨皴P鹿qHLA皱诚妁0糓佺嵝咮 JC?*胃柗S!rg偢r族蚂bt牓龈侩V碝r掩'䙌r臋喭镘g歾翻湒q禈锬卾5o ^]xT樤;澆讣_zD #蟰R9餔mm霛c 劄,7緋"LTx酇CX湳屟Nm脓趬f-,\瘍鮒^x嫶祟e*B赳覯鯃奝 }2g6q俿雏B)& DyI弔R薬-K鏵S<倬5c艨I%"濧(郬蒀sor鉄乫9鲡娈idK鏁浉璍/饿s2o壷愺>送4yI遇>瓓娪/.=:-鵧"櫠瑴輩瀾&L蛦!耀D^蠶嚷萫蓇y婂迵妌Iz2暱w贁彷郂|@zyA魫鏼5:剣R>/m6klG8o霕检G賌*>W:狙襂<䲟釵Ё 乻圭拧\龙9(#$2L8]1搔Sf=yV献媓嫷质燢D俐<)秹餄W!3鑺 駱MP=y権I尬靵..抎6\嶙窦缤埬"~陨悟唻[y&51齐鴛C9M/uhFv%垖A搵?I\秝n螺捂E'U*o骎惼4蟄2髭 狂f;uV麌鰿E幟%i]瘠н碕**b>#J}蔠板;&イ擣[朊󖄻+~菇o 譾2傶徂()V0‰頲閫8d聤酺科笮#g悎夏扦鷯枃熢捛`窌8K7N勝(8Z僄X$[k橎]涒璮伉T恴欱_6浸# 鲌;浰B損VV揲k峔&狰ES訹ap3w(<5i"u廆4扛uR花B擿 獋铅R#啟鞚錁摙 j 貕$︴粒.饤墌鏤#k- むS<篈E涬p鬆拴.臛;鑚#j龏瀞jk嵩>/1 RH揧瞢i韺疸 V慿lM=9v飿:詎3鏛从渋朞曧J豽倪~9K`莪慤=朅绢4鄭J矫Mo5砈}悑棣魰:詬髸YC`$}>/BY7m蓶螷肘癎>3竢l|威H\ 毊+蹌擷額籍rmjU&[c9刉櫐並鼁 暅Nh滄e弾.t廲艷 s0<= |& 奲-I…Y\p伶fs:~徍hcd嘖穟1d类侚齳G飳賞D訌S3涋fV.虺^噰}鋫渮P誶握:!#巹b$'賎v餲1鎭v\(k晉C^觔k/bΣ_0妉攰餰靨铐Cr3輐W":衲駲bC麸?|泀 gX謙 2t{;3#骂豃7X 锥訅'榄-ymb槬蝮掭濞Q%S4礘蕁杺~I繺z'bc諗h牆蝅耦蠬蒡箇训Ovp30肈1g,[颿皐n]"/庿閝R:a黲w跡扲蘅灊O򠯶`)>4籈A謥㭎c=佦`Ws kv"囄鷢鶷 .?毙g蕘6俕老1徂屧!)賡痲$$挏婷隫茯96v䲢歄j蘲(曍[2逫讖M甑當N蟦X[@4n:堠]'+0M唙t(*!(!v汯o鱶嵌齓淼=tD#k簢I<5諸1痀~r鲡E*,z#鼾″p拾u篘璤飩!yE躟謞臷VZ餬癶#QP帴;節_ts8J|[,W{,拗,&&糑区 /r@墫<寑縠鵿D/Rz⒂璯j紖亵&)o_c琿}8@. 忕n5$V毫i绞vxU德2']湸睱杣踳(q:`wB来銨zVQ嶰厍l7{f|~+"V?╛K奛衹z-i2锒 I問3縄@LG:m撯鄳叚 澙4円垮馢#B麠+慌iN.V符Cκu%!蔖捓7r嫝M睍漙a>!玱(?脭-FfG5C+P1A煠秴嚪,DpEW跌摗傊`p粰 粇勌 鐩^ 婽返 t稦覴@h.磌42曳狜9&%=瓢錭*> h鋧"g糁iハ銹鄤Ⅶ ⊥E敨'紤P澶庫赣B7姕說牒楄X53T冻m:Y]3=i歳y瀷rEZí6J榱Gc橐v处i6#c_`QZG?p*v璵'#淽7j"蚬挊!慓x$蓙撯馃+<燡{I/_x祸) 稵v劃 yHD坚盶哋ds阉欇E噟蠀纾= 垣5@$4lV聿餖險鉂s絀涜睂F 5e7rmB簌s侣/栽銌L4嵈_#鉯沔∣5$揕鶕8懋,) 诂笜#`5Kp嫼玦6裐'嵟淮忨鋵Wym幣?毑靐>%Hl,找>=.:敮S$r雗Y笮67,E旈[E腔:bL"}f荂祪6oE J6+.P2Uジ3uO負韵[M(6煵喫.鎕=橥蠺涛ωY栀涀憤()擳蹋N8皔)]3落鮦f7(亃n&'蘞槦Q淬f荌ll魠貲人'S! 椋~I3嶝闥y:曤蛧袀f+w龆顁澙枔$,Ml龅蟟霾pS阗C椡l侷. 昩虾+:W`醽梲沮l.<违4眤MJ弡рg吐姦1齊k璯鼩 披銯嫅4鑲胝塡18Cu(猛R\檺e阤d 旙暞省鍳 蘐麋⿵懚:恶肻脓M)出_px单3忑輡榓BS眳滹>奟aH漇湼K壗;蒸-﹕艣甎燍《V蠫|PG髻~ux^恀鞴半ク夐鋇I谒|+S1躿抓b4!琹欺7椤1%梌槓A峌8i<$RQo猃)搐鋸 38LX.唙p鎅*偶+听播td荱恨壿M恣呆Y炱/竎O>m業報沟朹鞁e9聥Ua磥l浈?殜&L!P9r禜#Q帴跘嶋?湛tI嵫|hR)Jz532$鬓饷3m5 :駰 勭=j/競旼Hf塏知k\/ 繕竓)*┦n;莥5檁K!钭$燕呂馀&檮冒w_7监榢Sd 萃 "沯闝隢孑:={Eku$傂q⺷爀姄H嶐処\┤掳b&V<8遉'愼5葉@]m澜址4K荱?d#r 綒\He跘$E 藬镗泍FF\s袗塉躴"噔Z垎虇驟搈y$D tYZ珷緄e韸g岨J戺K蕼e縞&鯤陷 麰舦2樛;竏伦寈t{X臐軩3燡亣g>;盒VET3mIG昆抆陼zt匛&ce駃泡氝2(VU--/6譋邥譛,途儇u({乛"硿愒攴T浻蕅}此XGx鄙 H墌蕀搆6z7駅*碎餩4轺鏅珘42腬.蔵hm$鞐$5骝║崵菚Y_d0滖C╅i宖? Z钗*褅识`H2 蜉 =<饕家V)+|瑨oLu]歖鞮XlB鲖i禢騏酡&E瓄栽鴤> 塚Z絖&豲_9脂q襬騮w%5涩怬猳烆2+19u枋y褗 桜 ,$螂x俹Q]握O0郣<盆k#禴稝+]囼A宯庐觥瑡Y喺珯 A1钜j齕!葱Wh缷瘕5C鸟z6鷓u繇%象p啟銋(傶殁5 V伷4莗珆>nW 8-/厳D信㖞*M$莟k柾]G+N斾眚&怣蛆r 1癉驏?燙|諜脚0抮~晨翻trO砍璿噗綧姉攉鄛跙e迴-,敿:E0;l=.a$78鑮綎搭%d黗s!LNN笂M 怢s筒.i[^濢緟?2G瘉鹯煂襭媃"-ux'8.vU l>騻zt、N?鞎客qE茳;F(T/琨昰6侪敟7 4鋢焿KC#rG劰殕针Rz$ "E懐谴郺侩墱Y橛婈畽Dl(鳝4y7 Q稘忤n?鴸0綢\狳>硬P氀Y洣溚/疧侗> Jr釈格85 瞎噝e鵦P秣hd鱖綟=騐'T'_'z =<壝@邨ど沒~d"陽 ;7锍=KO ^ 2瓓栨B:)\*B-赞Y瀂穓簺渇搲{`<&徂∞揪閃RN/?&駲6脽痪|蜉坌8镺 璿觀峴%t9i鈡2麼杦?皰亹K1zd滾摄峍ゐY碉 ?嗾巩闒]颽"校V畳U[昍,橈ㄥD}mwg P氦饈潆*X裡&籢'輻绲LT渞殦]=茅)迠s嶉0Y 鋝c3 .蓆孯`譺65匵mS墂=甑浹lLi酢l巸a眀Y勾龜j庞\^;f敺莵稯?d雦蘗}UX勌W冯獠8许)狠査"g蠄TB'窂惼-w懟貨?8 榌瑺`戉簪K|XG#籏涟XvAb瑮辚Un6╒).7茺呖}T溁7v@翬儗裾垟>!蠻颚o糵"蛸焮4)l^oy藹悰蝰8醍F}Gj媙騟鑳5鷿@憼M/E+qM=戎S<N六潆(憔歌6#氼N翂翜[裵F(^牤-o4 棯駂厅,"瞱-爂|和篷阈梏d櫃3Z邌D絼牁D9啇-畷爵摠I糱频庶+瘂#-槊见i[湜萘炽6璋暟瑉}\歈vu&fc~"殿w diq廡!鯗錺=#A畿g&uユ=媕q8!+*}!達5gUOZS9鄸榈楾N?>6斬巪'甩沿:X扉O摵%".莙钅~鋚予┏怟蠌B幟q苂Vj%(P兀P6蜘U姮捎帪 e3曄9K86~掎疻9槭鲘襬 kb璇TN翸鐼剐_>蘚2弽玂i徇\熨qE鲚朚 鲪%p鈉潕]A; 寓腙M#-t僞䴗明[⿷.W鳇譿F穔侮EJ贰$膕洷m謑yC t儷>W*愁5騆嶔閈蚃u頔yWr 校c8$^伀歏豒㎏=/m#銥LYH胁 蝿d%B*/芒艂逝!夘%41礚暀忂[%*"馃柮卂X*囂佾键橼租u熔鲽逯f*蚛飷Lz5wm5X工莨 籰輇K亀嵠踙鳣.;M- 1r'齵GD阖6A2咛A紀侅zBo鵶籎8pㄛg巋B哾4驣;漠8夈靍'\樱鯷#y 銾艁Tnh8卦棉炇?=d婃櫯(,75K嚝c)郒鋃ukp^诃>贤兴潹9)pKvF徊c.麙 躰(袍 !蕔変 X`n睡[方KnX#M帖歽叱M脁訿矅究= r%鉾V&h捊zm吨=oGW)妐(X黫t񽕣A^锕6"[*譑罢獎晴 WpK=爖>+"N致[Xr栋(軇t,諞 endstream endobj 11 0 obj << /Producer (pdfTeX-1.40.13) /Creator (TeX) /CreationDate (D:20130413212533-07'00') /ModDate (D:20130413212533-07'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.1415926-2.4-1.40.13 (TeX Live 2012/Debian) kpathsea version 6.1.0) >> endobj 5 0 obj << /Type /ObjStm /N 7 /First 40 /Length 544 /Filter /FlateDecode >> stream x讠S踤0 }鱓餼眯P[杸燖捖丨E韤 98硿←鸔r毸龈 "y!Epr喋櫶!8(0Bg`T >9纚['投s畚$乮係鐩}[:Oj裶霒70g銹\@n"!zK<(\ 噣(9E"x6逄u0Г7啵{韅Hs 怋 R稵泚3}光怜g锷e 嗜`硘@4hPJ裖I隊Q2撲藰孡妑NHm"K鹃跆兌`tf镼;犺銓}襦僳 2┨B 菦0fp弻杷涪鈧亄D1℡熴S◣齂袜浦8w恽k磤q{姾鮙Rz釽{08 >姿n錳aV訃蒈S戍z構\\"'柡諸IO9mV鳇5 endstream endobj 12 0 obj << /Type /XRef /Index [0 13] /Size 13 /W [1 2 1] /Root 10 0 R /Info 11 0 R /ID [ ] /Length 50 /Filter /FlateDecode >> stream x赾``睦 @殔佯-坒b&F堎 30歍2jo endstream endobj startxref 13433 %%EOF xapers-0.6/test/docs/5.pdf000066400000000000000000000345401245427370400154450ustar00rootroot00000000000000%PDF-1.5 %性咆 3 0 obj << /Length 727 /Filter /FlateDecode >> stream x趍TM嬠0斤Q喣峬弗]hB瑗魻菾v@柦枙哌嵅魾N覍驺i迵>煘>醍(蚓蓍+.桬祂j櫁草潌+ &;龋jMk皋]nr穿糯璎p7淶RD半邙<;4M'~9萅 f橊=龈-+∶鈨鏻k鲼魙w(诩畕\e.e绵詇QU呉:宆9舶f赜W痨D~7lI厣虉瓣c+綥舞{6准ee+ 鏈 #鉀;Dh菑燰s5 F7X壭5`J+堬擒倽俒誮3乾i+BM錛黀F<邕`U&祼簠T苈9V0唆W莠$kwe泧儧鳙 塎瀺3颕)m寜譆 9 烒╖'旵䦆Y蝟4G^Ia$Yw沶 傄堿嵝\詺塐#壐n盚qQA#>U0oVp喞)逜闛簓L4嗝 `酷栒豰/* 炔丅鈲5: 鋢H岓=sT沪/Y嚖z濃6紤上 p[5C 知QO藴Ik 詜H嬯>5慑褴窰_a|8[㈩(;^岟|鮓鮀X鷁<["Y-!=蒶 > stream x趰P\-茌ww!h囙茌 -H餈 窩p非憑r铨筷疥又s嵐75ゲ硺┉1H饽唐H(ú@ 櫄Z靌 孡 rp跙 2rz盜9侥)谺蛭67??`h肜4rX蚨#2祫瓭剀麻鍢袡伉鴛橚L堎&F倯孺錎#k毉 滗㥮%-湝禅YY]]]Y宭Yl虆闄甡' *溧2麬爃d鷭 25@禅梋椭躺杖x1X僊@菞 g)餽8@M@+_L縢`ca苓C﨤621钡3偢!35$龓派蛪 `1#腥谘%呷lmd餲鏔i1 量98頊Y林Pd趟敟 66 垞#齀@&/cwgf 懂峡bj Sg;V 剞$'鵺葖 <|=鋐b龙Gyuw;袩N?/ =韑f/$@轥3兴钵 噤 蝣憧2l0檭!瓤楢f釛藈魛/赾箫脣糒m!诸棵糭V 輜b趯1'.n餯驵0硈鴛y<\@W鶉iU6饂A9垯-/ /雏 椏UA鼢菩E)t繒4y鵥)JM蹛闯吊焠?窇 刳%;;絣厒硭n@7T 糇&+L廖6霑s2z1埞?c;J冚@κ`'嬁$魺[x)o 唨攎/鐎琭/龤 緮厨]鼌秴粷潘飛膵 /涻苦Ks6!跭+蕔賡V圻誣b_^r看f髹2檑䦆怂粎政_馥[翖_ 8 咎赠_馥q?)拍倭徨 .咳?52A^槼5船 l哗#qe殾抟J鯸phw綜GLく署_qK盍\趷⒒]<鳵z秕`:拐婞D羴X]?)澈瓒祝綏鐽y{g^t<>泛As[*踀躱QJ#5"摞蚉g綟pb&{艀}鈫1sy5崫=馤!藞靰蒕瑭皇u;氡\钬E魡H棎 {t捚S|7I瀈蕹竝臿4y縸l&佂z椢cKQ跗a風焼z偀巿W頢腏媗淢eg嫔掽姏cG&8鸪5O* 遃鱙v澢."^6C埅^b+ :粠=4藎!掄琨A撳慹軉f>tC)/砭腈诞~{岁斆KnCr3S.{ 指'&槎澺c帵6D壴 﨎徽-壻($燄2蓽緪GR%fゅ|醜PF4鳳籾B焇帝陲妡诳縼[猀,VU懨泚U$u6鎸/髆峗'v葰|h]p薰VE嚣X蛆駠|f K┕8u巒朢袒汩(.}菌^'- 館郡扆={!茎sRL+DY黯m0马朄齞穬會'Z磙#熃牥60臧懟畎颔S3蹻 攫s枉%r6?5蚸h镨A]?K嵨M}鬄>>6踌瓽YIw義A鎈a龘啂oHI鴇-|#>N澫 r∕檝柫洒B#穏鴗買M!鐹藮汨w(閽憛櫫"覹is=㑳囷1潂蠏)^醾>鴩鄍S/桮錳 _*A69*%-清傊!閗s珀迧#|鉛孨7像q'鱤6蔱鬾磜4燔瀱vfsa槇慲窯骃2叢夨瓓堝玷т5"歩臩nS橼燺&$v嘞#oQ籗1鰭y杬DTRh鹩9a鷗鹒钃 壝lT鷋q拙笋 駈袹f繪睛鏌4л2頞.逰聺B3/2ro<鸤;┛賂j}蓢熎挪Nd 堣悋w獴屣芜^8竹V輽0茆鸛鮩蟟蟌蟽掺m垉冉攀峎天$]炘匧鰼(|+**.鑘1?_镴各鐔|VD漍6}謝JY坁Bk9n橣X#见q$!%瀅翴砵镀u 碍'茻6墆5饯'q襂頨虓a4鍜9*,‖i檧j&詰篇铩ヵ鏗厖騃vsポ,窤陖<駝w嵁>"_` %^$i泸璇[昞 敍!+v%0湥-D蕫v}.X]餔彌%(x6玕邵E.稙馳嬬灢H銌證b_Xi鴸CN』辎q懚n荭霦M9貙ns2_朓啀-D觙劶S!针h@?Z 宄@戾╆奢W5,蚦)wM岃G;斅濴黊g鯬D嶛"拭" 叿螴G谄" 0Fa5eAkW>盗钖2\+騣KfK拰?賍1!w:;唩 绌輜噓啡5.,yN淳F簳瑭毩h厔稻>篓鱞#=焗虽 棂'抙M胗g臏&痯W卲噚\e<和,A+轛*伳R濧Y著&蝑誆Y耉畮奿_劰-紋&iR1啣園X滘p#-0tE5詏z481齩|糵痬╝務'譤98U雸*@聸溓 〦1Tx.\e8躺|赑L 殫珿l懫埧?⑵k瑵_:wqH蘿&钭Q郡U綀$稖偸稘c#S(厁縶奻:夌Wzj`5 貦螢広聝鎙帾/揋FG4:籀L莌禯I,智/@t.鳶!臟饒qFfaS)襘;8Bxm8贜帬偤殠\%qd浨洯诤L韇/4w!肢い褫槍iw5直躼绱ID8 訯*K>d鮞ps﹡g蛛8摟鋚鋰1堝/鍙⒙>I9+NF卦泑+=ea鵴+:}!B髤$i协r隸魦$34豃⒏谔^媦!1>!艓戝受襔7莵苈$z\剝熃衖醫翝咲:曇U鹗炩篣欵a輷癲M M 3帿q槊裃g擮亘麻绂蓎s榽#堚幗Oe噏=Q !赴琭Q週D伜 !堀) 彷毶雳橄敱 咫漼璒Pq氆奩猊K┻徰*e4oV3鮧 l鬁缮I耲\|HK渮G荲;珠賌峗檼艅钾`jZ]洶~6烱鰐; "瓬峉髏%咯"誵~禄i抦#U窚^薁S蕍U鑴t熜営课M卷凳~IS侥q苚4O僈鯘8-窣?eC鷇+ 檾筰W 壷&艾箨(傆焹曵 ┸尃怓斣砠in1芬5Q74関竗 S~榑漡i-古C: 儦`帋梶W蟚喟 坏Wao頮T臞畎S鋸筴>V Pn哼櫾鍠3#鰄r袩22憶鳡c篖壤焨cij殫' ) 佾r倠鸃誔6`mN3< *N豈22艡dP払鈯AP犯#Z秜殇k%1爣緷!}c'憺6p@喩8J赚4鄱▅)N.wd猩D掳e6g誛隱伤U#F譕&焼B廕\厯0-4懖媻Z3睬 _u籓K諃s伂名璑nc冮湴旧O蚈谸)'ΑG\"鷕,燚QW1澩龙L鐉bLv[叴婻tK菝H*? 蜂捺蔧8詏,t¤ 爿滏YQ璙簥t埘?徟>95d鈸4`碝鳈:詚>槩苖0臤墳}`.勉%n▃≦蠃4倲塮謔q弰Ⅹ=馌E桃娣<秝庹e豑驔敌 ']鏞涧蚫v@1.愿v<6V靃輜挴嶒/潭遑奍Ef,漽i噛鵜"洳dr燜该釄BRskK7nY- 呏鸗凋uny羰uP聖ht(vj+趬%q"h8~00弲&廳~强q蛫W綳9j嘽=76粉羷场M肵彤腟))榣?慒LB艄\T蓛<<堤唭 o嗒*\V魃鲴疥俬"芉+沟) !z癞X泚~篅訁╠[婋"儬[v 铃諶$樞褹磏 7筃}漩 7犒簊樂⊿綎}M馺/%)>Wc5莮qD彉倒础z蕟4v槀\頱陶c茍&)_蝵膉场麍絕乭鐛灢5@穜Ab抲+I0+姇 榮 i榛崓伊基征=/fI鶒Ww/:`z+f_躺EI&磨剈枼RtR命匃}篞烦+vAJ噠菷,堲骨伱8x蠴In7 畺A绌I2U夝绵}弹▉59Um-;W秢,o +j俧薋U雅B褩3?LL}iob *Te掜╢_f剣銖I6-O2鈄I粣j(罗瀨g罝|O珦w5u‰瀺I:绲嵧G儤艳怇犹cWロ#饚+L{雸U/擯/捱WITD %7丷uWe祳蒖k/_u币櫑戆跗移98<#況閯埾!.饊8尨噄柢荎Y缩喐VC#魽iD啵鲝b宖S崂!釽#瑁I穐-堭兄.厪豳孹u疷=匉k@A; 6嵘s$_嵲/崋肛#牼柬m彤浡齈.,d谰48c鮴墜Cs0儾'獞釞弮/擪簤h獦u扯腲!呥S(罩截bタ D闻@饇9/e,cB?Z%.(袏}**,A诓飣!佤澑s3漦蔖? `VF鉿-鑵+6`踄V岎}+(q況mV辗箌俐澈<鐞3 LM殗>9芛润閳鱧観爋M萈椪荵鯨IW蛔e枾誦pwH銌ju嘈kXW %jb躴>⺶枼冢鉋烶洎竊悾 炱虉=壳軰s!臛ü蔅旒%kG坵顥汧f九b"RWQ,獦兮栊+餥~绅&^Jg艑_4绒;L -剟臫扏b雡 G髾V譞gN窪堸F緹`玠(嗡鑲蔠3b蛽冺<儳 0哛3瞔聇_霛第 =家誈746V箋GMv*3T蛊\榏+0綠囖'冰k鎪F噲l纟藡e惄~咥{n蔙誷Z犘A鐞橑lC骓`瀷氝j健>L'|燮W螥PGQHT7<稕 5魣饇uC ;芻L婥﨡 鳮衖>嶰1mt臵N,g1砏柝凲=> Gp錝&?^Z9_軾騈/旦/覒聊黂酐G+3梑 掊慉溊`44>u;]裩}瑎丌0廬S{c⒖驶:炡.&|Sz菹"媧6Av"T噉@&8譵[徍臭ΕWr胛娾磭衭'Q龤Z斱󺲞碇sVo鞅檀#迀laG砝甅菂斝>刱g 倽j橇|4餍g騊9魚4+]6 a$p9坕镝:豢崬褙I旂淇+.技潞LW齲沷筇-U佷⒓N#S醢饱LH峇k鰜:Z缮'〉Y洏鈠.娥餦v衂厀DS砥3|晒.蚇1=<枕ouZ能=c.翛鰉癆j墄 鼙!壗&尰緙-侕 # 昜 $/m磷贵锴啚l(~淕澺磆"0禎#/遇;D-v,4 1姘94L1漾~頠牍讛傟]砤蒵j鞫聺Wv3遦s嶕漨勐3聦9q啤k|Ss鱘 剰輩閎澏5n搐3n^ rcvP赩h~C蹗鲖豱D ;窠*^.B巤苍哶zljO夊膬:岜r|慍U懵uIg`獤B岗嶱uL<9z8`*6/h&抜7瞣鰡B,N:蠡U=嵼Z瑰腠a&豛鞜冂櫺53鱒7嶪y聧i咥辟F%收t-⿻+儧噯1d船啭M娎唋6溰~緜[-¢穚緞'~9C}舋騘醯 蓦煥シ彈 羕囸胅莞塖楶Ap偕尟7}I歲J5譭肧."賒朝e萋譽0邛詋铅蕟c榹碓"屎%阩s禊扆a蹳&T==$j`CZD軙Z枣XW诈  _! X舊X眿 o褠枳.﹝L 檃呿%堰榌-怯簠+ Vm闛E1MSb翩禾n質G懅尛aaU珔陙R弩&欲∈眝'莆`z饅JI2N飿#w|ZWQ裚拕l嚶/"邒韼0自'.悍kwx勇寛寽尝舩?3>壪印hnK窪*'c6|缞H K=嫢,,b*筎誘p郌2}僥y]鎆 "蝞﹫<ⅲf1d:Cs陫G6$< 挖姜弳癿Y0 ?@獧T怺汳軣J0O 羷=甁 妰7鑆Pn&釞┫汏3輡宀=黾侦X鉭g>抽y,~ji孋4磲憋&?tekR),碵3挲3媙硯^喆N#KIF⒅!頙8馃V煋am猔 錹B蓬猱廸j5惱怏z捛贖h|ohRqz"偃垣 譣)牌H襾U幛跂挠M|E`还u'滗瘹∶;}癡鬨ШQ璉&pJb@F颠鳨l艆$jr嘝A.科か俣翠:66z鹼淜痧M娮 嵜婑l摮簤]玉讂 h掽洄_#,止箂 殭V棶q媑3B﹞&q69$馞肱<絙(sGp亞6jbC剩Q欬迳)疨PsaEU幖刘镬释&凬o? 虊鞕噺=忛殇麕N却衄懬[沓V*伴-s杦y25讦{'鈞迻>钁蛁鳩(Г训譖qQ鰏∕卋飛!旝>-BRFi 彄1ph钫伋逜 X&┓咘 狹褱+偝2Imc鞀欝仼H稥蓣D呎r覐咺やAs蟜!tBe==匊怇毚铐氻g2=C釅簡闗畛%A}邢э飱ET=嬐 雥fB罣冤"`w鰑藮苂芻殃嘴活V7;璕\訹鴥H畻2癄燰妵餏潇kΤ撺@7s莧跦劁6.滖寳哭懚莲萯螮A}鷮沎羫犭8.K珲櫰[>sS箛)#G婏%W~n|&喟罴閼廂P蝜{洝 郟G锟鉜-w誓I響 銁y饣u熯"^i﹋賬搻p襈嵵S駍屐Dkx蹳曯C曁躶B=檏"眢挳,%貒N諎鮇$赬k萁銦l戧e8∣|d!V釅>k縫`瀅5眽姬E觐H*體s=旒P俗7U)讱V M妈掂俁Πk琊婘瞗'6啲9吓橢3AXB0曕Vl1_秂OV(襰_孛u浃=謥]H ~Wy8t膟俁囐8 鼴qQ*U:)鷷0~蟇m 捋躆兦j$"燳銂00魭切A:砎游C敗桥(._朏8G+;桪U#砐a欱-賉欔9P)za请-&晗埏Ts%郌糎q翱t濯7x<匠1霡蚏8?舶An黾焔浻i薇 鋉器nw李8N蝊>)芰喈S镊|蟠[E憳#粸Rb瀩+:pK憂甋{阩ツn?轛HM}颣Y2g^姒&q遈8摟*v淍忶vwcJ躋b(&姯/Z料敲$歱頌08靳K)荵8j&兒C+欻削C>肅%鬰脇乓堅模SO" ,郊瞮倓侐媿j骛翯邔鰝瓐S灼饵%%$憀F膷喼裰屶塩璳'寢)7V実照鯦Jd鞎澴D3XKJ㧟圵5疌卤亾堀c琼[&s 呷讗晏@劧ép D$'鯅蛸(!欹]\轾緎x(Q'P郶翐祒Vt¤(Н牒>磼(6戇]喵躅Tc漞%牔乬<.釲殜a@>) 簀'卾傁^U嘹繜8D [鸜檟薯w坝禿 g5輷6:輬湷QG莂ㄢ+7 ',嘙 靓夵轺f 幺棠跏jA宰靘慭熠崿∪,毫by玲b9e0B矀腯=b鋍籂Bq韗増6睗&鮅弼朞磍U町骡凝Y 洆觎Yb觭秺m1? 厀7c葝T"燵!,\茕8瀎韫6j~偪9Jw拷昰*鍔┯,玤臞 Kg>y&淙欹轸2匮f盧>.爊3|?7A磳8虷覕{^>鳤鎗爐/夐斝_支5阌a岨葌=M裌@:鎓#7`店i\窭 禄k獏鈢=>縞o廻D03T~i8R!)%妽聻A摳HK!1:5啓B?E4眆薃W觠龏*鬐氕 y麦2麅扩矅g ?J釨漞m1迥_^>攼D/邴堅 -眳腴JS%麄A;N.汤7鍩"E_玞. P" 蓇 呖eH胷 霛NJ)枠$p觎nW!壆溴\6)爄K/4豨忊b 䲡 麌徵阀躞偬萇#":5"答H (yhc彣$84Ξ] 歓H鎎牖Z.啝9碉揧q:大岟濚:6袰Ga啦'崅銒w 殖U仛M袊鱏(x饻al(ZtFレ╁烰囁p'沛9j笩h{氋顔阾砑涳$窶]m乘滸怒jHc脬$/覙n8h蹥Rs嶫跓珈簰瑯潍|狥4夹nX穱7`娘緰*軾臾r/鷏帵裟4鴻Z6[舙Z將a  [Q絷Tp絴5)|+H甁鸸奀(0爱'>庡(^噯伒]^ 系xu;a!把7郄希裝7酐:奒(E投"鞃(腻鲽觘爷䥺炥!tyZ5则L<簹@矡鴶D L <擏mf`I䥺qj蟴(e睈\*E喨鏟t=yKy襓{ s堿忭篑 Q枇籥,*B儁垠硰汰氄仑酈$範魀sQ`JF〨c綻熃瀁D瘧謒3諝娆D撘!耩#(y鸦pbuU馯罱郂貃D傰On轺-1q9w糄囃(->+憄/瑦赶v癡 -%:K∵岽萚藩&赾錣廍6b垝M 掋湜豝zo^蓹=-嗑sB~S1Mв斜 k蕸(Z) 毗 瀕崴j冮v&O_狣穷嶜忡J4玧浂T? Tn犺 鴿d?_7馬 |瑈Lh:崯偹嗷鞡nH`裈囇F~ l!禼9 06碔鯔瀝IbQ螃爅b鎏葅9;^娏yJ芦囋P !9轨A翐|/0[0秿懔!櫴襌羃- m-,棅躒ゴ擐驽Y吔譇湔峙酳^1怸a峫静e纬媒b韋a慪姮)-r橦H辏u謄誂紘响萖 肓塞h凪;靴樆鱛鵋嬗SP9聪F眚促(1钌椸劀[册N衧鈋&扮E╟镣#D>腆D磒"飤仞\旅瑩*<獁<珒禮.耝A譎7鏊!毷郤倐螑攬/5x奱:c5斝(=B釁M毥Fr0w烓8亢 cK?娙睚嵛)_8 Gg驥&G劰}J奱塦Arr1T`/蔷k>J>,ゴ镻n{坹#g6 cE:姷絾痲t8k燑麸9姹7^燢1 ~W艼緺~H7r=fも鴋z襰晜)|忠寯(.賾舱a驫3E窩6 8V﹌`赽,&#崨蘂梛蘝% <曠滒-鎕锣豘$昌呢橅$.﹛}Ouv,$X[忑;趲粪.>S}夒 2硱緝k9v娹(歞A211版-GDb鶺6*靻高紬 俭0妨獭x'1綳 隇b鉎铝銒y 牞9駖?b蘹R'Vz7[ ['N`糤qt秾e伃;q /劈嵈 劜焜wH椱#*]2,鑏Q窳<矢H钤稘8簯閡 u7r;:穙峋K V牞愾狂]d鰲}@k ¬O阯<羥>磛 %焦 婸R}横諨襓~U Mq潇徨I馶騯跽(添`0珉鯗熏媹@悧 选 邇YX续卩'虥衫ぢ@ J)邼=?;]W楼9 #fKx腹\鈕柙VO#i僨JntK'l錸禰 X$谕诊"3鯘处f蟀& i妵~q雵4布3号!盈 #祼0t⒀⺷矉勶),|脸O贐8坜0@斵Q嗑f蕖P5燡錊慯8 讕喲块蕆诫c8离)椺G'e鹲猩$沛2曚鈉腜,z挙鴟{荨胄嗜痹` 险颇й1鳅畟{钽ぐ鉳u顈{ 櫇*軋{濧柞Ч煶薋% 7馔鮤7焂鯒:蝊Q0婌8銰塔 N+跤]藲-L隔歧*ax稉骑拿损铌衜22眅仄碓4紥x{⑨%&祓v^葄荺8z詡筄xPC馤Ix柕]哴ρ従眣^,响斛撵禴=瓐H)噒f^鹥ms6干r}䴓夼Z9狡+ J3Ng⑹Xu牔啎驩灘塨`-/[OP虰$K駊Cx%牭?~唿K..淣~泧3爲杬 2卲&/嗆A異禶掍-<4U癆A0冯觎&~>壆 wY3曏驯o6as_+礜蜾稨1V茁$梞伺迿oI2弉[:R#B屻L`硬▇漞R阄鮬0y#"2 飚oY~鑁^怕E婇 a THD p磅乸 K锯-0俑6S罞瘦藂鱋y埈#拡B! Blo摼袨04臭~:逌罰-滘鉬og檻膨=巂.娂篈  儓./]攸Ud近J,}t9Kj鯒i!1鰔⒔鮮鶼X巄铎)vO絣{y癛 W奃梘!p-lQ盒] 瘴%泳-O愉'弛OG总&iK-3錯1銛虰?)H毃 jj>*%葞)銃韹峎秊軜休1N秄;竭7屁鋉蝎X$b派 ┷蚩Y蔥唋J^ $ 噺k}$曰栊K)嫶⿻[攔i铗滮伬R v3踾;u<垜$>*];齧+|~泴稌槙 岽序!3pK1讱号黐后2锺菁/甓jH衩u T7;(廴Hc遦枂F ⊙m e奫熙1,癤謍~穧8L凟F陿踎偘;N3譻7i饐+崗Z4远?&R跬{3k忢犠^$9牪+YF唎罉E+oワ瑳w帍詭0霉p*1(&涖踘6芴 抸゜挐z5 煶v話@級B繙;2諟祮x/J咄 2磹鞙^$[ endstream endobj 11 0 obj << /Producer (pdfTeX-1.40.13) /Creator (TeX) /CreationDate (D:20130413212535-07'00') /ModDate (D:20130413212535-07'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.1415926-2.4-1.40.13 (TeX Live 2012/Debian) kpathsea version 6.1.0) >> endobj 5 0 obj << /Type /ObjStm /N 7 /First 40 /Length 553 /Filter /FlateDecode >> stream x讠SKo汙倔+尕2妙虏+Y憀,渊I:Q镇佮- 貮W煽响怿{`53`4睦B"C妳 A ,+o:躔癍5鱕a荆%4->艆悲@JI4A﹟幄D$!伤E*鞵鈨oD竬@o筸},銺壌篽鳵:i&-(qs緾8[$gu豤澡*p-w攺觉賐乜0}轚硇t鉥剿v銲~尢掔/身抭瞣尝噋 樆橮w1蠮A'>w{)0哨*JR曫&本 澄!踁.硔y鴟绖E檜v> 樷5掴~'甜s虥. 鼌朮镜ォ甭-畎涄`嫮楠f兛泵茔熛ciE檎鵕焼BE韄#U3蟴銝裓寪漅Zu齚慂缴 c拭說({恨2;眄☆)妩VmzXq妇孡2aSx#鋞氾y0 endstream endobj 12 0 obj << /Type /XRef /Index [0 13] /Size 13 /W [1 2 1] /Root 10 0 R /Info 11 0 R /ID [<0D826DDE839BE08F0848B1953C6480A7> <0D826DDE839BE08F0848B1953C6480A7>] /Length 48 /Filter /FlateDecode >> stream x屏 妖"畾g7焕-瓓箅┤饪ウ毖<^  endstream endobj startxref 14390 %%EOF xapers-0.6/test/docs/all.bib000066400000000000000000000022601245427370400160260ustar00rootroot00000000000000@article{ arxiv:1235, author = "Dole, Bob and Cruise, Tim", title = "Creation of the 纬-verses", year = "2012", eprint = "1235", file = {:__DOC_DIR__/1.pdf:pdf} } @article{Good_Bad_Up_Down_Left_Right_et_al._2012, title={Multicolor cavity sadness}, volume={29}, url={http://dx.doi.org/10.9999/FOO.1}, DOI={10.9999/FOO.1}, number={10}, journal={Journal of the Color Feelings}, publisher={Optical Society of America}, author={Good, Bob and Bad, Sam and Up, Steve and Down, Joseph and Left, Aidan and Right, Kate and et al.}, year={2012}, month={Sep}, pages={2092}, file={:__DOC_DIR__/2 file.pdf:pdf}} @article{ fake:1234, author = "Reed, Lou and Bj枚rk", title = "When the liver meats the pavement", year = "1980", journal = "fake", file = {:__DOC_DIR__/5.pdf:} } @article{30929234, title={The Circle and the Square: Forbidden Love}, url={http://dx.doi.org/10.9999/FOO.2}, DOI={10.9999/FOO.2}, file={:}, journal={Shaply Letters}, author={Me and You and We Know, Everyone}, year={1869}} @article{30929, title={Circle are Squares}, url={http://dx.doi.org/10.9999/FOO.3}, DOI={10.9999/FOO.3}, journal={Sharp Letters}, author={Me and You}, year={1869}} xapers-0.6/test/import000077500000000000000000000051271245427370400151150ustar00rootroot00000000000000#!/usr/bin/env bash test_description='bibtex database importing.' . ./test-lib.sh ################################################################ test_expect_code 1 'fail import without bibtex' \ 'xapers import' sed "s|__DOC_DIR__|$DOC_DIR|g" <"$DOC_DIR"/all.bib >all.bib # the following two tests provides entries so we can test that import # updates existing entries test_begin_subtest 'add initial documents' xapers add --tags=foo --source="$DOC_DIR"/2.bib xapers add --tags=bar --source="$DOC_DIR"/3.bib xapers search '*' >OUTPUT cat <EXPECTED id:2 [] {fake:1234} (bar) "When the liver meats the pavement" id:1 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (foo) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'import full bibtex with files' xapers import --tags=new all.bib xapers search '*' >OUTPUT cat <EXPECTED id:5 [arxiv:1235] {arxiv:1235} (new) "Creation of the 纬-verses" id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:3 [doi:10.9999/FOO.3] {30929} (new) "Circle are Squares" id:2 [] {fake:1234} (bar new) "When the liver meats the pavement" id:1 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (foo new) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search id:' xapers search id:5 >OUTPUT cat <EXPECTED id:5 [arxiv:1235] {arxiv:1235} (new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search bib:' xapers search key:30929234 >OUTPUT cat <EXPECTED id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 'search text' xapers search --output=summary lorem >OUTPUT cat <EXPECTED id:2 [] {fake:1234} (bar new) "When the liver meats the pavement" id:5 [arxiv:1235] {arxiv:1235} (new) "Creation of the 纬-verses" EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest 're-import produces identical results' xapers import --tags=new all.bib xapers search '*' >OUTPUT cat <EXPECTED id:5 [arxiv:1235] {arxiv:1235} (new) "Creation of the 纬-verses" id:4 [doi:10.9999/FOO.2] {30929234} (new) "The Circle and the Square: Forbidden Love" id:3 [doi:10.9999/FOO.3] {30929} (new) "Circle are Squares" id:2 [] {fake:1234} (bar new) "When the liver meats the pavement" id:1 [doi:10.9999/FOO.1] {Good_Bad_Up_Down_Left_Right_et_al._2012} (foo new) "Multicolor cavity sadness" EOF test_expect_equal_file OUTPUT EXPECTED ################################################################ test_done xapers-0.6/test/sources000077500000000000000000000027541245427370400152710ustar00rootroot00000000000000#!/usr/bin/env bash test_description='Sources interface' . ./test-lib.sh export XAPERS_SOURCE_PATH=':' ################################################################ # FIXME: add test for source2bib # FIXME: add test for scandoc test_begin_subtest 'list sources' xapers sources | sort >OUTPUT cat <EXPECTED dcc: LIGO Document Control Center (https://dcc.ligo.org/) [builtin] doi: Digital Object Identifier (https://dx.doi.org/) [builtin] arxiv: Open access e-print service (http://arxiv.org/) [builtin] cryptoeprint: Cryptology ePrint Archive (https://eprint.iacr.org/) [builtin] EOF test_expect_equal_file OUTPUT EXPECTED # test_begin_subtest 'source2bib doi' # xapers source2bib 'doi:10.1364/JOSAA.29.002092' >OUTPUT # cat <EXPECTED # @article{Izumi_2012, # author = "Izumi, Kiwamu and Arai, Koji and Barr, Bryan and Betzwieser, Joseph and Brooks, Aidan and Dahl, Katrin and Doravari, Suresh and Driggers, Jennifer C. and Korth, W. Zach and Miao, Haixing and et al.", # title = "Multicolor cavity metrology", # volume = "29", # ISSN = "1520-8532", # url = "http://dx.doi.org/10.1364/JOSAA.29.002092", # DOI = "10.1364/josaa.29.002092", # number = "10", # journal = "Journal of the Optical Society of America A", # publisher = "Optical Society of America (OSA)", # year = "2012", # pages = "2092" # } # EOF # test_expect_equal_file OUTPUT EXPECTED ################################################################ test_done xapers-0.6/test/test-aggregate-results000077500000000000000000000027561245427370400202120ustar00rootroot00000000000000#!/usr/bin/env bash fixed=0 success=0 failed=0 broken=0 total=0 for file do while read type value do case $type in '') continue ;; fixed) fixed=$(($fixed + $value)) ;; success) success=$(($success + $value)) ;; failed) failed=$(($failed + $value)) ;; broken) broken=$(($broken + $value)) ;; total) total=$(($total + $value)) ;; esac done <"$file" done pluralize () { case $2 in 1) case $1 in test) echo test ;; failure) echo failure ;; esac ;; *) case $1 in test) echo tests ;; failure) echo failures ;; esac ;; esac } echo "Xapers test suite complete." if [ "$fixed" = "0" ] && [ "$failed" = "0" ]; then tests=$(pluralize "test" $total) printf "All $total $tests " if [ "$broken" = "0" ]; then echo "passed." else failures=$(pluralize "failure" $broken) echo "behaved as expected ($broken expected $failures)." fi; else echo "$success/$total tests passed." if [ "$broken" != "0" ]; then tests=$(pluralize "test" $broken) echo "$broken broken $tests failed as expected." fi if [ "$fixed" != "0" ]; then tests=$(pluralize "test" $fixed) echo "$fixed broken $tests now fixed." fi if [ "$failed" != "0" ]; then tests=$(pluralize "test" $failed) echo "$failed $tests failed." fi fi skipped=$(($total - $fixed - $success - $failed - $broken)) if [ "$skipped" != "0" ]; then tests=$(pluralize "test" $skipped) echo "$skipped $tests skipped." fi xapers-0.6/test/test-lib.sh000066400000000000000000000444251245427370400157400ustar00rootroot00000000000000# # Copyright (c) 2005 Junio C Hamano # # 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 2 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/ . if [ ${BASH_VERSINFO[0]} -lt 4 ]; then echo "Error: The notmuch test suite requires a bash version >= 4.0" echo "due to use of associative arrays within the test suite." echo "Please try again with a newer bash (or help us fix the" echo "test suite to be more portable). Thanks." exit 1 fi # if --tee was passed, write the output not only to the terminal, but # additionally to the file test-results/$BASENAME.out, too. case "$GIT_TEST_TEE_STARTED, $* " in done,*) # do not redirect again ;; *' --tee '*|*' --va'*) mkdir -p test-results BASE=test-results/$(basename "$0" .sh) (GIT_TEST_TEE_STARTED=done ${SHELL-sh} "$0" "$@" 2>&1; echo $? > $BASE.exit) | tee $BASE.out test "$(cat $BASE.exit)" = 0 exit ;; esac # Keep the original TERM for say_color and test_emacs ORIGINAL_TERM=$TERM # For repeatability, reset the environment to known value. LANG=C LC_ALL=C PAGER=cat TZ=UTC TERM=dumb export LANG LC_ALL PAGER TERM TZ GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u} TEST_EMACS=${TEST_EMACS:-${EMACS:-emacs}} # Protect ourselves from common misconfiguration to export # CDPATH into the environment unset CDPATH unset GREP_OPTIONS # Convenience # # A regexp to match 5 and 40 hexdigits _x05='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' _x40="$_x05$_x05$_x05$_x05$_x05$_x05$_x05$_x05" _x04='[0-9a-f][0-9a-f][0-9a-f][0-9a-f]' _x32="$_x04$_x04$_x04$_x04$_x04$_x04$_x04$_x04" # Each test should start with something like this, after copyright notices: # # test_description='Description of this test... # This test checks if command xyzzy does the right thing... # ' # . ./test-lib.sh [ "x$ORIGINAL_TERM" != "xdumb" ] && ( TERM=$ORIGINAL_TERM && export TERM && [ -t 1 ] && tput bold >/dev/null 2>&1 && tput setaf 1 >/dev/null 2>&1 && tput sgr0 >/dev/null 2>&1 ) && color=t while test "$#" -ne 0 do case "$1" in -d|--d|--de|--deb|--debu|--debug) debug=t; shift ;; -i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate) immediate=t; shift ;; -l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests) GIT_TEST_LONG=t; export GIT_TEST_LONG; shift ;; -h|--h|--he|--hel|--help) help=t; shift ;; -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) verbose=t; shift ;; -q|--q|--qu|--qui|--quie|--quiet) quiet=t; shift ;; --with-dashes) with_dashes=t; shift ;; --no-color) color=; shift ;; --no-python) # noop now... shift ;; --va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind) valgrind=t; verbose=t; shift ;; --tee) shift ;; # was handled already --root=*) root=$(expr "z$1" : 'z[^=]*=\(.*\)') shift ;; *) echo "error: unknown test option '$1'" >&2; exit 1 ;; esac done if test -n "$debug"; then print_subtest () { printf " %-4s" "[$((test_count - 1))]" } else print_subtest () { true } fi if test -n "$color"; then say_color () { ( TERM=$ORIGINAL_TERM export TERM case "$1" in error) tput bold; tput setaf 1;; # bold red skip) tput bold; tput setaf 2;; # bold green pass) tput setaf 2;; # green info) tput setaf 3;; # brown *) test -n "$quiet" && return;; esac shift printf " " printf "$@" tput sgr0 print_subtest ) } else say_color() { test -z "$1" && test -n "$quiet" && return shift printf " " printf "$@" print_subtest } fi error () { say_color error "error: $*\n" GIT_EXIT_OK=t exit 1 } say () { say_color info "$*" } test "${test_description}" != "" || error "Test script did not set test_description." if test "$help" = "t" then echo "Tests ${test_description}" exit 0 fi echo $(basename "$0"): "Testing ${test_description}" exec 5>&1 test_failure=0 test_count=0 test_fixed=0 test_broken=0 test_success=0 die () { code=$? rm -rf "$TEST_TMPDIR" if test -n "$GIT_EXIT_OK" then exit $code else echo >&5 "FATAL: Unexpected exit with code $code" exit 1 fi } GIT_EXIT_OK= # Note: TEST_TMPDIR *NOT* exported! TEST_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/test-$$.XXXXXX") trap 'die' EXIT test_decode_color () { sed -e 's/.\[1m//g' \ -e 's/.\[31m//g' \ -e 's/.\[32m//g' \ -e 's/.\[33m//g' \ -e 's/.\[34m//g' \ -e 's/.\[35m//g' \ -e 's/.\[36m//g' \ -e 's/.\[m//g' } q_to_nul () { perl -pe 'y/Q/\000/' } q_to_cr () { tr Q '\015' } append_cr () { sed -e 's/$/Q/' | tr Q '\015' } remove_cr () { tr '\015' Q | sed -e 's/Q$//' } test_begin_subtest () { if [ -n "$inside_subtest" ]; then exec 1>&6 2>&7 # Restore stdout and stderr error "bug in test script: Missing test_expect_equal in ${BASH_SOURCE[1]}:${BASH_LINENO[0]}" fi test_subtest_name="$1" test_reset_state_ # Remember stdout and stderr file descriptors and redirect test # output to the previously prepared file descriptors 3 and 4 (see # below) if test "$verbose" != "t"; then exec 4>test.output 3>&4; fi exec 6>&1 7>&2 >&3 2>&4 inside_subtest=t } # Pass test if two arguments match # # Note: Unlike all other test_expect_* functions, this function does # not accept a test name. Instead, the caller should call # test_begin_subtest before calling this function in order to set the # name. test_expect_equal () { exec 1>&6 2>&7 # Restore stdout and stderr inside_subtest= test "$#" = 3 && { prereq=$1; shift; } || prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_equal" output="$1" expected="$2" if ! test_skip "$test_subtest_name" then if [ "$output" = "$expected" ]; then test_ok_ "$test_subtest_name" else testname=$this_test.$test_count echo "$expected" > $testname.expected echo "$output" > $testname.output test_failure_ "$test_subtest_name" "$(diff -u $testname.expected $testname.output)" fi fi } # Like test_expect_equal, but takes two filenames. test_expect_equal_file () { exec 1>&6 2>&7 # Restore stdout and stderr inside_subtest= test "$#" = 3 && { prereq=$1; shift; } || prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_equal" output="$1" expected="$2" if ! test_skip "$test_subtest_name" then if diff -q "$expected" "$output" >/dev/null ; then test_ok_ "$test_subtest_name" else testname=$this_test.$test_count cp "$output" $testname.output cp "$expected" $testname.expected test_failure_ "$test_subtest_name" "$(diff -u $testname.expected $testname.output)" fi fi } # Like test_expect_equal, but arguments are JSON expressions to be # canonicalized before diff'ing. If an argument cannot be parsed, it # is used unchanged so that there's something to diff against. test_expect_equal_json () { output=$(echo "$1" | python -mjson.tool || echo "$1") expected=$(echo "$2" | python -mjson.tool || echo "$2") shift 2 test_expect_equal "$output" "$expected" "$@" } # Use test_set_prereq to tell that a particular prerequisite is available. # The prerequisite can later be checked for in two ways: # # - Explicitly using test_have_prereq. # # - Implicitly by specifying the prerequisite tag in the calls to # test_expect_{success,failure,code}. # # The single parameter is the prerequisite tag (a simple word, in all # capital letters by convention). test_set_prereq () { satisfied="$satisfied$1 " } satisfied=" " test_have_prereq () { case $satisfied in *" $1 "*) : yes, have it ;; *) ! : nope ;; esac } # declare prerequisite for the given external binary test_declare_external_prereq () { binary="$1" test "$#" = 2 && name=$2 || name="$binary(1)" hash $binary 2>/dev/null || eval " test_missing_external_prereq_${binary}_=t $binary () { echo -n \"\$test_subtest_missing_external_prereqs_ \" | grep -qe \" $name \" || test_subtest_missing_external_prereqs_=\"\$test_subtest_missing_external_prereqs_ $name\" false }" } # Explicitly require external prerequisite. Useful when binary is # called indirectly (e.g. from emacs). # Returns success if dependency is available, failure otherwise. test_require_external_prereq () { binary="$1" if [ "$(eval echo -n \$test_missing_external_prereq_${binary}_)" = t ]; then # dependency is missing, call the replacement function to note it eval "$binary" else true fi } # You are not expected to call test_ok_ and test_failure_ directly, use # the text_expect_* functions instead. test_ok_ () { if test "$test_subtest_known_broken_" = "t"; then test_known_broken_ok_ "$@" return fi test_success=$(($test_success + 1)) say_color pass "%-6s" "PASS" echo " $@" } test_failure_ () { if test "$test_subtest_known_broken_" = "t"; then test_known_broken_failure_ "$@" return fi test_failure=$(($test_failure + 1)) test_failure_message_ "FAIL" "$@" test "$immediate" = "" || { GIT_EXIT_OK=t; exit 1; } return 1 } test_failure_message_ () { say_color error "%-6s" "$1" echo " $2" shift 2 echo "$@" | sed -e 's/^/ /' if test "$verbose" != "t"; then cat test.output; fi } test_known_broken_ok_ () { test_reset_state_ test_fixed=$(($test_fixed+1)) say_color pass "%-6s" "FIXED" echo " $@" } test_known_broken_failure_ () { test_reset_state_ test_broken=$(($test_broken+1)) test_failure_message_ "BROKEN" "$@" return 1 } test_debug () { test "$debug" = "" || eval "$1" } test_run_ () { test_cleanup=: if test "$verbose" != "t"; then exec 4>test.output 3>&4; fi eval >&3 2>&4 "$1" eval_ret=$? eval >&3 2>&4 "$test_cleanup" return 0 } test_skip () { test_count=$(($test_count+1)) to_skip= for skp in $XAPERS_SKIP_TESTS do case $this_test.$test_count in $skp) to_skip=t esac done if test -z "$to_skip" && test -n "$prereq" && ! test_have_prereq "$prereq" then to_skip=t fi case "$to_skip" in t) test_report_skip_ "$@" ;; *) test_check_missing_external_prereqs_ "$@" ;; esac } test_check_missing_external_prereqs_ () { if test -n "$test_subtest_missing_external_prereqs_"; then say_color skip >&1 "missing prerequisites:" echo "$test_subtest_missing_external_prereqs_" >&1 test_report_skip_ "$@" else false fi } test_report_skip_ () { test_reset_state_ say_color skip >&3 "skipping test:" echo " $@" >&3 say_color skip "%-6s" "SKIP" echo " $1" } test_subtest_known_broken () { test_subtest_known_broken_=t } test_expect_success () { test "$#" = 3 && { prereq=$1; shift; } || prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test-expect-success" test_reset_state_ if ! test_skip "$@" then test_run_ "$2" run_ret="$?" # test_run_ may update missing external prerequisites test_check_missing_external_prereqs_ "$@" || if [ "$run_ret" = 0 -a "$eval_ret" = 0 ] then test_ok_ "$1" else test_failure_ "$@" fi fi } test_expect_code () { test "$#" = 4 && { prereq=$1; shift; } || prereq= test "$#" = 3 || error "bug in the test script: not 3 or 4 parameters to test-expect-code" test_reset_state_ if ! test_skip "$@" then test_run_ "$3" run_ret="$?" # test_run_ may update missing external prerequisites, test_check_missing_external_prereqs_ "$@" || if [ "$run_ret" = 0 -a "$eval_ret" = "$1" ] then test_ok_ "$2" else test_failure_ "$@" fi fi } # test_external runs external test scripts that provide continuous # test output about their progress, and succeeds/fails on # zero/non-zero exit code. It outputs the test output on stdout even # in non-verbose mode, and announces the external script with "* run # : ..." before running it. When providing relative paths, keep in # mind that all scripts run in "trash directory". # Usage: test_external description command arguments... # Example: test_external 'Perl API' perl ../path/to/test.pl test_external () { test "$#" = 4 && { prereq=$1; shift; } || prereq= test "$#" = 3 || error >&5 "bug in the test script: not 3 or 4 parameters to test_external" descr="$1" shift test_reset_state_ if ! test_skip "$descr" "$@" then # Announce the script to reduce confusion about the # test output that follows. say_color "" " run $test_count: $descr ($*)" # Run command; redirect its stderr to &4 as in # test_run_, but keep its stdout on our stdout even in # non-verbose mode. "$@" 2>&4 if [ "$?" = 0 ] then test_ok_ "$descr" else test_failure_ "$descr" "$@" fi fi } # Like test_external, but in addition tests that the command generated # no output on stderr. test_external_without_stderr () { # The temporary file has no (and must have no) security # implications. tmp="$TMPDIR"; if [ -z "$tmp" ]; then tmp=/tmp; fi stderr="$tmp/git-external-stderr.$$.tmp" test_external "$@" 4> "$stderr" [ -f "$stderr" ] || error "Internal error: $stderr disappeared." descr="no stderr: $1" shift if [ ! -s "$stderr" ]; then rm "$stderr" test_ok_ "$descr" else if [ "$verbose" = t ]; then output=`echo; echo Stderr is:; cat "$stderr"` else output= fi # rm first in case test_failure exits. rm "$stderr" test_failure_ "$descr" "$@" "$output" fi } # This is not among top-level (test_expect_success) # but is a prefix that can be used in the test script, like: # # test_expect_success 'complain and die' ' # do something && # do something else && # test_must_fail git checkout ../outerspace # ' # # Writing this as "! git checkout ../outerspace" is wrong, because # the failure could be due to a segv. We want a controlled failure. test_must_fail () { "$@" test $? -gt 0 -a $? -le 129 -o $? -gt 192 } # test_cmp is a helper function to compare actual and expected output. # You can use it like: # # test_expect_success 'foo works' ' # echo expected >expected && # foo >actual && # test_cmp expected actual # ' # # This could be written as either "cmp" or "diff -u", but: # - cmp's output is not nearly as easy to read as diff -u # - not all diff versions understand "-u" test_cmp() { $GIT_TEST_CMP "$@" } # This function can be used to schedule some commands to be run # unconditionally at the end of the test to restore sanity: # # test_expect_success 'test core.capslock' ' # git config core.capslock true && # test_when_finished "git config --unset core.capslock" && # hello world # ' # # That would be roughly equivalent to # # test_expect_success 'test core.capslock' ' # git config core.capslock true && # hello world # git config --unset core.capslock # ' # # except that the greeting and config --unset must both succeed for # the test to pass. test_when_finished () { test_cleanup="{ $* } && (exit \"\$eval_ret\"); eval_ret=\$?; $test_cleanup" } test_done () { GIT_EXIT_OK=t test_results_dir="$TEST_DIRECTORY/test-results" mkdir -p "$test_results_dir" test_results_path="$test_results_dir/${0%.sh}-$$" echo "total $test_count" >> $test_results_path echo "success $test_success" >> $test_results_path echo "fixed $test_fixed" >> $test_results_path echo "broken $test_broken" >> $test_results_path echo "failed $test_failure" >> $test_results_path echo "" >> $test_results_path echo [ -n "$EMACS_SERVER" ] && test_emacs '(kill-emacs)' if [ "$test_failure" = "0" ]; then if [ "$test_broken" = "0" ]; then rm -rf "$remove_tmp" fi exit 0 else exit 1 fi } test_python() { export LD_LIBRARY_PATH=$TEST_DIRECTORY/../lib export PYTHONPATH=$TEST_DIRECTORY/../bindings/python # Some distros (e.g. Arch Linux) ship Python 2.* as /usr/bin/python2, # most others as /usr/bin/python. So first try python2, and fallback to # python if python2 doesn't exist. cmd=python2 [[ "$test_missing_external_prereq_python2_" = t ]] && cmd=python (echo "import sys; _orig_stdout=sys.stdout; sys.stdout=open('OUTPUT', 'w')"; cat) \ | $cmd - } test_reset_state_ () { test -z "$test_init_done_" && test_init_ test_subtest_known_broken_= test_subtest_missing_external_prereqs_= } # called once before the first subtest test_init_ () { test_init_done_=t # skip all tests if there were external prerequisites missing during init test_check_missing_external_prereqs_ "all tests in $this_test" && test_done } # Test the binaries we have just built. The tests are kept in # test/ subdirectory and are run in 'trash directory' subdirectory. TEST_DIRECTORY=$(pwd) export PATH # Test repository test="tmp.$(basename "$0" .sh)" test -n "$root" && test="$root/$test" case "$test" in /*) TMP_DIRECTORY="$test" ;; *) TMP_DIRECTORY="$TEST_DIRECTORY/$test" ;; esac test ! -z "$debug" || remove_tmp=$TMP_DIRECTORY rm -fr "$test" || { GIT_EXIT_OK=t echo >&5 "FATAL: Cannot prepare test area" exit 1 } mkdir -p "${test}" # load local test library . ./test-local.sh # Use -P to resolve symlinks in our working directory so that the cwd # in subprocesses like git equals our $PWD (for pathname comparisons). cd -P "$test" || error "Cannot setup test environment" if test "$verbose" = "t" then exec 4>&2 3>&1 else exec 4>test.output 3>&4 fi this_test=${0##*/} for skp in $XAPERS_SKIP_TESTS do to_skip= for skp in $XAPERS_SKIP_TESTS do case "$this_test" in $skp) to_skip=t esac done case "$to_skip" in t) say_color skip >&3 "skipping test $this_test altogether" say_color skip "skip all tests in $this_test" test_done esac done # Provide an implementation of the 'yes' utility yes () { if test $# = 0 then y=y else y="$*" fi while echo "$y" do : done } # Fix some commands on Windows case $(uname -s) in *MINGW*) # Windows has its own (incompatible) sort and find sort () { /usr/bin/sort "$@" } find () { /usr/bin/find "$@" } sum () { md5sum "$@" } # git sees Windows-style pwd pwd () { builtin pwd -W } # no POSIX permissions # backslashes in pathspec are converted to '/' # exec does not inherit the PID ;; *) test_set_prereq POSIXPERM test_set_prereq BSLASHPSPEC test_set_prereq EXECKEEPSPID ;; esac test -z "$NO_PERL" && test_set_prereq PERL test -z "$NO_PYTHON" && test_set_prereq PYTHON # test whether the filesystem supports symbolic links ln -s x y 2>/dev/null && test -h y 2>/dev/null && test_set_prereq SYMLINKS rm -f y xapers-0.6/test/test-local.sh000066400000000000000000000004711245427370400162550ustar00rootroot00000000000000# declare prerequisites for external binaries used in tests test_declare_external_prereq python test_declare_external_prereq python2 export PATH="$TEST_DIRECTORY"/../bin:$PATH export PYTHONPATH="$TEST_DIRECTORY"/../lib:$PYTHONPATH export DOC_DIR="$TEST_DIRECTORY/docs" export XAPERS_ROOT="$TMP_DIRECTORY/docs" xapers-0.6/test/test-verbose000077500000000000000000000013131245427370400162160ustar00rootroot00000000000000#!/usr/bin/env bash test_description='the verbosity options of the test framework itself.' . ./test-lib.sh test_expect_success 'print something in test_expect_success and pass' ' echo "hello stdout" && echo "hello stderr" >&2 && true ' test_expect_success 'print something in test_expect_success and fail' ' echo "hello stdout" && echo "hello stderr" >&2 && false ' test_begin_subtest 'print something between test_begin_subtest and test_expect_equal and pass' echo "hello stdout" echo "hello stderr" >&2 test_expect_equal "a" "a" test_begin_subtest 'print something test_begin_subtest and test_expect_equal and fail' echo "hello stdout" echo "hello stderr" >&2 test_expect_equal "a" "b" test_done xapers-0.6/test/test.expected-output/000077500000000000000000000000001245427370400177655ustar00rootroot00000000000000xapers-0.6/test/test.expected-output/test-verbose-no000066400000000000000000000011451245427370400227450ustar00rootroot00000000000000test-verbose: Testing the verbosity options of the test framework itself. PASS print something in test_expect_success and pass FAIL print something in test_expect_success and fail echo "hello stdout" && echo "hello stderr" >&2 && false hello stdout hello stderr PASS print something between test_begin_subtest and test_expect_equal and pass FAIL print something test_begin_subtest and test_expect_equal and fail --- test-verbose.4.expected 2010-11-14 21:41:12.738189710 +0000 +++ test-verbose.4.output 2010-11-14 21:41:12.738189710 +0000 @@ -1 +1 @@ -b +a hello stdout hello stderr xapers-0.6/test/test.expected-output/test-verbose-yes000066400000000000000000000012311245427370400231250ustar00rootroot00000000000000test-verbose: Testing the verbosity options of the test framework itself. hello stdout hello stderr PASS print something in test_expect_success and pass hello stdout hello stderr FAIL print something in test_expect_success and fail echo "hello stdout" && echo "hello stderr" >&2 && false hello stdout hello stderr PASS print something between test_begin_subtest and test_expect_equal and pass hello stdout hello stderr FAIL print something test_begin_subtest and test_expect_equal and fail --- test-verbose.4.expected 2010-11-14 21:41:06.650023289 +0000 +++ test-verbose.4.output 2010-11-14 21:41:06.650023289 +0000 @@ -1 +1 @@ -b +a xapers-0.6/test/xapers-test000077500000000000000000000020441245427370400160550ustar00rootroot00000000000000#!/usr/bin/env bash # Run tests # # Copyright (c) 2005 Junio C Hamano # # Adapted from a Makefile to a shell script by Carl Worth (2010) if [ ${BASH_VERSINFO[0]} -lt 4 ]; then echo "Error: The notmuch test suite requires a bash version >= 4.0" echo "due to use of associative arrays within the test suite." echo "Please try again with a newer bash (or help us fix the" echo "test suite to be more portable). Thanks." exit 1 fi cd $(dirname "$0") TESTS=" basic sources all import " # setup TESTS=${XAPERS_TESTS:=$TESTS} # Clean up any results from a previous run rm -rf test-results docs/.xapers # test for timeout utility if command -v timeout >/dev/null; then TEST_TIMEOUT_CMD="timeout 2m " echo "INFO: using 2 minute timeout for tests" else TEST_TIMEOUT_CMD="" fi trap 'e=$?; kill $!; exit $e' HUP INT TERM # Run the tests for test in $TESTS; do $TEST_TIMEOUT_CMD ./$test "$@" & wait $! done trap - HUP INT TERM # Report results ./test-aggregate-results test-results/* # Clean up rm -rf test-result