pax_global_header00006660000000000000000000000064145762474350014533gustar00rootroot0000000000000052 comment=c3baf30074ef9050f8dd42eee9ea9a0dd1c95759 hyperorg/000077500000000000000000000000001457624743500127365ustar00rootroot00000000000000hyperorg/.gitignore000066400000000000000000000006311457624743500147260ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2023 Christian BUHTZ # # SPDX-License-Identifier: CC0-1.0 # # This file is part of the program "Hyperorg". Unlike the program, this file is # licensed under Creative Commons Zero v1 (CC0-1.0). See folder LICENSES or go # to __pycache__ .coverage notes build .*cache *.egg-info _*.py \#*.py\# .\#*.py .venv hyperorg/CHANGELOG.md000066400000000000000000000015421457624743500145510ustar00rootroot00000000000000 # Changelog [![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org) ## [0.1.0] - 2024-03-19 First stable release of _Hyperorg_. Its development started exactly two years ago on 19th March 2022. ### Added - Convert org files into W3C conform HTML5 and CSS - Keep interlinks between nodes - Backlinks at the end of each node - Add index.html to list all nodes - Support node aliases and filetags - Support attachments and images as external files hyperorg/CONTRIBUTING.md000066400000000000000000000074761457624743500152050ustar00rootroot00000000000000 # How to contribute to _Hyperog_ 😊 **Thanks for taking the time to contribute!** The maintenance team will welcome all types of contributions. No contribution will be rejected just because it doesn't fit to our quality standards, guidelines or rules. We are willing to guide and mentorship new contributors through the process. March 2024 ## Table of Contents - [How to submit changes and report bugs?](#how-to-submit-changes-and-report-bugs) - [Setup environment, installing and testing](#setup-environment-installing-and-testing) - [Recommendations & Best practices](#recommendations-best-practices) - [Licensing of contributed material](#licensing-of-contributed-material) # How to submit changes and report bugs? These external resources explain how to accomplishing various tasks through contribution: - [Opening an issue or bug report](https://opensource.guide/how-to-contribute/#opening-an-issue) - [Opening a pull request](https://opensource.guide/how-to-contribute/#opening-a-pull-request) # Setup environment, installing and testing Fork the repository and clone it to your local machine. Please **always make a new branch** when preparing a _Pull Request_ ("PR") and **baseline it on** the repositories default branch `latest`. $ git clone git@codeberg.org:YOUR_USERNAME/hyperorg.git $ cd hyperorg $ git checkout --branch mynewfeature Create a virtual environment, activate it and install _Hyperorg_ (dependencies should be handled automatically by `pip`): $ python3 -m venv .venv (.venv) $ source venv/bin/activate (.venv) $ python3 -m pip install --editable .[develop] Run the test suite using `unittest` or `pytest` with one of the two following commands: (.venv) $ python3 -m unittest (.venv) $ pytest Create and add your modifications and push it to your remote repository: (.venv) $ edit myfancynewfeature.py (.venv) $ git add myfancynewfeature.py (.venv) $ git commit -am 'feat: fancy colors' (.venv) $ git push Go to your repository at [Codeberg.org](https://codeberg.org) and open a _Pull Request_. # Recommendations & Best practices Please take the following best practices into account if possible (to reduce the work load of the maintainers and to increase the chance that your pull request is accepted): - Follow [PEP 8](https://peps.python.org/pep-0008/) as a minimal Style Guide for Python Code. - Follow [Google Style Guide](https://sphinxcontrib-napoleon.readthedocs.org/en/latest/example_google.html) for docstrings. See also [this example in PyDoctor's documentation](https://pydoctor.readthedocs.io/en/latest/docformat/google_demo/index.html). - Using automatic formatters like `black` is not recommended. Please **mention the use** of them when opening a pull request. - Run the test suite before you open a Pull Request. - Try to create new tests if appropriate. Use Python's regular `unittest` instead of `pytest`. If you know the difference please try follow the _Classical (aka Detroit) school_ instead of _London (aka mockist) school_. # Licensing of contributed material Information in mind as you contribute, that code, docs and other material submitted to the project are considered licensed under the same terms (see [LICENSES](LICENSES)) as the rest of the work. The project does use the specifications from [REUSE Software](https://reuse.software) and [SPDX](https://spdx.github.io/spdx-spec) to store license and copyright information. March 2024 hyperorg/LICENSES/000077500000000000000000000000001457624743500141435ustar00rootroot00000000000000hyperorg/LICENSES/CC0-1.0.txt000066400000000000000000000156101457624743500155500ustar00rootroot00000000000000Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. hyperorg/LICENSES/GPL-3.0-only.txt000066400000000000000000001035561457624743500166150ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright © 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 . hyperorg/README.md000066400000000000000000000236471457624743500142310ustar00rootroot00000000000000 # Hyperorg - An Org to HTML converter _Hyperorg_ converts files from [GNU Emacs's](https://www.gnu.org/software/emacs/) [Org Mode](https://orgmode.org/) and [Org-roam](https://www.orgroam.com/) into [HTML](https://en.wikipedia.org/wiki/HTML) format. The resulted HTML files are intended for serverless local use. The creation of _Hyperorg_ was driven by the experience that there is no known solution available to reliably handle the *ID-Links* in *Org-roam v2* nodes. **Status of the project**: It is active and in constant development implementing [new features](https://codeberg.org/buhtz/hyperorg/issues?q=&state=open&labels=40047) and fixing [bugs](https://codeberg.org/buhtz/hyperorg/issues?q=&state=open&labels=40046). See [milestones](https://codeberg.org/buhtz/hyperorg/milestones) about what is coming and the [changelog](https://codeberg.org/buhtz/hyperorg/src/branch/latest/CHANGELOG.md) about what has been. Don't hesitate to voice your wishes and opinions by utilizing the [issue section](https://codeberg.org/buhtz/hyperorg/issues). **Technologies & Standards** - [Python 3](https://python.org) - [`orgparse`](https://orgparse.readthedocs.io/en/latest/) - [W3C](https://www.w3.org/) conform [HTML5](https://wikipedia.org/wiki/HTML5) & [CSS](https://wikipedia.org/wiki/CSS) - [Semantic Versioning](https://semver.org), [Common Changelog](https://common-changelog.org) and [Conventional Commits](https://www.conventionalcommits.org) - [REUSE Software](https://reuse.software) and [SPDX](https://spdx.github.io/spdx-spec) specifications March 2024 ## Table of contents - [Installation](#installation) - [Usage](#usage) - [How to ask questions, report bugs or suggest new features?](#how-to-ask-questions-report-bugs-or-suggest-new-features) - [Donations](#donations) - [Contributing](#contributing) - [Example](#example) # Installation See the [Releases](https://codeberg.org/buhtz/hyperorg/releases) section or run the following command to install the _latest stable release_ from upstream repository: ``` $ pipx install https://codeberg.org/buhtz/hyperorg/archive/v0.1.0.zip ``` If `pipx` is not available on your system check your GNU/Linux distributions package repository or install it from PyPi (`pip install pipx`). _Hyperorg_ is [not yet available](https://repology.org/project/hyperorg) in a GNU Linux distribution and there are no plans to release it on PyPi. # Usage ``` $ hyperorg ~/orgfolder ~/htmlfolder ``` This will take all `*.org` files in the folder `~/orgfolder` and store them converted to HTML in `~/htmlfolder`. Open the `index.html` file in the output folder to see the result. These are all available options: ``` usage: hyperorg [-h] [--hardlinks] [-s] [-v] [-d] [--version] inputdir outputdir positional arguments: inputdir Source directory with org-files. outputdir Destination directory for html-files. optional arguments: -h, --help show this help message and exit --hardlinks Use hardlinks instead of symlinks for images and other attachments in the input directory. -s, --show Open result in default browser. -v, --verbose Give detailed information. -d, --debug Debug output. --version show program's version number and exit ``` # How to ask questions, report bugs or suggest new features? The project is in its nascent stage and currently has a modest user base. Any issues, questions, or discussions can be directed to the [Issues](https://codeberg.org/buhtz/hyperorg/issues) section. In addition reporting _bugs_ and _requesting features_, this section accommodates all inquiries and conversations related to the project's development and usage. # Donations Currently, development of _Hyperorg_ is driven by one individual in their spare time. The infrastructure being used does not incur any costs for this project. Consequently, small donations do not significantly contribute to the project's progress. To financially compensate the developer's time, donations would need to be at a level that is currently not anticipated. Therefore, it is recommended to financially support projects and software that are relevant and essential for the infrastructure and further development of _Hyperorg_. - [Codeberg](https://docs.codeberg.org/improving-codeberg/donate/) for hosting that project. They use [Forgejo](https://forgejo.org/) on their servers and are involved in its development. - [Debian GNU/Linux](https://www.debian.org/donations.en.html) - [GNU Emacs](https://www.gnu.org/software/emacs/) can indirect be supported via donating to the [Free Software Foundation](https://my.fsf.org/donate/). - [Org-mode](https://liberapay.com/org-mode) - [Org-roam](https://www.orgroam.com/) currently accept donations only via [Microsoft GitHub Sponsors](https://github.com/sponsors/jethrokuan). - [Regex101.com](https://regex101.com) (see _Donate_ button in the top right) - Any other project or institution relevant for Free and Open-Source Software (FOSS). # Contributing Please see [CONTRIBUTING](CONTRIBUTING.md) for details about how to contribute to the project. The following communities have supported this project with contributions to discussions, tips, other valuable contributions and also inspirations. - The communities around [Emacs](https://www.gnu.org/software/emacs/), [Org-roam](https://www.orgroam.com/), [Org-mode](https://orgmode.org/) and [ox-hugo](https://ox-hugo.scripter.co/). - [Regex101](https://regex101.com) - [StackOverflow](https://stackoverflow.com) - [debianforum.de](https://debianforum.de) # Example On the left side of this screenshot, an _Emac's_ window displaying an _Org_ buffer can be seen, while on the right side, the same content is prestend in _HTML_ format. ![Example Screenshot](misc/screenshot01.png) Here you can observe the identical content presented in both raw _Org_ and _HTML_ formats.
Org HTML
```Markdown :PROPERTIES: :ID: 13ec1e89-abc9-4d43-a4a4-3005a1c9dfc7 :END: #+title: Foo #+date: [2022-03-09 Mi 09:30] * Introduction This node is named "foo" and should link to "bar". This is the link: [[id:e4bd446b-216b-4e34-8d10-428b6fa5e257][bar]] * H1 Heading ** H2 Heading Governments of the Industrial World, you weary giants of flesh and steel, I come from Cyberspace, the new home of Mind. On behalf of the future, I ask you of the past to leave us alone. - You are not welcome among us. - You have no sovereignty where we gather. We have no elected government, nor are we likely to have one, so I address you with no greater authority than that with which liberty itself always speaks. 1. I declare the global social space we are building to be naturally independent 2. of the tyrannies you seek to impose on us. Source: [[https://www.eff.org/de/cyberspace-independence]] ** A https link [[https://codeberg.org/buhtz/hyperorg][The "hyperorg" repository]] * About backlinks See the autogenerated backlinks in the next section. The node "bar" does link to this node "foo". ``` ```html Foo

Foo

date: [2022-03-09 Mi 09:30]

Introduction

This node is named "foo" and should link to "bar". This is the link: bar

H1 Heading

H2 Heading

Governments of the Industrial World, you weary giants of flesh and steel, I come from Cyberspace, the new home of Mind. On behalf of the future, I ask you of the past to leave us alone.

  • You are not welcome among us.
  • You have no sovereignty where we gather.

We have no elected government, nor are we likely to have one, so I address you with no greater authority than that with which liberty itself always speaks.

  1. I declare the global social space we are building to be naturally independent
  2. of the tyrannies you seek to impose on us.

Source: https://www.eff.org/de/cyberspace-independence

A https link

The "hyperorg" repository

About backlinks

See the autogenerated backlinks in the next section. The node "bar" does link to this node "foo".


Backlinks

Generated with hyperorg 0.1.0 on Di 19 Mär 2024 09:03:51
```
hyperorg/misc/000077500000000000000000000000001457624743500136715ustar00rootroot00000000000000hyperorg/misc/screenshot01.png000066400000000000000000004156251457624743500167320ustar00rootroot00000000000000PNG  IHDRJ gAMA a pHYsttfxIDATx^tU?O X?%!RBW;{0t~*zF"YK@]yZ2ttn b5M Tj"<T3>}}ssNNNNZk콯}흰{]w41 3"T #k~57?mo'7wyk/(fh֯ovikg̔*U^^db;ۜ&dL}n޼_'k;:>l-6!'onez>1S4r%"}ںMP_YOH`LdHEqxFsAgRJgiP5Ec$E{yi(J9Q3T9 1cF)d4r.w ;>@ZnCu5[fxu;/z_Tv^ޜU^ӢQΨkbO ((-U3vY"޾thgv5iieg97\vC]NZ~;N;yZkyuV-OfY;=W5ѷλFq%ߙ-UI QiyGe=Xkl :)]>u:MW8[\]sLlF5[<\˽!Tb˨o˙aZ ӷ<}za=j=ݻFe=z~tYb).5y(1r=2|.{Co쒐Ynl~//:K[;HxMkjpoKЭHԯu4'r\=kV=3bE{\ӪfSc35۴uб碌2M Pa|ۼs*u*y4wUtʮ.ű%&2JAOv:۶uM[N٧͢2`UfH[KR`؜ݿ^nU7JwڴtAZ^(NV=ZbzՍjUZ?˻3:HW% c]/,Ձej|QwzIӾy9*-Vw*׃׺Uf##mmfsۇjNgijV>N뢆i*DQiQM hUuJ!e2 e Ɨ_Z5M79Ms/ںƽ>Jc ZtY^FKE[V/RYmtMɠTmG:0:s<6w$/ro[m}qʾfnCUT^'O;סU&ӴGȉyUY'^ts̫CvlK>zt uGiiT*oL,O\N9.FWk}z:^+fQWN>_2 7P`'֧UtvrU&|أSS JAB5:Urf=٬Zu[|!jAK}% O=łtaQ3??!/s.m3^uoF5g>;V*[6O!}E}jգӧ FPzyUJ]2;庩FMɂհAZjU̮ʭ/j.5;~tM= ÃjZҠyBި\&=L-jTr5^?z/WTe(Uރ5te~+KոbE U:~=jN=\uZZ<7ڶW`^ Ж;R=U}cz珽 sVزuy:Rj[6k5.Ӣuziuj󖕚nhm;5*Gu ]cMQ/TVT>۩S&SFg,W{׳Q 8eѪ PnW>5mhϯШg7~nk|]viNw-#R~~eG JvUCzyNs"Ψ:;帵Q+T`ZwnT=wr}gOϟi 4ٗWK|nU9sEo.Wc pfg<^~Cת*ߪ[*YFm+w4Jw RgB 2B3BsX\Sz'@P\.Spi|=LB]/,)T1>]/5?9D0/vk[}izz|qhos|i/UM7KUzig͵ui%7i˥Zm6^vݧ/ƽ=y*Ԧ7ޱ34JӋ_Ժ$y^\uoD;ZjlTT;*\Mj=jg$C$ q} Ai"r_'hϬz)5DM\oܶef$g8S&֗D$舊5}TeJU˷y9u]eOjC* --nVr<^TL;=\ $.iK^GQR0,eZwSFv^>ܷX3%n3+hm:m gJS-doغW=1W:k eأ-v{E\ΨZyrP/>\>wTqUl_:o}MTΪ뱥2k:#Ka/sw^ F^SfU4pOnh LSŻ#[AjTSq%1jWY9#p7xriXL0([cueywN윦ج7~j_OVH/_:p:SȁȤ2AKy9api ͟??<̛W)N/1Z^rO%͊4V3͋:>oJ҇#ϙifٚU:FcV0]U'k3<|4_r~ [~䭷-΋x]_Mn"[Pӑ+ m[=qʓeTͼ(3C1MumcvcL.oqb_/MctfD5+I8}9>5ܻNgN;[PvVkP]Ny׮s5d$2YJ/RIQY э]o(Kb9G-╪Lif6hT}L4l[$+pS~=n]u23AʏxL~T]TULʼfğ鴻7kٍ)l_|[UPWo֋퍪 ο=jtUwV?Ur4Nm4K~.'D]ruO:"gЗ*'4 ?]-6+N> solKwiyru;UwspKc˗yez =CIط嬯.m=X{s~s/歗yERysa|=T)>㫤f4Qyvm~œ5Y] °cJuJ]ƁND7}۴g:[6q> hw>icgTr)\XOpu[iuZZbÌ.Qoz9j߭$͘3- B?L)n8p=&oGggչ=tբp<B'dJCUߜ[$wg4ybL_>{QOTkU}}E:pgIӤUX1_H6@uWJոA ٢5}eZ^[\dΨ5꛹\uQMpY~~ܦbP *[`i,("@8sDQ\1``3cv~t.S[GEZvSC:O4jϳn_JsVիEOt4KQOD[╷,/o`v5C/aG9H̟>ujFe^u.0I˛ʩ TuX6vejQ~^JT2zOoɹ#3fjOЫ"J*AkZGȟU$+Ҝ[O8IG邍z{s\9tP^wD+)6W@e#&⚰ ((t{?nOң7K:EyTuGu,*Oz˶_ BM8\CGLg%δk4U\ٝ'CΫZu#m;˓s)6W^3 Re{!?Ęfgv4R_ EUiݳ{qv4]\njTw?ίجHS^zՔqQ'FRT;oҞktSrm~Flc`gСÔ=J?O;u|;!72vrr%cF^gvUڶC^iSSR²`EzWxIfK5k(5 w}ՇZH_U4ujʟ&5ejfʟn/Ipl$*sl%zIZ}m?ZoPE0ͼ+ >Xf`p/JTC%ҚIOz^ok}2lAᜫIFaUe9L6uG9צNiaj1BlV::ϥNyW:hgyy.%~[+&6" wz}Rŗ(_s -fؔcqJ?ڧFnggWhZLev֟"?J^=Wgd[wD}xώ$P&5@hGfdd*kRKG-}7ub3`K7wN$9R%L{JH5\RHZ| Uo\W+M6:MC߂^/_}S["/2Gp+]G 2N ߐ\uEkߤmej\U7@NUZRknü4T5S˄FWP™^ꧦγI凜|/G9}T)3|}~`_XF^F0I/U? ͺb w_J4_J.#Iu0V-ԮΗ ?ЫږβQoAiɴdX9oQ0;ej+U\%ٰ^F{_v~MN}?6Y>Lϗ!be걑ET2ͦ5is@wLKZF5Uhl۪VWUhV5loo[ܾ}.lTSQ|.ŻJڱX^p~L|!bY'K \yOSw.Mרe)&-vʻXSwr|g??=[o[3x/{0< ڼ.evyIl"`&eb-)V5޳<;R5n#-Q6c#2H oz㢣:g~~$)~B8P3Egx!75ᗩT33F/6kıBoWiщ۵(?R?MS!hk|E o~xˏo^dlo*VH]j*dTAzktS2m>[ڵrf~.جUzTܤ[W=1ꍝ+U8SMV-#6I5'4ťgj< F^zh:Տ^Lw?[kH57k-^l[uSE~7]|DdeXe,1e]^8+SAu>u~A]7GmSv@}/Q2d%^YfSM(SUSnB6GW{z\F{&,]U{%;jJui oh}Mfܨx.%* U: "\GζL[dgdyrl}nֲ*SMl^<]ޮ\i3Haelq XpEQϛfCJ˪TNѪGw=޳^8ר|1DEEE֘&TΟtP PXߧiLRtܧ*/WfN7*b%8l-M*џ줫gWiWOLPkL,;jJ$ݺ(@L)3/R]gOpju?¨g~qX(Ui7/)):t2/!{ϔ>-iԃSׯvT?>3-Sz$]-rN}?zn5S٥N jCv2jcSGԣn=ԡ t)L ~SցSUy, WS`rw?L/7taMJSσ{b`_Jv)'-l2.$g hloCLm`0Ie^}՗ND7ǫo 8#L!nPɔTm^:C@"u>00[]tjj2 =W=S&S|eR WՇ ?SgudBB[n~nƎ&Y6Lr!9}&>?v󒗏u>wc3Q#yolҏ"20X?adz(J7\qo:mM%\O_;]/N:gĸj~Z>O73cЋzyy[|\.{ߩ ck|s}uǼ_S]_npt_?)Bhvn W}[eɱMڭn֖`~rgާ-8LuPŞxGu*|>=ZymŜP7Hq)X^|A@m6,'a=<.:7pct?һ2i=|H]ց_VT{[Ϯ{V6a9$\VuyNSdSփy3oUW_zt'7Mfǧ^ӿvU= ;c.]3gdf ~Wşѕ%, KS]{45PSP&~D=L7wk{F>qO7(q}j_Ӝ7Mœ4)5ibJ\Y&gX` 6ӳ[>J&y=2ϰJ=|'ǦLپGwz*lɩe=g|>L9s.7/ HQn?Ҕ5$cx[wl?4L*OSox;L*+F_e>}pq* N{os[B(O6h734ǝ#곺;]m ?8~ >+Ud_e= |. 'rowsϹtcpɀ9  ;ʝ4v~N9dL]pʽ#߻M|LՑ~Iud-zk?9Ή=DƝe&Ue3>_v&oĞ[K -g{k ֳf'nYx&Ge,7دw?ۤ-?>1f|>Y[,>u&ݶX.ЂKUoʾrF)^~{>ls ce4_xЏն;I* J*gq9Wm?46]98϶B0IdJds>t_Nt)  e̜Ŀonߟ TKܬb T֙>uogzϙ?~|V^(hrw+%\/ +A`)_%k j F{r*cs@l 5s9IlR&@Ֆp_nқȼԞrC9+ B/ KS77뜏;c`b ЋtǓ׃2OJ#_ R2MݥLb,uogye,GK̹;2A*'x77>48UW&ΦK_I%{W?=1v/^ԳϚ*Gg&Jx͞0}AEc$ es}YεI'3)|>փLٞOnShx`6CbͬClLw/8W O,O*#Uҭ wX0Sp/ݺqbW_Ihԧt'c L]o tJ{7Ӗ>_NkUSwL: '|?&ʩ~xGM^MwS`T-T}>M_*Ft?8)KҟL'e`^G7ڻGV|U>ROOZ-$1Q=|'2H$YY֫nsl}^R3/3gw32-X< wUk4uӯ!È>0X#64cU:(|3JtM:NuҏOmFEfzבۿݾT+♺a7C2ȖiF~mt]_QCY Y i=|򵟜KXֿ׵Ŧk8eMvΝ~ So'elA* pKo_SAfpcð uo 5k;<吷#>u=qO&PuWg~|HÔ+%MEګ<ul}he9_3?\]Ax\y868bnsd zӎ'K ~ܻڶԤ " ~eM9׽gW:p9lfRE:Fy-"/jSɚfp2,2F6u4?u=H&_ɹ$eD1&Y6 WY=Q _+7TJ1:I7/P}"!_=0rNmƋ6kz-:֝k[$s_汑raR{?үRra8k` %wo( g8z4r./sq} q=06yNL\ay pUt}q-$0LA瓯ʀeO: t?@3 QlI CYɄz.Ɣ[2lA2'_FϪ)\b&52fcKM.:7DSt=O%Ct|ۯ 3Τ f?)NT^=)e!L=g7&!ϦzvovzBl|v6T2=6MZ?jXUz`ޟ<8:ITMՖOuSKg [<\W&AO~r'gs?4m|6N&Qf8ߧ$&e{;_\N·y`ЙTL>/Q6;yջVv#ˈ #A*A*A*P8VQ0Lo?*  87TۦZ|s. 0CZ]=gzi9ZtUYpYMsZ.TaI%%UjR]#hoݪ\śhXՙM4FUi6uzvmrՕAĪF ;j}uj5 Uי4P󣥪TmG^Z[թg(** !y34N'vRVO0?FWϮE7ձ:/?ovxLRO;#zNt8Κc>/Q6;ƮVδagyonZO׫aAG9=|lk bU}m=7v:ڬ['-V4ҨUY9Ԧ%uj-w/ӋTzI({C *\`dzQTkFl͚2L,/rPGieg_hTFQ*yiUe߱\-PiFi[iT(Z;0[t|*Ԇ?-M[nM0r;F'?w[>ofi啚UJ sYun]vʵAo .hPCpi6G[ռɎi}TQ;WTe+蓪ߴ7N&r@=z}3xwD]{[&j^|zx,ujucqOHh'LtH?_)/*B( 8GuDǨTS<َY{TŎYJTbG5F%1A# :Hi5vP̹c;YS "1]Լv?2씡LH0dRՌy4*IGlqH O." ٙڱ\Uo@ZFDswN S]9ҟ?YӋ3v,{5T(l{2uW=5@aRԾm"Mꅇvv:yz`~W&vjީa#ڳT.S/V ;:Y1A(]jeƎ I8#^6ոKE DRL(צF{)qgK;kBTm]f|h'~utjCue'Ј Rl^܄3Eyw:,n\oTJK7uיִߣU-곳bնT'줫TuUf\zzpVghieYF5-f'̠TY?7TMј!/_)ѿq}~$FR'8Nr\P?~t}TM |YT֨kVjsKmTvVߏt1g.ИiujkyJʈʤ2_M5v&۬tMbT'tD*=EeZ6]_"Z:/SJl֑שzBuffZш Rxm(o%ƔjJ|MJ M 0Gנtb\kt|ۯ 3l&U0أ][jkh @5 `83jv BTYmЛvgՌ53 wErbXTY)U^FЬ/(0TSfUEWjV͌rʣj]OVV~n|;2*0ffW^";R~)?#Ӿݏ_.rԾՓ$[)X.Dʬߩ9_ߧv"1l9Yl(,I7aJ#zH]VbGc|ڱXl%4GgEgdY쫪)%- ?6šw΍}OwWAٻFfX;ޢ; 5[b&Qq;z m F]vKk˜ -5[(c=ϋ;hgrorӧ:Cl\cE[y$;K}`p2R۶mNocd߶}gVt9ƙc'䆩ƎSH nRͺ"@`nhƎ׿uge Ѩ=VtFM :M :5M-`w骻$:Qfr@QArh鵟ݫͯ؟ Nw,sUJRL  }"{T ִ>>g2t>-(QU{qŔ.])U&@+pͿ~g^W2#"*unoSu"(=eҪAͪ?>)ZARҮQ4j;mȤ:ޢuyaL&t]bʮaxr|ŽWn_Qu:8[<UnQ ŰL0vy]=/|}`jwyȘ)UeU"omϟ4i}:iv|JfҔ vzy.lPf1xTk6s P~Vw<'p;C2 P}mZe˧J';b9cnjk}աVrZU nVU s͎uzb^%I)tGSRp`xWڭA7LE>+U'N lM//Z)U6pfTjTᶚ4]1Xr\Uu]\zYLٺPGJ r9~Ͷtd^Z_N/ oIevvi ~١}oa ]5.@Ύghsu,c:vEGNc6-}^U_mDwv1uzb_FSuQҳI_|3?쩔2|8B2t-**t_|U3 {L0)Vj5sߎ`2l34"*69Pp3Vl~& sut@Yb EEED@FJg׌jn~Ɍ);H͆:y["wm\ m\1քۺh=N︽EY{涪@?JŪݺGeur6Y0iZ2Sv6^nDuW}}J79\ޖ*ᕛon} EEEA;Y5clsF0(w 92wdR+unRvFn5>xv/juک]^9{T>% 痮L3fL*TM 0Gܧ}sȣ+ ݁$@:*(fhlsv~qJgiތCk)]ytRVO1zv./Nۦ]=#;InD`=2v]q(v6')c]_]}<gtl6|LtӱSjמz$Ed#rZ\^딛a[`Z]";/sQ3az/:Phf ds>!<DΩL* 2 ͟?hL ` / imv:Zhr˛Ԛ$+f0dlds>.ե_Zg}K֕9 ?b87)}DsrV7}ΎG@̅;tך$@r Ly4䦎#H]1d86j׺Aϵ蘙7AN~aͩ6m d4F6dqk\F 7ʪޭѱW"vF>̼z50wu7L*r˦ds>P%He՛Re*j`L̪dͯύKEe)*U͚1}تfDfTFJ*5٭F_sf_{Dy}e?bna6o"۔ ~&ٜOaw=۝vJ+f{5?&xr ݘlçEgSe/X֝Q(L_PUk(\7 *ur.[dE*rsTd\3*gk/P:yQcgxbjɏm*g_8x}gB-߯/~vgmїycw+dM69|>q ܟQzߖWrϥvӗMʢݽ˝3GCM\gze(dp m۴uVm֡}o r:餻I.w0`WNplf?f^X͸>~5y<< z1iovYoįCLhTIwNX& fL |rᗽcKy mB$^6U/z wr vW ҕz8H\ g-#,clT?&ʽQnםs0IoR )78BI'ԓL3{ 5ڝїK~lg{Eh;F&PFsMJ6Ig9_Zuء ^K>ͿMlP意7n 8NZA0nH^rK,ysEA77ܟqB>0/7FGs X6ױeOj"M9vK'67 OVۗ42M t߫n6 4iG3n.P6a*9]nXw 7g}MP`LfUUEgh9ü˽[gnIiS-Z5QmLScl^g4t?Fx:?~# |Bv1͉ɈױM} DqoxTϵh} ;!02R b.W<ɾy3L8po/Oͮt(WtuU]O?2$UnQ֝!0 3{VE߱S7ձ-:Kk[Ǜ^T9:6 (P5Ec G:1lJ?x}a.]ۼJr_ӯΉ~&]=cm~a58L= /+Uuq*t߶mts8:@M M:;ƎSi/zCL0!c+tw_v_ks_K/mn$dI:KLcdM= l]6ILfS58ҍrKRwr^G`t& /WKzS;ξsl {yc'_2뎸19slW'# kݗ^Y^7^ {W8c)nS|h'cF瓈ivmΗ tg}nW: +N(@QQQVo3R]o2T;f-<&v N12g>Db $:כ;I*+ sɼo7bisצ R٭s3l)ֱyzdl-9?S-Z2=6?ݫn#7>&g3?:|M)6?ۺ312y.Κcd3`voоIԾ_c'<;;~B 3R;;d}BɌ _ڐ6M8'651;8-ey!ӱl3c#9jܠD49ƜF|[4c o\wε{+L*>2&\! UTȍgG5eg `@`Ȥ@@@@°WMhXM :HH0Ryk-!`e~ ;ޑVn`e\ V0/ ]IdEuy{7^n3o&FsOe78ԄresÃ*_{ȷgXC]}CfÌ-tNmn[eFSkKS:M//oeӊȴU@EEEA;?S&iO3MӤԟg4?myKߙmo67*rRU*qŸL ډ82^nwv284FordZn@fJg׌jn~2sNfRSw"}gm.Ӛ4vwxK_v.)QJ~ے5ؠ9۽s'W"Իsix_aV̶\y pMe DFB(߫А8v3z"9uQ;4I{~ vR*]d,]h^]6vSg2&̣ލӴ_J$rKX*ٔ[X}[ja~/bFn TS|{P%gԹvڎikJ_yAC-;]M /*6cowo$b7'Oq]IC?k/?_ gkwwNAȺRg=ߙ#j&MNwD"}/WtQR ί U,VLe]"72 McqݟݟC%)3/rBut3-Xv RQcGc7?/jO2+708 Z ak)@R]t?U~(N?mZTӝ5*ͻF7o5UvXUn?guTS*1ӓʵ Tڥ)nso^5ۏD;鱥wnǜ.O`>#6MĮWrnm:9r. VTOք>3P\+*k<@*o}TBYBE%W?J p8вT%7i6u٦zikm'i9-G8ڥ?F˜}'& 5ѣf5T_ ߥ5_(MפnWݤi77ѭlU?~j԰SGBwDTW6M7CWN?YOy֘cӑ2UAUR=Odf|I75v{Kﺳ\gߌL&9ab/\c&uemٖX@S|NdM-O9Uj_QkTw[}/M_YQ3jZ &+a'ro"Uߗ>uWGn>ٗ_kOz]5sHzPD/I;_Y4T~q#p7uCL]K=u}m ]}ֻNֵ ;m }t&}` p)VՃ/?z{k%A \,jjoҵz;/\2;+Jq6~[yջ_U zm>h:R jV5m]ʾY~k =j1}^+go2ϊUYfU%r6mӺ`cD)S}/o=UFҎsRALLS7M#,~ϝg[j x*~?~fUŸ] +u.^wzD-w#Twp͎ 6n`+_?QRA6PxYkZƲ)79da6}MFt2WKm=˵PNƧWTGGkڂǵ{`J?;ةv;ZP}Nb=ܤEv2guE}_֫l2ml;V45&=`Fԙp-2-@Ao\#'x`Ou,X_87L4O]WVUY$'+:*U1ю̷:t@==lFT |\xuVDb蹏k?ӢT웫L*lF]1vEEE)5_3i;'s}[jTRM۬ޭ,>/Q6wB-1MХ^@@i~_C[TsI^5I~\>QckUm@M<[voQwM Uq;A;[j&_2َ9׺zc7~7K]SSDTmohzrGR|YQ>~9%ɽn֩帝7ZH½ImRݴ1 1^ۤ.H3שqapLSLϦŚIg;g .I ?Kpi˼c7ř4>OE,{p*FۉRN-l@ ](T2yHwY5s6?[ $P|c\%4Je6kWAqX+}Ah`E.F5{Z$0U<{@20X_`Bmx=Pi&-7 \or]IZ*MT٠ v}ζCa3R 󠐟&MeYr)״PL!LCT>`U}54M9:~NDՉwhF%%vFgo~k  voKsWhGl23 _huMZ17y1-f~>879Y εgD܈4O՜/HEifYwk Z:^U?PGwN90Rj];$&W;VܩM9W} |>9{&oלkpKmFmoӦD1[^ >ËDm>VSϽw㺮\3ǹ/vnw_6'6Ώ  ϨȵN$g_6B6ds>ʴQ#_ e7g|&4/fX;'edZOUF6P&2o'LƤ3޻cle#_ϐld Iu/>HR^Y=C2Yz8}h{Z1L /`\"E%ά9_N !۾: kݗQʡH~>yZm"6`b%ߦ^kXν듐^`TdzQkƕI6'z0 P_&۷{/kX/fX%;Fu4_ 4gQ״9#goPE3$ YY_UǭN9 '9wTմbE +lF%/ yiuwGViKr-QHu>^}5$ep_kRLK+nfeP3B5%i6zm~:|}`GX"OUGmϝk+5v^LA!^ɗꎑaG=}3ЯtυeX 9̠FW̽iC>a@1g;eૣ%s/Ϲ::#/A}?n| 8^z?QP7[sx>ROr͝ŵ5ѝ2/3}Hd׫ XgZr6~֬$n/ε듌[n1pؔ+jQ'z0F!fTWih7ݕͽ6!O Xw h` 񃹦C-\&ՏUD7@6]\\3d0z =k :wadQ U6BO!e}w=}gbC܀㪇$@<q!σ5]Z3W*ҫY]64ha头<&hs`|щޛ8xlr{}/3zPx5yyO [ 8a&+>(#y/YKdɒ 4tʒy1;M on߮g64r;R7$y p 龉 mSb:hx~v0)T fB6'zP2g@R yڱDݾhU5_=a|)ڻm(RkJup^^('V!՝\}ZтT"H;sF6T9b:d_/;{S }$N% v6hX3 ڡs;ퟻb>>ԃ•ͽ/;Cs5=ɚad$ѡ(캓:ED!"H5~>,LVq#];+^ Pvz%5ץR6>F'v;8tYL5M${!'_ ZӬFҸlss~0)sS̉9_O^f1eOes/duIp׃\ߧY]wNx<2șszKҜ*|rJ:uܧ|&V6) ;j?<|u66m2> __XX%픧`I!$1gH~ݡb2 wVY1_O9@fȤʩ^׎Z`t4~U+;cgۿCMI^Z3w;yaeq7 w;mv8 Wa\Pp9$elO6es/dM> K=4z$6V s˥iZfSuoҝ+ώ 6sNٴ"q?tL*P8Ȥ`I#H#H#H#H#H#H y/Nka'W!>?k~57?m)L3z&n%MZ!nV 0I 7xע2`&VBu7 tNP6jz_5˾i00 ;կꫡ2a P'P mXIo voKsWhGlH0 l@;w3] 4wؠ U9)$IZ|mο9W ز>ަrDf%6A,!jsIv0)ڦk?o CߺvpeكpY2>17<u (fz}b*)PI~mRA,zg+w --.ŷvJf[f}U9M>݃:&P|SeO*;f?gߓnIs7Hջ_M+V6V9_`mdwGViK;.ϷB> O 0ID1;bw>ff^5_9kw\|&:j|u&u}jƅй1SW AO^74}gzOk⽥El[ #¹::#/AҐƍj".&8wO=DAB=6a) sƎh,],f5+v5 K6ՠ:q<ڤ,7@vNdPH3;!}zJFcҸ>E]ϽM_dzqs ~_oYn75Q R% /Q2N9PrfMㅦ-$gk݀ u#\.Ug{Tşџs2{K}R%vPί U.ݥWhݮ rR염*cر؉6(gڱwG6dS|Tb4rO- 8OM5묺w3F]ކpqF-t6 Մ@"rR]>2lq4>p>{~S7d[sU&uiMg xoD{ 2)P]~q4^I6x%: v{#:grU.Bs{? TY ;Wi~7ͩ_۷ 4|B>lLQdZ=3Q+Z5 xMai0W}Sk~[Evbn : {̭n jLo\鬿Revs_NuiMYC..(1/hUpkB{ר|?jYd `PǦFo+TrnWC ؆B6m~ MQ^ʹOɐ;z#pكv_jMWxt4TCEJ6X+uӠ~RHClP& B>ts IU K;u?YB;49+P@۰0u+=˖::oH߸0<}1O*5f&^rf"ЬRhTpyQܨϨQ {a>>7zi0X5/Do9}yOf|^y)~FYm%Dk^+JNhΨ TOLvkF-ɱPf1)b|O! CK]^7s9Uj)D7ݖnF ]DMʹ_]?4{Du]*5}薣S-:f$T&v2F-Kd0ۛmKT]U]e祐3V6J\C[ufvH":~Zk9oyfA!Wl$%st׫^5׍rm^ssm/ڦ`$K ֨X.UԞΜ$/ K4+\՞^G(k \PRA}[ZMj_ |noKN UZ7A z?a~nk +=R@viejtDI]U[w) Ø2e^9\nsT@i([v-ؼ.Q"Ǚr_'fh-W=vscϤ,νeͫy%jRmN|S:56e:}'A kP}Yއ5sjWs0/o[5wIei*\yTʏ.ciS={b lfxF1)3zlpۇN=1pP%@ Poroس:<ߋ:?Ʈ}s@RؤI %;B1Tf&OxT`&i}j S.{Tv:s7R|c> f-uoҝ޷ܟE^.Kr1Wgp/^p3=jsB~W&>6թͼDp.D}٧|kRm'v^Q&sEV9[u^\齴}{Q_&eZ r VՆ^fEm:뤭K_2U8{"/-M\`o{۬m1̭o)[7brڶwzǖӲIF21/@;4 |g*5|V9 #ǔ/閷rK_ǹ-.5:w]ߍ|9?㣜%u~|]GӨ2+O1F1ipSh۷՜o5#M=*^֮k~ ~c[=rlNrO4im?gِLl:mr{ӒHT+ w5WmU?˖8M?;:[̖Gu>]oPWg^.iYF6וTSΏ~^{X+& `Tϭ9Г9iHKpW$f_XW9?%}q糧QVi^;ef6LL>9 pcx/KtI?"ԄqF^Ԝr9u*){$fH\N"${#;0&SƩ[Zҹ3wMg٠na&J=LfF>j|!~ &0t3b'cG'wYw1 VWY?~ `xQsWiVܹF}hdܹPUZs 55jL v4*܇גM}^B\?R}/g߹H{~`_u}˨cɾD}%J? r ?.o0/* fgy/fr($`|qL>vtyaY9}^`7tC`a-r*_2T@a1A*Ӥܦ%KƎ/Mf"Ή0%ƨ MxC6&h-YҨM9  v;]}K6uۥlO:a6u)AmMMz7閯o\VŠ靘 *`gQۄۚwl0sEjG{i~_(Ʌe2-U̔h^I # 7kf%4FLI6,#Ÿ1:i7g1 ҉6'\ԍEHP0T[f hއfsqN1 dۧ4gC6M fdP9u3e^߾Y6e6m\i `:e㝛JgtۼWO2[ߜ$YgIeQ72(z*ν1ʴ r `}!K?z5ta!0'Q47nPkdyP^ǘQο^2g (0}9ҷeq;7N21(w?wUuF_/;<|NL696kQ:7$gE͑cOdfԞզ9- _ٍxYFIքZ:&e\Y']Kf$.@){"/ßL.FZea7:״oK}&+ӹ7^񂦸2}Tc)2day'\,Øir{SsFezϟ Q4mЩa'Ϊ6w{߳\y@ ;fTkzT#%wMr0V=j\UM\uݨP&ii -9n|}npCt~e|}Gcd >s-g4הY\K-lx}(RfvY'-\:\{ʒ6Hj:Dxʝ)qTFz]_J.D:ƐTPpМ Lsgbf1p0YaGۣ#لa6GTLVT}w?d|SLpwmǜr;[/KF9ϐlTy@Yw-dt|ۯ 3dXiƅv P H@͞ݯSaßocv T c0XI#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H#H(7U ˾iN^1epI^&rul!R?Ԅ@a8d2L Ԙ4 |Bb9*uNT}/ϝ]0}SW/;6;[}Sc4ᇾul&\Lvush;;/}~vι U03~R2:c7*1&vEnOmq3{4~-Gm OkR_ܓ`?Q?<>#tlI - NZ>6w/x+_4 FDd{q>O&EeD=`:[iJ/g _Qw2~%l*wuLsO5c1 L۞0-P/ S#em$6z٫?}wlşџ3-qpc^H(^s㥻SqlFߖgQNuNIUFϬ;#@yS#uQ;2dAK>|?k~`k^MZz".oDz7F 7 i{۱1zDPr戚u"B0IG,@ۇT˾?z}b\`kr4 -cgb Z ak0dRCLR7qn)gB}bM}\~3*ѵDZN7T7Ns3:}W\]QX7uCJmp/GI|اǖ֫JmJUKKT}REcfթ;kT>ɛwYYs3:[# RQ(H5^X/:Vy{tӂmy (l՝f?'03 Lhߤ?'5ttfM*RQK6F5iyzKgܰ.Bn&{:ܮInnI#I9вT%7i6umsc&;Ng}w9*EMAjU TO>n΍QEjl٣#f.Yu=Tũۤ[A;iz-L[z4~2_תuj;n'\g~oLS}/5Nd&uX}O5j!;RsZUEO p#HG&M[heYQ>}쨫U߂f?ۢ3^R6yviϊiem$ \pI4iwoe*K3:YOeZޫlOnTm(wZ_ E&TWegJTFzAj }{3\4U_T6h^j_@*|Zys U^w#My!]:H)fpx}oFeY?:1U/]'v1"/Q6w"jO@ '4 C_PaT{Ln9H7 $C80ԣZPkW[HV/ pѧn;҇vR _T`).c)oQN;jm>ƅ*l.KbLKOujOlTw{σh6oG y-{a`+Qiv\;v`j(Oxo&ɪR;nQ6GUʎo٬&Ԛoիٛ̋iw<ιcڽIT95uD[Ur[f]0sG:ռӆ~ץw.qyNKOX[gbMdh_7Gz,[;|cνXž @NVCmj1E4f"nY LַKDTxV,I#u^;fRٽZ$*Uv"Fbs3dbU<^ _G ׊ʴhkԢ2OWVhVoM?ОWՕ6TjGy)PTTi)5_3i;HA6]yS]ALudRaXs}>.#՘f Prg>><_u6@%U4I+ U3+mB p^J,.;G.i[V/ѕ98p [ny<9|@|X} 5*Oh-4JeQM͈Ȥ2ު%Q=ʜb~A_|c\kЦi$ ]kfhFhxz]FQZ*Ij}uS|U.lPKušz~y6#{L]sj冧6j׺ϵ蘙7ಛZڸb*nHbU&X|^o٬Z8e7ӗDK/aҩjyna81>2<ɕTjfd>;k;a*]ͦ e"lzSk5'E00~~YvinD|Oz5=&,K۔@HK 0v]Z/c*Fh?{ y7>bQ<$mkz5̿͛]*ڥ^;s\p?T ٱO_N,A_\Lx}9&6KsoЙTD&]jtoݚ36:pGC7Zv _`(&(3DZi efc 5Oֱ"XhM ^ (lGW]נM:`fȟAڳ[mKתa 2uhOnspGH%;~'F kSBl`8t* -$&?H~nW&'ܔSkq>փ䜲o L6 ɦfZw>Ca)R<ҕU%|&o!!փJ?Cr>6d7:5ld—ja﹡z>&$\172MYeR|^ȴO&BH+'31'r 1s>9dNM^#-[wHgb{.i!?Gr<Ȥseb%=6T?k+`VC6P9&:O^1_1t\;+jKy}Us\P܉ُi%ר~X6wl%sc0>VDw:'؏=s8B" |2= p~gmѷ/oX֝|E>ya}r!_u4"gH1 EYwN8gÿ7wo}Y磎K>G?,7g~;쟉?C_2u.ynmn c\Ӭ- ol^KdWF߰CT{./:dp3z衡2P;\Y~J&ΒC3ʋ|[g͊ދkKB鄎$ i[{Ě4lϭ7D}C0pOKs6-|2uxE1瓬 ^Io{YcsleXG}M3x䲿zo߁ʅjh˄q^Ed{!,uVGdpe=8Mj6,|&w m}#ikrGV2d?' ߉eG0Bb:!1",3&8pb0::Z`9x˰J ϩ~լꚙFWN_tY:_C[+vCR~ē~4\/;R}IU.wXKAxkXrr[&7`S&bLr)m_- D{{woˎqkvp?NI^C%u%s(̏'ô9ر\R!ӗzAnJچяs;.J4W{QyL۹U^]GRcXgA|6qn!J4=sim]O\V\z朗;,@!H JN}ݐ oW1k* } G8"-..~j:LDx4K5ZtugP0*{ѯE=iu+b^uzrtw=Jm8җO2"H1|SsyAm!9mGoY|iOՃ^JG(G/걜=.#)O#ι2p\L#R†Qun53Y<(.xͺ^,~Evuz\1V_?ū*Zy:?Oqn';&y-&i&_EM[6m61'u$^ScR&WN}arD2ͿWw5Ȯ| ǟstU|z?1mnPʹ-o}K2_yS8nT$}$Z`O͏;tggj[_hDb۽TͽZ|c!_~]ͫ;^LPܼvS _&N(;3Ei:O:~U9b펂!"}UNzEIۗTS׏:3.ifTuK_s`H*\!ӏi˜PV-dǔJ3 ^8hz# k7:NYt6dUGo}X/*h1Uex=O~n;\x]Ϲ0N\oҬ;eoɞj-My$)^䱟;{Hn/38z]ϒ,:^t۔>ܢ??&s糃XbL5;Mq{*k Ϟ;btȳS  T/$SՊ=$>9 +5y+} ՙo*]WYd_5gLy<^yLi:&h"J4E{;9"ߢ Zvi~<6ƐrCVShiQ8ޓD:2Mù"_ V2ғ#]yʑqK4u4gT_\F*3<`4;43ۓiļϟ$Ӌ~'|b{HFn?K wKF@΄ågA MZ<`}mmDԝT)c]פ_ k׮]HH1L{N}M{OB#[ؠ5Sŵ18WN#^/ޮI3E7njp.$MWXٶiqX3-DI&mk=~&:9[0q-m$it-S&Wjfb:\gM p/#Z2s/4@ѾAМb:-ɕ)'h%Tz?7Qʹ˃,rL7>ZK'M|S"!H:Tf_lmqNlL\'j?WQ s~M:QFЈ15bkdTrש3C;1@Z?k<ĔF/|4WJ%~[%ԤVz]3R5i~imM:[NW2a;z~@!H9m!&Ea Ygj?h ])pVs,_7^U=t5,#pY#]FRQ*MQo) *5t^5n @H*46U8՘WRi?~D%z}UN5N_<U?KƩxT❎x']Ng}+xvd*L*zLcz .R]i!&hӦEM A*ՠ#L*wݢƲ}/w$ϒ#UFW"Ĩ̝Q.3{i) əЙe_]@OyG鲹,Nsu9཭Єz@0ݟGil? T(aS=S|7lg-RՋۭ=j~9_+UuEs(Zg?Tv.}k7]6rJiLݗՁ]Yhi^O4 ;a珖GכҦ?*m:^z>$ ݶeM-aϕ椲vs3miKI٘3+Ts۔d?y'wK-}ݲyVv6$ʔ7{?/t+ A_蝀F 5B Ɠ:eݘiK;{^@L4?ҩ-{tXwAOiu׺oyAi`q\J{<%:%I9ڣi?uhhmO,*-}Bo3-]m}Li 1ut~uh>(~rfЎ7xb~9|޶$ż✧HӆDm{ʴ*A*S]4[ j,;Yn"4zm{f~Ij_9IS/Su}`?ze~C7ĿFi>=JxrsJȵE_>d'A^^oAtv!c <Ajy}6&84YEUs;"6$-\6$wrld'ܧ@bowvB+Ͽ0u6.pv*@"^ԦdgяO_eGi.Ic?y~VH9Ec)d>v} N?^|{߬ Ѱ[ n]>ӎ}c77i]ex9S=_>nH]~ra&Ҷ4E [y*4$ _ OCVR&ɹ݋>!~2ʃ<ϟۃxi-$-srD!)v\G A*}z<ܫ5`ȍr5rwob?Տ:ǏS,6jɃ<-Aw9q:CdfA_ꁧ|,AiIDx y]'|+Dd6Ց۔b=ir}p|n]..[zߑx5r-z;< U8MWhmw>Sc UzS@Ʀ?ſӜy$S^Y*ïNi.?|fmȟ׼bY!+WIL)O㐨4@ H8 -Ę(m4%;O3q륟!|KYb+?LǛ̧s#:;F|+nhLUK%=OA:*rI5,;[_> uC-z5h o g@L)DLz.1gVQܖ ( G3=rO:/Y!qͺ bT  g?ا^ŚM8󫛯?~WoO=viS5$^`ʷ4&zv>*h@c1[L5ȠN>ڴx\z06@+} :m'mNGc\1< aRY~:"Ag|wZǨx~Qi@A\(r{]hb|V1$-TGiWկw?TT!ӳȟi>2WU81`(kC$ʷD:'Y!xYgOń qA-K7mm,<'ݵ$)oZNaR]gӷ+OӚ/k3:wר*3Z'Q5k6MEM}iR cqI\1%=O^ׇ~u_>SMNOEnCzJѡtp_A\(z{؂`=osUˎ̿xqnuC@PPbx n)gcϬ[ܾ2#%L!n$z~~3YԷ4mHgO0[@^RD͓DӶ׷~yfأrpgJ:m X%Wȉʼnٯɞ;yyE,xj'\aq +4i/Ư⿿"*o4ON^(V>A#![^x e`m^$RFYEvٸ_SI{GL$~%.Oٵ!e;?ƫT,@ҵk.7}Z2L ~O7`O>LC W.V|#w;@2I#k{= P]IS$|)|w*˳^ >ӢX9M3bW0 # wR@!HC rWvZG ReejP}*S*Wk4Ͷmc &_HQ+I^"$Q*Mn}(|/~N= FROiFkU +R_xVgf 2t2pc+-Ыi*S3߱?-^ڭ|=a?i$IVĩ|kX Ѹs@1gl?&~ [`|]yˍWwc6xNǍ"V5:}9*ZRÏ~v[:\zzOϟNH=_Z2ݾ;*3mxLˣ^fF:~i+rT)+Ʋ3rCM%U4vEN[O-j,ϣ%-G0=gBTn 3>麟_w^OL#j5hyٞLFOʺL?(o_ʭ^gMiTGyo*;UݪdV٣YZߏ.m=ڷu?Inqs,Ni6a"$o(yEPU고;zwI/maؗKwoO.WT)uT\}w*>y;oS KN6Ug^ Kw[/Su7iQm'~ξxѻt[3]]!٩5%z0=hG:s^ǔv?E>;eKq:=[N:u4kyBOu\p[Pdf~5d߃Й6UNUyQ^LRM}ˇ Z TGBM:v4>Piс:{NJ\uNJ}tJt(>+Vskz"T~WRͫA26៊3!MN3z6;Bſ׏S: p? ir< du4ZЙ|šϝ:)D"h'"U*父~;m&*`]M Wh!;oq '߈sɏ&Q U*7hH*v7{bӖ?K'kQG\QG]wOPlF:$jїBkў L[~SVxLLթru&XSMunȻfJj1KoC饧:'C}'>6>I܏o{2x)-tOz#cj:a<q̾N˛WN Om]b[ӆy-(ӽ #`I>wD,{읒9-V1,ml@=oȼ13yΉ{S\~n7MQ~|{~V{Sw$s+פ_4Z|{e1bTo{7m'LT<۞# ط&,d77au'oSx[֑ϋsgncѰ:&zyƶԎz+?T"T@R1?Nɝg9D9mwC0ÇmwWoz](MQmcßi9ƒ4EgΈiBS_Mh>&Fgqڔlvfvvσ=n=;y&LܴEWaVU/.Euw|m{[@ؼ^j]\_4_Ïݎʓ!y0ͳ$,6ўى.c}Ntڄ 5X ɬND#=ۄNzJ;?^*'Tכ&o_sun# lv׻`Ŭw|`Te{Y HiƫT>x8EN[1[MӺ9cS5zUwV4@֪4^NV<Yk@,7hONi6~n򨣩:bы,SI:6in]vOAv0hs %pWxSzj5YO=txnK#cm;+̞0ξ]nшs V^DKvNF&m`@Cܗ^hL)D% /z݃~,Ο򫣗LFǝ&՝hD_HƷʷ asFn̎Zw%kI%GqE]E_v;Z\_Iw} y.EKuQ p%ߨ2V.O빀5VYiQFޑ>2MxGF`՝|?lK1ݮrxmJr<퉷@Hi#K$^Gԝ^umUu4'Hu2-MB&ձ%b4:-];bNN4QǾ;r©Ȳ&;C4%Lx n.KthynA=9 ':'|3> F=9G@LÕ @ A9Q+F N Z'?+^:уL| 8'ٙVmH'kWc <ڦ.yгLꜟ;Ѿu ^l?q|<5!ݓ T:^t lʣ-h(8:_SY2oR}qѺ=|1k<~iOEzZ4-=;>Go z*m9s;+3X"ƩqlX\zҦGN}Fqh"J%k-~\Gԝԙo*]˭^gVũIdyDɫVYt9vV#)Ĉރ}onu3*JE^/;\5t>znb6"QHGi ^*;lΝ @RٯSXSUiZ3ioupH imwIBu}\mn}޾muxӔҞt9KZGnԌ{8CEMϴ~w?yѱ wۻkj|f|#X֝4eLrF@R6[eyC߾)=khO;&$Xu;+20+3%M6[w&'Ƀ<`Gq=lmaiž#9'RGhE~_<1$;=߯?FnQcIBu>=]jziJ'˼s֓ti25X>NJ@kO|X]hR8Xyz$;$)rMZwz-Sg7.d?-zM[ƕ]^u42($iI0>-Ct頖y9m/QԔgMS{?z!^ m5= '#;͎6y^yb}(U_ps JvW1%$ӢpS5e ȩڳZ"5*?gs ^࿋;CV;5u!KN,sSg~::5XƙJSjyo\|~xyLշ-gv6X[~Hv[%8ig$ۡYuzAŷx뜑f/`[AVpR)|ݪ\7ŧGO.~z<(6HW Z$rzʃ`3:W NS@f>7k`bOn<]_k@nK4$\eIa$,, r Hީ|Ӝ\ ^?_\i R@bR@0A*T; wR@!HC r \m-dV?b:;޷JG>D9߶\ݣϫw{|>gssچQĹd֏49Ne詗Ǔ`xf})d=}Ύx=aBePfSovF9Gj'nv Qݗܞ]IxӮ8iayn#97Cy|{Q{N6#|\Rw.SooY|6oyPf[~V{Pcl - JۀB . 'OMR6AUt9Qs;گ={NFrJ)v;|;ƌNޡ*o.Y3C;eKwuSǽ=[|pڝ$e_Ϡ #κR\FUqj>"( ЌpbJTD}+Uəw@g9@ ҟ- `;]NN>0F[D.+Yo1 7OAc؛^ D HvFFYڬb~|Ĉ/ $2W(ﱛQP"GQ"إAΩwuu ʊ} M+wCU>mr_/H)].v<#DeDϠ Ot'1A+UguxcIu˛Z#;5zG/i)G!RSruIfDwft x3ܣ `>wVpE avʠcܩɠOHwd7A4VYxNnpSx'{*0 mRKsw{ϧKUiNI͇1*Ȝ>2NpD|zǹ/uXj324VпϠC .w7/0+BQW'*-zMFL^;xTdog*uYFH,keXWwSwJnjz*5 `)A/W˺E}0Iu9@B\bij:#Ɵ/<}N)W#0dp`Y){CJ#ȣB۠ҜmŅ5"l^o5u^5x*A$GJ^:b STEڝNSn+|Z)3 ֘x2NAhy@߼Ws8,;negHNÕh;+S\W7mܷLhr*-,}ZO:dё^:G*&GW]GIǷuVVoS:ָkiЩ@/vLMo:*1XBjMkuYk<-kTР#,"y୛NcGXeyxkyΥ,9Jvޏ;躠OY jfZ'EmOŽ?(k?gX~SP#?Ϳ]""Da< ,!ojځ*>eJtQ&Nǣ"T\UU\,^^;^ycDZٷ]Ցo.&ώʶɎA:&cKZ3@ .XY2OYEW pfY#b;vɘixv~nXDNujws=I=?L)T-ヘƺ(C.r"/65Eδw1D<0&h+m=u7`![Ǡ|'X!t[ =e5/}|I# &9(.FH{;/[FD_E<[/()S76{z:D6۷ |vYRŨj>2 {.Ny_ÏOAAڵk7@,#w;HN `m>㣝 d*g0P|Gh5. ^!EFR!ֆxPtK,Z00>з˼KUTPxrm&5ͥ=s5oZNⵤFc\N```$A*T; wR@IiF>|`MJQ†|G4F N|-Rijn۷|LR;k?ƩxTnU\mq<FߥG4{_hs_Wil?~σ&~lr֒fևrOKׁׅy昜Z\T+Z+?,ai5v}Cٴsݾ44Zuy;oݨ39i[U)}Fe||:4zn5<=o5-gtltX99ޤEqoħƳ;Ě*/?g2_ |>ќ Ou :>v6#ޏO_}{}:OOmHs\W7%!ei{҆@J7MUyqS9&_e[WNi `sj}腱Mv}K9mfkZiYy?O>w58Ujh84Fòg~r:I]3"mY/cbtqJr̐' ܢ}4E:hs:9¶ rG:=ZpC'9m?i7f:ytDTnخPsT~ݞj;a= Ee;S 3 RƆoڴ=B<ϮZ;B|\bV}?3W^$γqH7u.0 _%F4]]]Q,G@JttG;9TIm&PG_&j&VN@ ~"sI };I;:ݛ=diL Fvdș¯L~'tžJ{mGfc],{T ե>XޕZjfH#ɴu_G)Ҧ yM{?ߜƫ0UQJC\}GFzSQ2OM1!O=ֈ\JOo< Y HUh00yq٣k΀3ңհa5*nS9 ݸOF!S)ܩ}sKΊ=z?O!>y>ݟ^ʘ0Jh/H~[XTfn_*M'ChhYuqGhJS}<ҖO^`0xTGgm2= \^yB 1Z3ʃ<Ο"-7쓬۷_Cjt M#9hVI%:Uzʿכ;tFh$pͧ#ZeV#פLwz= +$|#z8K̠  IzOV#Ta)M'FŜ #R@Ԧ_+>N+}2A2Lyӣ7L8a>^ >Kz\cRxqߺ3|biQ}~Qi1O~4ʻ0uyA^O^oR}qO#Ø ۷Z`YAz2!=#Vi}N2HS[7]ԔgީUMtbj';r AV}֨#z=\%FZvclKjʽKR]L]'*^sخPmb* Ԧ7H ɮ;ZuvǛ&݌5]8{f2JjC<ׇFC1}|:qӣm,t$b;(!†:n{kQI~XM1N߼yk^'G}3}#j׳|oh&(`V:T7֥*ݢΊ \T K=_bOܢ {֞ONyG^ {RM~ߢ_s$neF~eڴ\SkjçT#jeڙi!OtV4:Qijo.$}n[i=;eM-!Hu rE Zmj6u>=]5iyf{eS>l_w՝}51NJ"Mh2՘RR1 H^ydz^:< '| ucH2eӾׯ{{iTV@1w$$74|}EN ޔXɛ~2o]֙"dq7}mzG^o*hߦdt5@_M-RU k-|.ѿ_zz  VO>=Z~ry^a?=ރMTFR@0 r A*T;JST>[mJ=Y@*'T-ll,XkI/:rU*† զVA$u`0 HhQ񐉏x4]ot=зz UN:czlӏ|FWhޣwCt5 5{˴yݪR3TT*T6[_ ۯ z6MejQcf{b[q'qiԇUs{qM{Oh3`{S7gVǏ*4=nI3q{|$eڋqy7my?FߥGF}cXk}{Zsjv͓?iٖZGTZ4JۆpPwVz7;W/~N= FRMu_#GQN0T+Z5iv]OEdYn &Wy&(kU#ګеjI'2 O_$i˳|d@lmSVk0pOuu[#w̿ysUWwu6TZvY>yn8244%aRԺV|+mIu(};TykS++󠵨x XvǙYn54 x]~5rH:0;u{Z!( v`Os|_Ɍ+4[q㝫+1;WX ˃ʔwu{ +}&mO̴,SC&7f^@tdby#ՐCCeՏ3oy70 gx 6ߊ^=)`ʙ9j$o/nL)9ާ7Otm Ē/I:Ўyհ|zN|dFmiQY#ѵ8kvgtl| 2u:hG+}aR-9ӏnċS߬!V7:U?t$sM9ߔa(0t7i1 ]|Z4qoQӤ>F2rcc_xᒴ!;ͷڷ굇H[?uĆ §8N-6 JMUUVGQhXF|SqTkQ_B{35_@Οѻ_g) }X 8 )Әi+HDY:̴Un|Ҝ?E.kMӯz`ʍ!Yq!a^wquCQTak^rr$kZ,=RZ%0X֞&`9O$LkQ1{d%]ڊP>< EZ\\ mQz0`tp뒮+2YY D1Pɫ@wR)u{Z!_gz44vF(K46E ߈ݵi߈ǧxc*?jM 2ouSZ"e.}بTi+j/k ]QtXL0UU`՝tVzC^!Hu)O?;.Zx9aQAb,1-\\Zathc˧Lyj.|YnО XQykT TH%.,骴0e.65&YwZd#FtyUHEm|AUw@BmUC6)ґ>mE-9ޤu54<] =bmj *m-^})}2V}g^gZ6ߊ\Ӷy+ > /(dәK-_$zT@,Brg+e_5є^Vɳo.rcZ|#mN`XE&FЩveISI5hu'M^[jn`xc p*8LkhSu5?Rkh'WOUva\5gqJ>czRRih"²VɻoR}qя#n@Yq~Mǧ9Si^%ɷ"^4: C:uwj6cݵ켤6XqОk:.G\o$6.D2Luvxq^%; JNڐ*O-\Pe2M|Y,ok,~-jMt?c ky~6#w*H˜mcN`!*jY|+jK^5*.ZnȞmM5._S4yn^cgVaNgV,Ӟck l85q!Siz_=걢HS`uIp%mE, 7d|L<dwpTYMd_>~! N$1Pu'e^tm3.<RvZ=36@KU9zL(rڠPw@_ׯߩGA VZ_>=Z~i9mPl;A*=u>"^dVK%*V_Ai篰m}gȲ}{eT_:NT#CsA`xL7y?r;3mz . v$~V GzI)T=B Mz2T:#ȪP炧;z^@WJ}c~_l͙fOOWn^iD׸<2Ϳg>dL(>>}{/-ϋ<;+h% _koۍ*1_рFŪhJFqcF A*@in}*;?3 hSua[Ds;yG7wvIx:͋kD.&Ƨ)9Og&^҈Q3L&Pm6i}ysԤ3 _c7;DyﭞZQ7IhRzʟ5y䏧5"󿰲6c)4^%TEmrfٴR~6axeV6Ķ Yˏo=4ˬ7VMT^`B 1B梨kΕ4Q;mz)VT_w EϔSuoV흞k1}5'eYerپ:ϯi}~;+timFm:xV {ۉZJW={awH͎6ÎuAfyzӁw>>ZK,GXʋ==L1ɀ?2X U¦+A*8:@y'Fv7ħZ*lC=׋FL"Zy >`ܽ{W%yg'hDa;]\+ȘN0c%fK#tDP@$)Hgr;;t ϓn#BW]ʺ9SeN;g EXA*8N`gnxu\r7;pcFԴstm);!֩tFno%&U#|9@N)+9*Jx|]u3e^HQfAą  ˟#t)eZz# /^9O^cpgTpe H4~)LҶ?ݦ\*3*0 {u' 2֍U5e_@0!=#RJS}) ѳ՜\޳v_Չ-㯚9ԒϛsbRz˟:|UBGjHL :j-4xL<5nu4n;0b .-%%k5#\R4g䮛<:ЎM[ݵw?Q#_*ؤ :E]aӸIa5֥*ݢð)bi%kM|S8A78BD=.4I߶VZhzNP=owT#낕?.ră@nZ'PՍi$cSBζ40|;GhzI#?Ye]bwsI=?4V㫟]Xk׮u#p) ˻x -0 NǛNMMi x̗GL풏^|;OMmWG=h涭*{I9eDros_TUS>Aȝ-sD[?;-r6kаNx*i&/<07>gEvOL6v \?)ycܛoqЩG&Ny_扶*m,4O`x{ 쨟|VCQWuߣYTwC;|8K7HC<Y<5j,k{߷X^G _?0q_h9[zz'=hm9Fu7S!~vx z섚tFw)߸lvfTݘsI}pni~+8a7>-Iuy?žv97I'K{t=ks;4^A*8KĈ/9WDMt>;çRx@=uDx\戭4dD`3L7:᯹smI;ݟH{ԅFWhk]oׁ؜̫+܀될73P"42|~_ Ί n*H9c'zo$ğl? kܲ۷+o|G}^GnYA +:t=Muzԁ$s%M}Ⱥ)^_=Adm2d*3K26W-y2`ec8OVޏ=ܷ,q>F켕AD. >C^v$KO|GIQ cKDO9S:hGfc9w.J)}@6Ҽ>ɉr> aߙQ7*pb#*o̳G{ t%| ZNdzNR'8 X/8^v>u%h{`^&>F"6ط*m>&~_.(|?=~)~=yPtW>H5!:ujS%_4F FSC"O:}tEuys|*-Іȓ7<.d-υSs.|烢 ;˜zuqnݻ/{?;a##N'14nG؆y ^" z4_G5QKSLf Twףn9lD1=FFպN<}1:u I)ޗ883dyPtI ܠ :}?x@ '037ܢ?BO& zFVc/ͩ {oC w/<0͝:|ٷ*kk͹Nd1E1dOԪ=kAB5ʮ_|L ҟjnhy/=O鵳^P,CڵkTnu{ˡ?+Uo7t̾j)zBzUڭ&~^emmݢ xWj{C㲴{qmso mloy}*]cTO4n5EӔz?:`}tqiHg3A ;~:G@][w>xٌ޿K*aDz|G婏?_TqnQ)Za)jzEbO´iihOҶ*M=H#g6mIN4u3m^wVq+g~t ) 񄥍nZs!E>;)< /rl4?\p6${9yO/FFwvD߷? +9wQV@ QvS=ʻ6h맞VKͩz]j:o+{tmeez2i޴cYp|4hz;o>O-k]2{b9"4|:Ly^^-.xlB;B}iefw'igߠs鏲.yl-mpAIжȴYeC2AJ ќOHi<#c#">`H%O3Λ4i|ޗAg(OZw:? Iϟԃ4U Rd!~4]8ҧ!<`XvVm5BmX+ڦ^yu&6mW3L "h?ӐuziLoG |}<FDzz:i+[[$?my?NĿ@4m?|vDF΃8Nz$mo5HqL]Gs'wG9xpr?JS8PeK%s=K1T} ftTVrb?TB mHn)+N9Wzw7컋qLarDOHn>Υ}6A+?l>/$}95_PQu>ZsLUsbMCA)WUi|J gj[ \qٿNB!=R{36~@~?ĸ/a +n{.#}?XVyԢ/! R2}+G۸^Jۀԝ"?< EZ\\FxeZdٷUEAuGw:n뼍j^NQ0"=No7dizm66j05f ^M#wˬf90iPR9WJΕng#G(OчntPƆ?m ;Sxu_czʃ4"Ms:4ם;W۪|$Oݱ?HSyw]YZUWW"4+a5Xi?[< _ߨ%ʨ\҇Mŭi$׺LH4Է4 }XR9 tW jQK;.Jj^r 0wtHjXY+..*iZQ.*mn'\6uJQwޠOvVy,k~K+5&jZY߫~dj'h2=kkScbПYW[l߬5fTf.;Gb4; A* 5_ z?/V÷ض-kr^ G3b3zqGEoPEqs|+G'(d6HZ<]W+WM_wIOi^S|kHYTur_\w>_羳9Ӡ^W"iZzܻWxmȽ<-ggIx6UA >)vM#MeeӽyEA*hWH\A0~WAӤZ}|-"GL5HsAp].4ʃ4qS>N[_w7iG->W.֠Ol$I8,1?C9tY^y|.;|KyA:/|e[ϬA_TP:FzLY't=yH*G"y)'0gX| <&Hs|;WN? OW:ӿ Q˴/ECnNJJz4Kȫ5Ƨg4MSs=׮hH[υPGzTAE ;E6$yj*:8y&߸^G%v}6;'rʣvM38QڪeڣcOtԃ4/~N= FRYwUo:}Z}L mAtBRLIL5ZntY kߣFF)\^yf?=iOr#4y)6=]h&" FǓj_8.ڪty,I"#֟S_4Uv.?ƫ]X#l$mȠE̺ ,arӅZGH[~L494FR36@KUy|W^iU_hYH*T0Ь2͜ ՘>H^eT+LW&:Pm 6!^6A Oʧy0hΠ7~2&whw_{nz3Al~O2M ~vBO +SPGI;.iAIKzn}$i:=}kC؎]>[$Q\Ҽ݉.61a75Dտ~ΙU>ԃA*KѿgZegJ冚LKi?NĿ@4m? `/ۣ%h;mI~*S#Lm*3|p~؏X3m3Psє^up|P#R?;ug~{2,iAZyAO?@Jؾo ڷ} z㗏OUaGr9fJ).g DO͖ŀU/SLn{iޛ\%RnWՍM:yϙ0i66gt!I~obϾ'ߚ""4)c4~E'2}\q:|S+Q3:C>YW?DfFm{E'oܲfdz;~ա$oAOiK ˃4<z:6оŪo ڷ|GYm5P}sߟ0̷zZs<60^\gpx. |ow8 嶹x2$(mGi+A}P/wM.+Xf~b)+'21$sSδn||+mworn}O 5 :Oi sy4yISwzO^.4<2mu.xQ<қ"?)5x.ge"OQ]J  ceHweeYٷ ߶ٻӐ U'|`v#?/MQmc Ǒ6my#UĶ*uq ^}KyӢ,dlhj(w4Ƨo-MMS::p N}}􄾱n D0(tbq1Pݢ;#sGtO_Üo*y3r" Jޮi S &MrHMw>=ŚLM/Oq'9Lٟ&z3&F:m8QGlwm4S}.[ 8Fk6&2}tmS8m)k=-m_䈙{.F4S_g]g?+mL<=Vq^`經O>D=0dt9^7uo;`EN}1٥xn|ǗQDO<ݾm >f5 *5kVVs[Ny&j&'!rව<>!HŐťUWy֋1so4(~P5EĔ_<]WQvj3 gLAEm/_ x^;iҖ mJ^`շt<-~52&Y幦(ev{lKӧpc0Ρ{[{ G N\?MĬLt=ϢyZ4݆L!#u5玧.9*9g ey:S LYۅ7=M1~Yxt[E`qe} Ho.rcZ|^.BV-#!gzo8?neDG>|G̅^f_U;ҖiZI[Z_i*f}WG%Mun'wKO^:犬ybYR+>]VT٩un36>/*:QI*6>M[6mYѰzpx;eMetD;NsC3ޱnrgEvw{~Ձ+׽/岓W%0%mTC'PpvwhNWmvsC;D6YڶtjNAYqүr[F> غ#`ob2y:ul:4:yԑϟjbI޶'s5,v=t* X! &|~MdW1]չyȜ}Ɏ<qF4rϫ#zK;Z5~qΡpQDa#i. P* vL_&7m}o{}"&8nu|W1R;gmWrs1<4Fn"0Q߃u] He8ߤb >_9񫪜/>6zz4-=;Go2;ǧyDo4YJi{sUm/܎pOID2AғrJǚ^/O? }HIRҤ-/sO:4yGwry4ׯye:n#~=KRND'FEۛq7gůO<Rc.e}ı[Xn۝m 5YA;;R0dCa!F9Ӌ2Rq*$ӟ ϨgUs.}#F5JsT)=SO:=#gmz)=em.CknLi RvJ_wg0z]AS`uUѩ CU>0~X9ww1cszEwGYyښnct)Ӣ QX1ۆoczg ٦$ i&z=yU gz=R<ȭ4v6ߜVSSoE[IEV՛['k\zyG,ל5ֶ}[|J$iz~!Wy'P{ISoit:dzq./v?VO+MIAҴ%mrnzC&y|z=R< Ƀ$t e=s4g\.ygRƧ_Iw˧rKVeDu{=ZFĺTWt?u\uy8(__r:ٷUKICrw`>te)^o^AR6_WN_9D->zj!_C{sLV>6E;͢gh6zP94`7U2PY?t:ضhd>%s1zedymyA35 ȣ|?< [`~sy}"}j[|$i?_^?*Xǟ=RJS5 AW} @z8x(:WרtX>gM'S^kPnSw-EtJu#r?z1gs+k_j?B:+1=wIX;ƒwb9>wo]ӛX]dW|}/ys紽$GXy=N"_,+*J>9Jn5>]ğTuNOjQ^)ThzڢKq^Z)_*ddm5j, ΡDhٹ׾:Y#,ޑ M,Ĩ^1b_ξe}3yMǃ*%ԇ:ݞ9풞Bs+D2Cg˧L11skA k"t}{k6u4z݊S14_E+c:kKoL@6F[BGy>?[D1\uOu漻~uU۩Z*_ \=FmۄJ/;4{%F"Yq4,|낤~ˤj~|5]SuLnS|Yo^Y}ROGSۚ1rEږN` e/wa?H,2q2Pp|ӅAnXޯJ55 #RS=Y~c5t}퐞 >?/"@u۹Z'E H%g,\;t;!\ZbFvn)q^\#:.~QNaQE 1LH*T;}>Ԧ#ł A*]ڵkWǏ*t{LzPhG\6i!HD_evicF Scz>Gggg*|P9jCt͓ejȤrykK6j450PsW1$QQyz>|`vzPAS {(p_h!DN#yt%SS^lp|*Y~˧ qj3ⷠiĴKk)v4l=gLAsPW3yS94]N~mF9ߜN=u[Xy:{Dby*_SM[pn;=rˁ"b47?dY8񕩧EM9ƀD}ǟ< Z>'/دs7[G '9=\H3{ig*7^P:b /_}t6Ï!3}umVO>]o ؜aSt;J~lA*i7,7hOkDg$}sΟѤ s^:V gc^~<@cvsY]!5D _O{~sÇtKQ7|2?XMGش1/Iu| A4;?aDnmI9'5<koY>6*cuU7yjjRɃkʸɷ)#\RӦ ֨H4}xݴV{ƞb3!j5vm/_X̌ڙ=D2QӴ8ނS ϯi}N4.;X*A#z=d5DJvsUG<#/X̦yj>ܢPiz?O:=5~'ߖDwwrnK#|<꾦toγwk-x"ψO&xd/'Ǝi@!.C֜tqZ0Qu[FhBطZD+Dǟ2 qa˃f[y1XW2v|@s>iF?Qz=Qo]Kp8yPeDg^S^F.`q3f٣MbP!*MNG>MO'oN » :ݳ:c^ubJ,1U-iXKtvd1RGMcם=1#r Ɏ?]C1nfom &!Q5Zd=hU[Ŧ~R%K{ 8ۏnK譹rO+j_(QHyCU>*sT\c06@+5u?_Ϩ:m'mMjSYI*O? }BR^éhz-z}*S<ȕbќDQ2'#CsRg#@F8QSXe򙜞ONwc=aS^uya&H~# Yŧu磺EeM| @{͝lw|viKz~@ c#+7!%<fy-S"4dLiu tuqAܼ{dyRaz̤p HŔJST3ӷi-jzKjZj=Γ (oՊ=u_y9~ 7wċK=6Ky30ꪇg#lURb ˚+K紽^#Ͽ KLKi7kTD#td>Q;1>D:W9!#G9 ak4ejb'_'#U~,w՝D1^TT$G:- w5Փ;ŗT'w ]$ؿ5 ^IOBGN+1EY61 2֢#wLz#zMֹor-ҫiAl\K_yNO)y}?h{v6H=hm_IS.t(yg{ -A>5vΊ ZO3VHJ@Jw*s~3zXc@R1<T7쓷Eg꾴kAkDoi1`}o=U7}{)\}_=2ᆵ<`m= ywNKr)Æh#ZQe92BtsG/FNs[T\.pHyk*(BOe6UnD:c?/u#yUMag"~^;AJ& ȎO2_Kޞ꬟vx7\͞UU]/ыwV92yM"*X&]C' o8m]b)/P=sGcQeM.3dam0YGzD $SkY#X=Xrm'WI j@ .5Цz 2Z_y&cIv$+QlK(Ed;)agvQƺ$/G۞u\ȟ_Q) t3G@ӽS5MRb9d"ry5~jDFmBESUN:QsDҞ"N钗qG4}nM|B[G_V2?k|M/3SyˑOOwtZsUy pP}] y%h #RR5a:ݬ|Dϟ"@3% !HūԴGҽ؎^r*M4ZO4q0a?H?UT; wR@!HC r A*T; wR@!HC r A*T; wR@!HC r A*T0NiJGnG/JTz[EJǐ }{,~fIV?i]tppDY;B }Ĩ^Kdu~~FVg_28 T0hyj{9m/! u:+4u~R?TS~^:{kU(RW_NP A*P>|VReVsT5l]QTRqryM4BtKLGdڦL[9ޙjIM/r!2Ϳg>d7tbw1ĚI{fi=Yo^>nk.O#^cM%E׃_ ]N7k;[9Gڟt9ߦyw?sb.DzulsD=yٓwӋ2k'?S9ϑo&~dg ? um|LN|ItײcO=i\[=n=ST{~(;fN*6I߶kh2#wM~wRwz:js;4Sf{y"g?m>'L CH;\snNnQjtNі?ͷw~N|sst:Sqj5*_nٍ7k:]2aKse{zfqA(o~f~'"N7٤Q֪<ގ8Y/G:ݛ}?qz섥 ;1C8ƱWO聾3pFWhk]eC/9䝌|M^wpFPM ؑuDψK1?g?in]vڽYP9zju&Ukjνp>u>xϝّFO#{jvGevktYbXAs:9 ;ޝ 2:QB|cAND!*dp{4Շj<~qXsM/u/G-;tG]g~Ol3yؔFZP{D9=wyDfުvE݋+9&&_.\`:=5ڂ^۩)d3:{F^5:PϘ ZCϡ:e&v0n\!IZy@$ E P(33˴ז3N|p@\-VP(>ŗ<Ź W]>B#N96r]ttטHlpSgyTFG_\HtvK޶QfcPܡF#:VC,3֟dǫf/Ȟga8stCAY|JԈ輓&s$ OJt|xݹvΟ`[Xz;Uayq1Mmړ.iL%Ku=/Mr4k}A+9GDTKl28#yvJ3; c]Ϙ-|^;e(F[zudԟ!HXvZWTiljjzq۠Wngg6EcߧVԷ/,MլOMO26@5:}5Z OW<Я. <җ jx (ju3M}˓V>-/U1E<4X>[:zɃ_6:mIdmgڴ,IоEMV)w&aye9F6>\.v2'9)ehwLcDBv`꛷#33ƧۙyQSgМ+[t]SLd+Q'&oxND幦 O Ct 2#3T2Ϋ"GTOmşPS9+RZo65 ^Q Úw"D }v,8m1-PN? ' JRIZLvjT/T _0hVCnG^72p!Hutj3_Y9Lev?ltY{j%#(MSu핯XUiRZ`'n<1vYnƫnv\G#Ks}u7SLy@ȷdkkDԷ Ht:IZ-zC?W*?xnFN'ȧh!{޳?;`V0c`MRoSkO߲_o"mp49ԦƲ#ӏΑyvA^#:SxݲLCZ|4 dZfy8x?|[n챚W;׷yHlo,qG<ͷ&Ɛ8>s.M$-<ڃ oa@>i$aVdI8y9FOi$o ˔++~x.8ّFU'#4f:+a !:Sh , -12#`ϫ L;(!;ӧEtBOQ%xj~D(GQ&hE0ԘLyzh 5uVNZbJ3h4pCO/wF9[4_d{b0 #9,pڼ8ds:ׯz9aFhg9yr$T2MhA|=;iN(\uԒpXR|M:x:~gE䍓 r v<ʩ ĒA%5h|w)m5/<>~?1$-s!~i xORI; FoQub̰X3mp[{U~Nia-cKgQRH -HrEID"pŲ/(&,?'$ Wޘ[. ;:v/@⪤UHzƞϟ=yܤ:rAg5ݨc1Vѓoov5ʛlڥ8&OHoSJZ7q~[{d3plzE::a} 3n5zfp2V0a7*eV]rymA;0_&oUVPCF^lĬYWvO92𖣋npOm~: 6?-%͎ooꎺZua/,_㼳y˧0F )|}֯R"^ʰ)Ś?ox{*:{Us|ԩtO }65lhSnXBV A*X_O.Ά"^wMmNC&z l3KqlZƆGʃZP Jb=vWk Rƶru%UŜ=ԝ 1rk*ո+'nUV5P9Y6"|w Ϲ(E6kZ)ST NڟM>鐤}&!+5j8qq̇l_!a@+k4}}SN`NnfzEm{zs$]t\vMAܼcE=i[Gsȸ9je!7oHo}y*S|Rd?}L)0dSc6_$6mk|es#=)>C\ dt59l4ɚGO^m^:SNT~`_Sن\WϧvϒYx+U(J7ʴrJwB5a./јJCc5gxz9z5zq\ۘDzioco6H^xj6ʳ/r*@-kְ]_n 5t۳ uy-<}8oWX*֐-#4;rĽ/w9C4GC-Vi+~cows;tk]u9?W1SǮCw9Laǽ(OC~Ys*y :qvRehc:|Si 2ԟg{׾5]tztYzBYfySC,CnﵗZxOħ.B &a$מ xD9NTIٓn}k_V@+6dc 2#sׄZ`ٗ6 PUa`^JST7TTK%ғ槱SlhrO½7pjQѽ=:bN@߷?ozK5|s3yvwZͱg\^U̹r҇eX͇MZ*qnp9E׷^U?atz'`.0_4I1W!ǚǾn YD=$_$`)/~cSϨ/ssH:쉷+ JԲң&'MOzT׻E8{htK{ωU}y(m]ەP:;]blҙOgqTP&Q1*b+feR9ѿWͫϒZ}'x>碨gc[M\ ̦ϟ j9=맱ݫnZw&Aj 7,f\vC!0DK*Ӟ 5krVZy۴ʆC_` 8}DɃZgzN$]#2g5;G"b5):Mw膑g\*ViXDͷvmW_^NOHݴ7/Q Ju?6J/'_Eܜ+"sO%oa7oTs.j|6FQpv?隫Z~TA*X7Jn)Z __9m4Jn]!<^sGţA9rK[xE91Wz6mگwY wHל>CDl#kiys-VI Lqj3osڮby5iS=tQ'}5Q֓bT^Pj\#kP>k%QoF&Xz;~0"|w[?laDGu-DQ y`f%,XP{?aw cgxT!jhOV%&NtɦaJm*s?(Z5s65YTPh&嚋z ]>O Mڎr+j}ZRQN'5%=q^ۅTAt׭O_|NKpILfTzuIdZ~W6l6ߦ{fcH"JCxsW.G{uH+1F~.;}Ү_oPU~pr]>*%l)sڮyV6Uؽ Îa^y]ϐ`Jg}%=ohv{?3 6ˌR/0ÊZbDP \TVQ5K@Г @hh [;_kOm'E p Ua3ZE =R\ fجݴ]vCTBO*=P.zR9fZ'$=z*nBz4o6sk{oM<&gg]f83(mzRz ?R9D5'CI:zLH:%u>ֳRgaJv|Vhn#ߺWꗈGEI?;j<sǙ1zUtVvNNܱ]2CҾHꗶgL*q;zy.κy:?\AjH$Z$u(nZ, ;#ZÝZnedN0Znnozj:"N9bHSwy~bMmakj1^?7$cCc2iaJS%fƥG<ҠY'K5_!2M˾͎}U&IX67f8΃RN Uz;RN)c,\ .:7k} WOHv<װlOn0%ꑱNӐyGxQq'l>*joH{2L5="U;0BI %KZɃDA6YڡxN%$*F5wKvJc<DX.یai3l@ЫhJ6Yi:1Fccۍ_IV`)wA 6Co'y}0d(֥&9U*UgJ㼚8- PgUMUa{Dmz@RH/}:ce<_zo٠p|4eqek}e~G_J[R̨]+kGϨ~z%͜-=s"pZz;Y>f,="Sm/j]ؽI3v/0f氆EӠYnh.rdJ@=;yPr1zx!c=Sos)Z9S2vUy ;!6o5 ? R>V_=cOH58RuT`nf aD+^MV#unN iꉤ-#^j3͒C^'q0P4'f/8Ql{0s0^%*嬂}iW3M J2/u9܆U3Ճ)E3T,RЩ_*v>y nx;0w -!Gᬇ:AZ gofwm'"  )=tQ|%qj9 rTJ^4^!rs!{ʘ=˓L/!ZQI9^n7qR*Vz9\{ eGp ]gP=k.3|?VV)@cƞ nz-RuT5.hT6{a)kL&u-|!XzЯ o)}oRs;m]?mǩ<{n}Foع?TyƿV=K 75 SNV3@ƎKpz^0gޡѬ!Ss$/Xb 9X X^^,y= :Xs?ʸй;lf:_foLy{X SN9cfksQ϶Kz(נ4skUCe2c1u F@gȉ׉Qyr ,\Ni5#-ҿI9dN+.N1#NmvX4^ItB:7bn(ɪ< xn{eOGhn0Y|0Ympet;LCqؾTHn΢f92v {'۠oPg5N[F(uD>O!Րn9o%"F,y?ḿTnLp_Bb77\2VspHbuHV̞BC zJ"q͘^t(l!;̼wwX||lB;4m5<&i¤~MrM8po k6gI)&JQ=Upah VsjA*TUzr8;]1 -)٭3G殔Ǚ |n9̜k(#sy<]l7T0:6O.\l9ڵM9;]:쓔O:Q:NA*NvXpKҒHJM!wM{儧U%SL A*XN&htGeB鬜2X2'z۽M)G6MFyA&sBΜ7- AװaOvHd)i./Uv/xrsdt_%grzFvk\X=BA8Sy`{q`l@ Y̨ɴqJOJ396mF&󷩠XSy`S6wXG: ,pw H Sp=z)`n|s20^CO*Ď bp 4@I@R v;T*"hԁr~Z;J|D!"={rHjC4$z٩%%{8?#Rt% A>60]m dw_4&z5}~=$ZR(dUɌJڵ~w AkČLK׺uf#aWղYm8e?zF3{1K,X`G*^T*H5)ze45Uߚ_!:{D%=yXyRFO|'鯫ZP*q4r{Z4wʚF6ò9(rEJO(V1tz/#zASmi#7;ϮY \wlU2??%-#]eoL0i9lj"JS>A)V=@.ٴ7z"=1ʽTwom;ғ#2U6iϝo 5tFE6=il=(~%˧/>'%@8ߓrӘ 4ť%u@w#rWFҜ6ߨHHN͆o.!hX,ͯa7HoQ:6S~E)_wŠRJTz([ _T:@V–ia)]qշ("_UDDk* pډ'T=\^)[Am+;Tߺ~1"LʚwKCb G `VM7^Gm&ղdty2,u]oe$!-2͌dn@e< X,lB2UwHǦk4JE+S1HuAE{u[`g4^ |uM|}4BUߢ(LyUE-"HUDAkR ԙΦTS˴@6U3)`\!-JU [j{}ʧns5E+uV ۞:kPiubrM8p_hۿ_IIKCefim^Q-Jw.f7i.{Xʙ'ޛpyW}ni\Dj"HѰaOv*30Gv| ;%`.kNiLΟ+S(;ǜch]WYI6Jg~ *(Lk:FEm+Nek7ܿ6d6=Lp{V꛿Uá^I\&sȑgegM7;9h~kIEst}9W|o,8.ҙΡH09JW?w.fFIu3pD2rHB%#]33)5=Q 1DVud>/k׮}l։Y^9b_c@%{8lu\i_5b5J"(̉Q٫gu'i&I—N[D%f'ֻ Db1m:h|ҙA[xsZDm6I4}31iwYeZ7oyVv7U-ӯ ,vޓgnɛ.]g{\׌Ց`r~Y1sj܊F}Z_y fTѿ&*m۳b=/ǒrϬD\krZtM'_;laarN$]#2 jw_y*][d+$LFN .\4c [D|rtM*fhtM'eKn뺷T#*Yª2 =Z]gZK`/ҋLuҾs>!"}9H_yf&3Gr_UERTT!钞  Mjcj~w:҇wOt-f6XÀU{(91Wz6mr=5wN̖JiCn5# *a4uj+khY˷(S2dKʚOڞ H<6mꑽ'ñJ׷(jL+x?=ꘒSE2HJ4!wܰs@ϲ3f۫]fϭep}9~M7LֹyvN=!an{x 4!e`S2𢱨`vX1shǎ[=$ym{d{V ;:q9Wﰁe90.<+?ۚ9(ow>eObX{l1/u\!uSVyx]nsam<ŻNkH]2)w>ﵫLu)Z=>gߺP0Y?pS?izg 7U)'5 aݭVA5^eYz brk}bL(i9sgnO&mgC!ˤ:5φ뱧=.N>9l_5zҟjRWy_?aTSuGF3<)î:Bͻd`嚫uzYiWڎCޕ:μ+z]8,rG_q%uz U*ϵZ)7BjY+]y,- 74a]uɌJ.'gBa=FpΓ>/8x+֛Aߍx3Gs^.Qޓ ezґ>_e{8WsF>f:?jl:owq R畟UVV\ Xz'S'.?Ϯc9,P Nmm.v߹ Uykp^?O#}EkX謀lp! ~n0Ǿfl498$].tC$,X@ Pa -dkgnM{Ab6S=Tw!5_Ϩ^ P˧/>'%RC5KJR NIX6{>=TgۥN gmmXtȊ +आKo׫Ws(1{ʈlw=Ta\#8+G7#TGҳXPOA-qn7ʚfգE5ȟX&gӥϭ^~?ܾݬ^'Uqu<՛NO>t͵O׿_qG>uC3Qc-w[txS]ePtP⬓*g旟iϢzu-lw/Oo^z:U]Y̋`ׅ9}psSɼIVo 4PL̳1^{I.Ϫz)w5sN*є Ֆea9[C|yKl%St{h+eV(+Fa?g-3{G>.ܾ!Ԩ9Wy /X>|*?|$˚̕ƣ|2O2MCY8 q(CV'{z ra^.tbg9T9Uu_$/ʿ_湏3aPNG pK3?_*2br]ddeY}nd9r=:ocEjV9zFɚ,?vr3L  G`n%{X[_^<*[~>vvXFWO4Б=G z1[r+TuGz篬r0kʤ&_viS=A0cL1לػɪ j./&gkV:Wǧ΀+!mj2sg}Xz涊{#}9vJlvx@@ {x75p30}l/d/dzt` b~ $ԾV]jx4G~Gj跿reƕw,=z|Luy^+n wq*s: Y;.'`_[g 6mߡ8|01sw$un]..A^S#Q;_DI^+V53#0U0\]}{l{s$Q;Ηwu_sqkeZUZqr;T`IIɟit{(E2uwnOu6¬_!If92!;HǼTtȊt46XACeTfCGc6n]A[FyJXVK 9mtl MzES=;ΗE~=EI =Y~PKpǬsݷ9;w׶.;GC=ZVyow[jXRCeݻd[y搋!rTf]lQj](ۃ2@t;#>{=ߌLF}~EoE0n`7ZR=cW~M g,{#&UAGMykeZ"/o/7J}7*'J,X@(׭O_|NK7avQC  ]I*)1שj(T 4T}/#=_EqTU>V`E\{VsVz%VZ8\^f ӥ5 }dWkuTГ C F @lR v;TA*Ď bG #H@R v;TA*Ď bG #H@R v;TA*Ď bG gH~#m?8&ZK U-7rRn%9=e.)-6@T\߸PE ~D!"R{dϞr XH\-k(6 ZO!d2l3]xZ!X}\%<,|{^W~] B9D8ZG?TZKj5II?GT.g4иS5럷գa0D^%/3'2/%6*UuzAQjq-2?.Cw&T`l"S~Z'k׮s+?3?*spD=7%y>9[/ 8k*%(k:dφMŲCeŘtJnT.W}UQԒܹ>e5+{~&O?~lundSk[lLr7VcѡVA+}6Q$_&]>9L.SՈoaEئp׏b8U|sស4],cݥ_.jaʴ׏˪瞘' QA[/ (Pma˧uufϵ_/pz GL )XG .vJWʛ~Mt:Ks<TA*éi;b!]auˈzwfJˈ8뺤:yg{mMK:^Tݻ;L WT˒}z+3ړEzbR|xD>~[>2kM\2UTSL6P*pl-0;r¹]7/cq5|*f_<ı'sjUAz>y.-Yq?Pgun>c~"}6׵v\؄=rUpfP:ۇ -Md$c헉ҋ 2|r Ht-"Tjؼp{f;s\\\g+VCɜWܩb e8GfJ6KA5&Okjhvf{SM 5d1;rP7F'1H5лzZ-6 uO,R6n0+l0q9SATE}6݈2Vjo[itPʭ{8vŵMTyp feɼ!? WwlU7\?ٍ%bdy|'\p׳yKWwo.( @U=4 yGۡ)d rnM2>b){?_F䭞cYyDE`nD*Jԝ2˸@T9Yg< sn5\G Ʋzvg);2!ƽ/a?rTOK:Q 'uwKfǶj>2}fXWV>Y'~;,5ܟ\e-5fK/+ΑD ыbDʐɨ@LLIi쓾eOj ٧z-ZkMu+J% lZysӵQϼb.)iuX 2<#g'`XM=~!1xqmQѰ6Ky*Ng+mB`-~?U21*>a6^Q `d%`yĵIa5P^]~S@%4йeY[`.݇v#XzS*urn>"O|SClz=b ]?.qj9j@-w>*ɆFCzCT*ڿ#ȥ",A J5%$j*xxH}e:@eTMk V*=~9ggbXQ՛jo&ٴi&Z4I~-{aBSCᨾkY|]43\:{)Rmc7erBGٍoǶM*|}?*9uUٽr9j>2ue+(]z]-rgnf ǩ< #zuNdl 1n|_QCezݒ9OZͅCe(3 ezDepg.63cWek}cKjȃs8a69&koRgL~G*=LX4`o\'p=sU!j,`q}j/ԭ{~"=/ {p@Gm4{Rdiֽ+s6=Oy?fwt(?KX~VL:jOei͞o*%_cW%q֝St?;XMqAA4:ZyP@-TCQ}USKnȿpAd:;`sD*2{;O@!]tjN+:؟ Uwlm^W;ƫr>A*@R#U* F|ˡwQE.=9,{OPYF͗{칀7j=5%s,suݠkNp_KTM- kÜh%oCU-m*ܱX#s4zTejnJ3Qh-j=엑o_m.B*U+u+vU }᛻$Yu_T^<(XJ-5嘖:`r{[1KixXn_}+ûE>O3 T'ˬ*pq=S*dE}@Sj{n0֥U-RrwJMB]ŕ 1!Vd]gZG%}7<= GoS<=Њ(Z˺7ͻ3FUnVܜ"HPz>qt ?󘉺SkbσeW^r=s:3&Cz^$k&;/#i-Sf00UFz5`}*sdEDAOYՙYR.+U""ך9Ik#jka CC䶹VZxnŹrdPJr~V9W#Ca6-9{4 b6NA{AK̟sY'7DU@a[)LS!o~xlMu海zV>j<xR>o%=̃beW^t;P K @lNڟ9gXUʾjϑ A"/-_zEwRs8ZJEޭpJ&Rvw[K{"aeȜ+o m(Zk7r@l/~3 <,Mݞc%9ts$oV֜=*O}G䪯J+ӫ#mP=[~aX-W:s Jɩ|}[WnO\C-YaLZ| WV8q.o 5|_|cU'S"q,n?&WډOm3]BG=Nni)X\~(:<: `~r Wܔ:z Wwwߡz!i_.Л ̤yR=6y}}}h6Oǔ~= `cg_v}٠'V Hŷ&yF!B^PIُ=Wmkog={e՚_ u_u ApOmR=3U$@AE!ø`zzVea/ې^J\ۄ˝[FL::LKaɵ~sS=ʹ7<~Q>ꅞ(&=׍~ik!{{3|zTqj}'rV}矉^u dI,VBOva>UYZצf{2q_?aqY5VGj dkLg9G'RJ*5Lޢz摲K=L_S)i~LU>>'6,\15-SjϛPzSGO"鴤'Gw>ў?im6ZўM>ۨcMHtQx2QlD&+ަ8?,(_i+87ԯ烪'PZQ);ad*ezUKk5<^ 9{\=6ŵMDe䍗f9z2ߝF;@ NPQ|2瘑,˸"Ris3S8uyOOsu[PvZNuWz|f.ߪLO O`>QhJ?<>ۦ)&l|KHeXـ5o֎mҾH=isQ9z*Nڥ_MfJbUn.UҨ'&O,yX`*8(oq86@!^\sճ7AP_\fJ\RQGQzd`$ Y0$lʹ <99k6INm J%v/(Îq#O@j>H!9!%K/K~|Fo+{wW6t{ޯԫKdIR 4]40ytxˮSj:3/r[YI\`ZgMs<{>C$ ~1'}s1P ٧gE{~KI:vY6M= ` =XdO>TxT6Xq>搆}T~27bSO`B 2B﫡RK/d 0*Ps=vR}~zvZ^;a_.>Ԃc0qcHp-+oz,7ԡ閿m柖\^t^ =?]z]* A9ص:;T+ʖbsK%o-X/=k}o\k~ ؛ȭKdcʎ8ay>*xT$;]zufȓVrLyJS3yQdSd*t"/XrΧ\A(yco_RF҆+~vPAårvImxqΏuz@LQQv߼3Թd@Ht k@y ſ| Qu[| J?/-+PA\Wv;bkj)ߋQD.yYv}xg۷..0Qy(T,U2pND^t>~7gԵ:7_TFΟXAyR<_|YFqȓ̼0gr}Wu\iL.j`+oet-I LrzK\v*\3oɛzW柾&^W= gS~c WF:zC]**?6*fPk\3>#sS{T =aon;&i*i S>*G *z_HIө_QxWjH^}4쵹`Wy>~A-L9,ٷN˹/>^T:q_a+e{=yq.#_\"/#ظ%ٞ[ف\L?{ |ퟬ|ɯuչ}}dϻ{S%կYCRyHK2T PL;[~U2MAz)9?~sEͼir+$TUЁqb tڕf5WCPA*0y{Af;YwLN~/5g4MK+, jH6/%~_۬֔*߿$MG!B2{n ʣ{A9\(>Tay=)Mf)'ݫ*2٘ݿ X}$E z:z9^kj?|TvɦMv-6ApݛLיWO";0AKzT^pX6=&_++?l`_.W'G$iNa>WyK3ԟ6Nșl(n 6ņ+V(+=*#xs:){vOCP28*rч}o rՓeBۘCY%kr\fJ~ߔ A4_|buZu|dޔygf0ߺWXqce7Xywwj@jYQX.xR'z=ywd?|ufHVc\wDs;(O1CPĂ I5b()<ՠ<&#;U<歟1{Q |^X c_u 6rCFpMCWNȧK-*fi3yyDJf?{?>0dϭϖOZQ|R>mbn*sYugyȳ:уG#HV>5AOzMUrޭڦ|JX]9/j:Hl'Rg/~)P;TPsJ>S`l RkOw)\%gA*%nujU9AԯfgnPv7wTO;'qԼ4; `;HUUw:/tHXv6D{8/l?$W˧n^u_w |"~ &d\#=S_LhjLF2ѽR/s=L OޑsS*i2?.CwTdlϢw"ll[2~˖II9QyT0׿(G?*[7=Oђ垇-㖚Өzۭz^ =Թd{:Β6ٶ̪o\4 ҶQ~HmOGzocj5ɻeJUX9n.7i{|5-7 GMow9(s=ezBU^0e7y__>Hy~9h9;DZ=fe#(Cc>>\KyomWE/?P+>E_x zM%c{WIjg^-PVn&ACc2M>yōʹ?T'mLMGb\q׀ ( k7L=ۤm-{(?Crcv֏$,˿ rnBzo1d>^[/w|QI񯍺rHJ[=bY 7%W=?\#/wK륙˛C} FݳŬc~y 'Un>=T8&}X/)J6]Y~jZv}zqүU N-UE u3&~k0#mBzt+~wTz;z/"5|^W"#?"Cyʠ1޿O9|oߕ}_MHj; u)PSҺcp**)|Wv=/\ӃQCaetRa;/Z剷~kaSןzd8*WjI]_(ɂ*Wow"i.iʻ.Yf8dzRܼZt<a\Ȫ{IHc=? q~wtS>4!JXm{|~y}r_Im?.& x%أ/omzKh=Wr=:ek'~`oÿɝt_J>%qc.S~?i+xCҙgeBL|Һ]hԡG^Kә ye+<Ex4x IJoz1e 'Ie*8ufY!%[ Gy`'?˷wu;; nW D_`my@/Q0;ŝOV/Lz"Oz|T^_q9e=1ߏ[\ y6&/PYWJg~./I zÈlܽO?_'f.woyշt fJƞ~|ɮ;wWˮ*S&eK:Ǯ۲ݯ@ 5z Bq}9b@C}$o㋋xq1p궊50z}+Lz"`R/WQ'_qjs/ ?}vwǹίg:w>9|s[2$xzOz|D>Wy);?V sk\?oN;1o@'~T١llH>5&Tjy~`ySn FLQsa~Rcy6HvT!N5zpZ:dA߾EښHbU~czJdr>k ydC,_ 8LҽLS25]T&}lA`Qs9u7P4-)yi6e,c\!Ow6 U54\ȱ30LB~c>)Uɷw t%Kww$}(oS1_&qg!wj6IOg˧`gfY6%?~n8MV$wc_z;ɱGbG)~c cqqR?]tgUR>G[:dC2|1TdQa4$s=43B.[7%,+Sz!( 8#JuG]W9z\h" Wgg*35LkY|gx(ENɰ=O Ӳ~oomoj 1Wo~{r@:>} I eS<1<.vrvF^6r A*Fv7D~ף)=, 3ֆt_;~VoQښ\~$4*^>%\|Mހ VK=?UOZKf:XUVT 1q| 1\ЂۙA쐀eEOvQۯW^qooq{T.oB==q]\e'Lf>6UY1vٶSυ5-e"rЫwP^z]:D=2xF8oQwOo?xWyGR՘sǾS]|梨.y==6[a`L>"'uߎ<[Vs!6ӏJG{;!uI6w{PVzG!-Flz51O@=Ҷ|l{ٿLV,umʣvOc)A_̀+Ny;rz}Cy@v.7oGޢrBQgı-2>>}kY re+M{ܽhZUkBZ=O Ǔoos񛳅̷7zͷTV)̻ߗzd]֓XL} }tlL*\@$Uz~nx%)zBWȅA>&KZ%ZݽrcN+6<*=+ v pygjGhh Ӄ1+G+9znl{!w[vlU:Rrނ\ގ}o@^ҫ}xQٸ1'|o)iOH=/.e0W+{dcc._G/jy=ҩ<ݗ~]G/h٘+/?ܔ?/r*)RAz&/usWKzZ*":7}PNzX1OTϲ@CQgB]xr=VOo=hb6Pf|ozR\BY?/m?J7|r#zp#'ZD J[>`|꺟|Cqi Bc?TݼIfXq (T(R\ @R v;TA*Ď bG #H@R v;TA*Ď bG #H@R v;TA*Ď bG #H@RaNK$"?ُ{ԄCn f JxOIbL׵h6<_3|zMblBzztkAX}߼Vz|:*u}u GfԟL<也ߜYUZ۸>I uwIvӯ#suF#yG?;*Кo7w,0q?gN{^ :H yT"p[d~.W&3@d/_ ُG̬i?,ҾH/2I}\ ̪_P¢ziRo|կ^[Q5h91}K1%S^e(';doO ԺIP簽X=Δ9l)e,Ky;ϱ{Zx7Q"oCְYʇK*&6w}HEXИ^^ʘ+?̧yJu}1]O&i+Z}Ň뀵us0/P_ސjao%C{ev>*~q֕2>:|;E{v3ϭfi+#-v`J)ϽĽWWt} >U^:GזFHZᢷwCw^v>j*QLowD9?Cfdx*>$m(z/7#FZ;5QrǨ+/!nH:\CɻxDF~>9~{?t]\gVPfGO&IrIsтmraPjI֩W$yO Ȫ{VWL\UG8j}'ҠמEZ K ص~s4\e:_PyojE~<#w45=0n,I#a(:9S'[{s11hBj0ڥU 6LN:$÷aumc+*Ѐt<+V1뙦)x},#نB76Gc]93(lW Ӟ͟ytg21uҍzOla sfxX~Lj;f3 ;Ts(uMh~RVnu}; uȮf52-SǍ=fp t׈fxykk3]2=PrEHowTs9O!H췂 yW*}MD,ϧlB}RY=]=d1͊ 76?wtH>$^"8}Px\DcO<.v*TPjH6nC]& zjQA>+?)MN{Gy/Y'#^uRL?5ʻUk"OZ[b}6^Ny>;|*Z>a;>t}ce/ȩws~z~Y (p^A&}[Jȼ{yOx7ΡByPʃZ/ĵ3ؒ~b~_>0etժʚj#Hdփy8="WO?.~u,F7_YV ~Ju*jwTjZ=Pق4Q3\s9pecsIRq7F},)CŨ[熥vKΪ TxeVp[TxY= ;q!0K܀`Vowc @_9BϜUk ʿĵ]=oj'U}$KrZ9i {x}x%'Qr$?8(6KAI&; ?KzTUQ(lQSf,=;s18aSj#z';Ԗ^7/qp ٘VSPOyP"OOnf*qդ{e^Wp½l*UCj?_'yfjv]En/4oL \zߪq^56;/ZGj{f{G?w~Wgny@9RD-tOkLf`O2E%5`-fÿٻ(06Y((kRD68pe9>BU| .5W0Ae>C7gJL Peu믞}I@9J"Q1տzv5y@d YT/jg i+vըh61ҿvz}Jqy[λו뚨t aV^TnV|Nn)S@KIu2(m 7%|EI~zB^vDL<{\`cM L 9~ `낺pOdSeU6ϧלG7D_X[֩=IɄs\ltp3]viX {B:^sVs2g~sLINXډ˟<+Dgy|bx<˖nuIݯhcV0Mfz*)Ļ@ӏiȟKfURw̲7{JbR_+sOptSױN3 owêGVPʽ}3ȽlqSd>,+=(J]Wr揣 Ua4k̝8[tJytU=M42IT_ HxUUuV8|3u=̨ᢷwü65/ReyU>V7&C59@2y6gԫ yu䚨9gH<'}b|plX`AqT(ΐyig~U2v_wv ۏɪ/9Efb* ۨ9}[ޑ.\};#卻nSrnj7ń%|gnR(=ŎSwg^)J&J/S ׭O_|NK0MRF~j'7Z*?叻wmz$N T_?-;m.2}xOfU Be8(yz5,oySlHBCe)0|E0pNcS:g8}j\{Ry R.Xyd9Bpse{XXZΩ N=q{5;rvsUz*mcx+RyHzTIQ@L9qm={'/;-ܩpj4_?{S)c5{RГ 90w@R v;TA*Ď cep}BMM?&D=L@- UKj5II&B@e2ҳv=<\ɜ3g ="s-=sNHo"!3s{HcL/wpzUTuișAiK4KX6ܞ4c ϧX7 `u0t*ђF+2엮u/Gf2:7gƺu]2o"sblr|Wu{'=e RVpgk-| &ԟe<&zD7;PVy{:&gJ'iθ9I%cڇZgY$AiIt& ]?ػcm8E)0r,%-#]eoyP9Y1߂3M}e[z|*q߱ŕa?֝8ͻz],g}yHgcv;ga)aS(eZSm P{Z [-˧/>'%aY#iG@FKFlJ;=$*i>O϶K:Ճ)پBuPw <۩UtȊtԫ\ ^0t#c/5wNig1 MRdp(jGtVCH RE7Ů? 2_~2*Ӣk~V2-|i՝JOP^ b!hH|.Mz5=ŏU8È+š%q[.p2oQDQ˧ر]i\Uj<<bNpO|׾Ng6jZ7Jt5FaDQ!;Q s;܌#i2Ys ^x_w"'(e~".6S) /mկe~jsGzEU::SA_j:H:~5'}˞iirk/2i1h|t9yMqeP pR)ԉjt\d)_nSu[Y8 \ײؖR񼎳LϦ<(e)=&&'Ɣ0yP(n6 ycakm7"x;QSV=8;aUglcg}Vs%2"v!u<(SEQ?k.YvF.5T;E)aUR2Zwʪ;EN_aܙMe{X<7$cud9Uu|J0!mO^Ze8_2SAj:H_~lZ+]fj-5ٷ_P`鳧lSZz[,:T Xĕו:ޞMiӦ{!LzZ-3[+ؕMOhٳ<ԏeka\as59ls`ϞlgIsShYqFYٙ(^GFTUsM]ʴZW)6—iyP9rFwt\+zJ ׳UAeu0xYR9p뱿DQeq'"={7JΜsi\y=ʴVͷ7Hņq1ӣ۞ܜH&ee{`O`/[}'rgOpņ*JѰۨLσju8;Nzw ZnY'fd|GJs3j:"Ng{^\'-Czfվۦ&ΌKc^d tW$@@F҃"sCVLdd 5=_!&HeSLIpy%HGhM-Dzqˍ?r_v<yIDATSlhد(;sLks7D9{tzQ؟js2mNBعtU6@Cd5umGF03[jLN1USwK8r+('JxrH I{xz;{}c/*k?RݝC}{e N>|f]ʦ#wed9ѬcSϨeZWzrcH:$ g)5~ ,$5֯(_`j\y=ʴ4LON}֨e9QyWO\#[.В3DI,ثڐ\;Qsnt?ɚ Rg}WmuL̓*/N9s%fYM sma lSJ+s[ {Vv9T2;$XyNkY^Q˴z]{b*-8N1;\n aVbwn|>7̰75LAģlS_[+ғe; Pc#H}aRKji0+jwqӇQuN5?s/W.S-LDJOvrkM{Xs /-iys-J@MֽUOPzatddu`v+%QD=lvd\㪝ׅQ MNHe᳾ 5-BF;;˹/`}:UXz?om?6GerX!M{OWI}*tf1)zek8SyE7K-h)-OCls:]w1A[Sp}IQ4r QJW-|(6!blT[/ <ʞTtaD|dFwser_m<\Xq>d#?<UF{Ԑzyy6KwIW?hZu'dcE:q՝Z.S[su6mS܂~=N~w7.F7{0CLJIc̅IåǨI\,*̹AgWѽeԷju:JsߩvTs{4J Y_ywZL#םqb}@ٞT@hh [;JbzR\?^U fT(׬=M{ PP!ѓ 'bG #H@RD#բ}fE*h {L%ֆ0ְa K4l=f.%-:6d^^nzTD&3*=kZ3LdNșKZΞO HQI"HT)wӵ֓~Z.4=>=5gTC= PÕ-VR{Usw\͞T4ЀNjE 瓯"'2RQ򭦯X`LwݷF6ٿ%u@:u|i,{ODOvsSsR)U%-ҽS٫%Jz(S(r&]:=EIov_+MsݻhiQ0_^>}9xF/™s=?vdWvu]2VKf3ۓFH 7CgQHtu[vSe 1'jԭMXzXu뺤( 8ΤZl:.QmV\ yf{R99{AdItɦ'RKgMz9lϣ~Y3ZT@auҵIߡ߂+ӫȾ*!JzOOoE&nծq_ zR\s'jIN˜2QyF# jǩY bF['=gBca]Sxgd, !Jk0/̩TAz]EsS< л'x>SI9<Ɋ cǷGP|+M"fkRnOIsUE=.E=+u9+5[srWs{_G_I)E=z}=WsJ%aWFJAb(j1Huoi]hA͇3уTʧuަ o3q-_SmBWfdNٴvꗑIkξj36'=9`zD6?)'ʬgjSug Y3ܟ_&ӥ'Y+]^5-B`"̹#I |2Q]%zzY =~cnf8^6Eɷ5=@'5kzR` H@R v;TA*Ď bG #H@R v;TA*Ď bG #H@R v;T`fDB%Kn{i ~-w^%LH#-bCRx iSd`{4/QzN/lG cLR*Ng$<~Rגc)~>9K糎4@:Y=Ӣ_:3{on|Le7= Xɞ>|sXzFdRo2x( QCDsa I5-}y<[& 1I5kP[A3/ryOj-$SJD "uwz9yl3,'Bc2wܸCOYo(W,k1yˎM%c{SѴ\nքViSOoU25Iϫ/IWpdpK^ ˣBk*܄ަ^}y]WV^,o0"֏(KZze@0TvomԨ~}s7禣L[eGҳ\(2=(Z6FiHgZo5[Oz켸 C-+RX@P@ KrCe_"7@|詍v%yct^DJ7Z ǧzu_~BJ2x TP|q [v$wήB}?#IVog;e~Ge< ǿO_(?>-M ?$/^{KEְ_8Til&+VvKZ1B^6J΍W-E)M'Ȼkډ]q]*;e˥r9l}J̝'+9aWgj}O[y чn ([cSᢍCme;y }{Xzׯ+Pxnߝ:*c%e_ wܦO[ P.XKƿl\\=\MȄ=%d|YcC Mr8%S]tt:#q'_:˗ȅK'{ePZ5U{8@}[V FzWrc=o@,6cS! x^yUQz:稤vwug-0!.*4A!Gڋ%D0avc.ԁKףW/B.&W`Y.,F\+&۪g{rNY۽oߟsΛsn,lg̙8RLUhhwi) kX u5^ےښm1R\:=^鉃9 uTwGr_e1fV?{*>yt}~0YU_k9SoُqB+q5 G@>:=uV~GuVSvЋ'Ⓟ3q塕*¹dpT:sc[WB冰kCnZ8:]偝1\,B -ԃF@> 1DOtdczC?鈎YV1U{5_h庱xou-ZK1T&2+h=vѧ*4$ց迷ԯ1PԽVFMR &M|%PZמ7jVc{ywuĞc]u4mс#/JX߾%s-h}}_> ^Z9kÙ89LJY"R7bZ;<Ҏ6ݿlpn&aVɛPu[mGJ%W+T{GcB2YO Iȴx8Gf+aTc⯻dipc88WhɉZ@yB*`}ے84AyT;{s|9yMY1"NW^}$rB*sܹرcGd2ʽ<&'Rq@D˱X_WLĮT+nFL2Y^1UB*300Ne\7.=;qU 'Pܳ)jQjmDys@H܆jU[<{rۿ1ћlTmgɨ7o_nW;نGB*sȑ`|/o\1.$bDo&q%]3*T%c obr/Ͽ|RLւO㡑ѽ4m~'ĕ(U+^/bZTו 1uorTJkvqq2\Y_lJ{'P}k4771גX # # SPDX-License-Identifier: CC0-1.0 # # This file is part of the program "Hyperorg". Unlike the program, this file is # licensed under Creative Commons Zero v1 (CC0-1.0). See folder LICENSES or go # to hyperorg/pyproject.toml000066400000000000000000000046531457624743500156620ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2023 Christian BUHTZ # # SPDX-License-Identifier: GPL-3.0-only # # This file is part of the program "Hyperorg" which is released under GNU # General Public License v3 (GPLv3). # See folder LICENSES or go to . [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [project] name = "hyperorg" version = "0.1.0" description = """ Converts Org-mode and Org-roam files into HTML for serverless local use.""" readme = "README.md" requires-python = ">=3.8" keywords = ["emacs", "org", "orgmode", "orgroam", "html", "css", "zettelkasten"] license = {file = "LICENSES/GPL-3.0-only.txt"} classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Programming Language :: Python :: 3.8", "Environment :: Console", "Intended Audience :: Education", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "Intended Audience :: Other Audience", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Education", "Topic :: Office/Business", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Sociology", "Topic :: Text Editors :: Emacs", "Topic :: Text Processing :: Markup", "Topic :: Text Processing :: Markup :: HTML", "Topic :: Utilities" ] authors = [ {name = "Christian Buhtz", email = "c.buhtz@posteo.jp"} ] dependencies = [ "orgparse", "python-dateutil", "tomli; python_version<'3.11'", "python-dateutil", ] [project.optional-dependencies] develop = [ "pytest", # Not mandatory "pyfakefs", # Fake filesystems for unit testing "pycodestyle", # PEP8 check (via unittest) "ruff", # PEP8 and some more checks (via unittest) "pygments", # Some unittests not working without "codespell", # common typos "reuse", # REUSE specification ] [project.urls] homepage = "https://codeberg.org/buhtz/hyperorg" repository = "https://codeberg.org/buhtz/hyperorg" changelog = "https://codeberg.org/buhtz/hyperorg/src/CHANGELOG.md" [tool.setuptools.package-dir] hyperorg = "src/hyperorg" [project.scripts] hyperorg = "hyperorg.__main__:main" hyperorg/src/000077500000000000000000000000001457624743500135255ustar00rootroot00000000000000hyperorg/src/hyperorg/000077500000000000000000000000001457624743500153645ustar00rootroot00000000000000hyperorg/src/hyperorg/__init__.py000066400000000000000000000070721457624743500175030ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2023 Christian BUHTZ # # SPDX-License-Identifier: GPL-3.0-only # # This file is part of the program "Hyperorg" which is released under GNU # General Public License v3 (GPLv3). # See folder LICENSES or go to . """The Hyperorg package. See README.md and LICENSE for further details. """ import pathlib import os import re from collections import Counter from importlib.metadata import metadata def _package_metadata_as_dict(exclude_keys: list = ('Classifier'), license_lines: int = 2, ) -> dict: """Get package metadata and return it as a dict.""" m = metadata(__name__) result = {} for key, count in Counter(m.keys()).items(): if key in exclude_keys: continue # single value key if count == 1: result[key] = m[key] continue # multi value key result[key] = [] for val in m.get_all(key): result[key].append(val) # postprocess project URLs urls = result['Project-URL'] result['Project-URL'] = {} for entry in urls: key, val = entry.split(',') result['Project-URL'][key.strip()] = val.strip() # post process license data result['License'] = result['License'].split('\n')[:license_lines] return result meta = _package_metadata_as_dict() __version__ = meta['Version'] def get_full_application_string(name_as_url: bool = False) -> str: """Build a string representing the application name and its version. If a git repo is present information about its current state are added also. """ if name_as_url: url = meta['Project-URL']['homepage'] name = f'{__name__}' else: name = __name__ return f'{name} {__version__}{get_git_string()}' def get_git_string() -> str: """Build a string containing the applications git related information.""" try: git = get_git_repository_info() return f'(branch: {git["branch"]} at {git["hash"]})' except FileNotFoundError: return '' def get_git_repository_info(hash_length: int = 8) -> dict: """Return the current branch and last commit hash. A special case is when that script runs on a Read The Docs instance. In that case the branch name is extracted from environment variables. Credits: https://stackoverflow.com/a/51224861/4865723 """ git_folder = pathlib.Path.cwd() / '.git' result = {} # If run on a Read The Docs instance if 'READTHEDOCS' in os.environ: if os.environ['READTHEDOCS_VERSION_TYPE'] == 'branch': result['branch'] = os.environ['READTHEDOCS_VERSION'] else: # branch name with (git_folder / 'HEAD').open('r') as handle: result['branch'] = '/'.join(handle.read().split('/')[2:]).strip() # commit hash with (git_folder / 'refs' / 'heads' / result['branch']) \ .open('r') as handle: result['hash'] = handle.read().strip() if hash_length is not None: result['hash'] = result['hash'][:hash_length] return result _RE_SIMPLIFY_STRING = re.compile(r'[^\w-]') """Used in `simplify_string()`.""" def simplify_string(string: str) -> str: """Simplify the string. While word characters and `-` are kept the rest is replaced by underscore `_`. This can be used for filenames. Args: string: The string to simplify. Returns: The simplified string. """ return _RE_SIMPLIFY_STRING.sub('_', string) hyperorg/src/hyperorg/__main__.py000066400000000000000000000116661457624743500174700ustar00rootroot00000000000000#!/usr/bin/env python3 # SPDX-FileCopyrightText: © 2023 Christian BUHTZ # # SPDX-License-Identifier: GPL-3.0-only # # This file is part of the program "Hyperorg" which is released under GNU # General Public License v3 (GPLv3). # See folder LICENSES or go to . """Entry point for the package""" import sys import pathlib import logging import locale import argparse import webbrowser import hyperorg from hyperorg.exporter import Exporter from hyperorg.reader import Reader _log = logging.getLogger(f'hyperog.{__name__}') def logo_with_version(): """Build ASCII-Art application name with version and git info. The use font name is 'Straight'.""" ver = hyperorg.__version__ git = hyperorg.get_git_string() logo = [ r' |__| _ _ _ _ _ _', rf' | | \/ |_) (- | (_) | (_) Version: {ver}', rf' / | _/ {git}' ] return '\n'.join(logo) def _extract_copyright() -> str: """Extract the SPDX copyright info from current file.""" with pathlib.Path(__file__).open('r', encoding='utf-8') as handle: for line in handle: if 'SPDX-FileCopyrightText' in line: return line[line.index(':')+1:].strip() return '(ERROR unknown copyright)' def init_argparse(): """Define and parse commandline arguments. Defines the commandline arguments and options and parse the command line arguments. Returns: argparse.Namespaces: The parsed arguments in a dict like object. """ app_full = hyperorg.get_full_application_string() author = hyperorg.meta['Author-email'] prjurl = hyperorg.meta['Project-URL']['homepage'] changelog = hyperorg.meta['Project-URL']['changelog'] gpl = ' '.join(line.strip() for line in hyperorg.meta['License']) copy = _extract_copyright() epilog = f'{logo_with_version()}\n\n Author : {author}' \ f'\n Project : {prjurl}\nChangelog : {changelog}\n' \ f' License : {gpl}\nCopyright : {copy}' parser = argparse.ArgumentParser( prog=app_full.split(' ')[0], description=hyperorg.meta['Summary'], epilog=epilog, formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('inputdir', type=pathlib.Path, help='Source directory with org-files.') parser.add_argument('outputdir', type=pathlib.Path, help='Destination directory for html-files.') parser.add_argument('--hardlinks', action='store_true', help='Use hardlinks instead of symlinks for images ' 'and other attachments in the input directory.') parser.add_argument('-s', '--show', action='store_true', help='Open result in default browser.') parser.add_argument('-v', '--verbose', action='store_true', help='Give detailed information.') parser.add_argument('-d', '--debug', action='store_true', help='Debug output.') parser.add_argument('--version', action='version', version=app_full) return parser.parse_args() def init_logging(info: bool, debug: bool): """Configure logging. Parameters: info (bool): Set 'True' for a more verbose logging. debug (bool): Set 'True' for full/debug logging. """ # default log format # see: https://stackoverflow.com/q/7771912/4865723 log_format = '%(levelname)s : %(message)s' # log level if debug: log_level = logging.DEBUG log_format = '[%(asctime)s] [%(levelname)s] ' \ '%(filename)s:%(funcName)s() => %(message)s' elif info: log_level = logging.INFO else: log_level = logging.WARNING # python default logging.basicConfig(level=log_level, format=log_format) def main(): """Main entry point""" # The interaction between locale and datetime seems a bit wired on the # first sight. As stated in the docu datetime use locale but the local # package not the OS locale. # The locale package itself doesn't use the OS locale for datetime things # by default (because of unittests). # This need to be activated explicit. locale.setlocale(locale.LC_TIME, '') args = init_argparse() init_logging(info=args.verbose, debug=args.debug) try: _log.debug(hyperorg.get_git_repository_info()) except FileNotFoundError: pass reader = Reader(args.inputdir) node_count = reader.read_org_files() if node_count == 0: _log.warning('No nodes read. Stop here.') sys.exit(1) exporter = Exporter(args.outputdir, use_hardlinks=args.hardlinks) exporter.export_to_html() # Final message fp_indexhtml = pathlib.Path(args.outputdir) / 'index.html' if args.show: webbrowser.open_new_tab(str(fp_indexhtml)) else: _log.info(f'To see the result open {fp_indexhtml}.') if __name__ == '__main__': main() hyperorg/src/hyperorg/content.py000066400000000000000000000411161457624743500174130ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2023 Christian BUHTZ # # SPDX-License-Identifier: GPL-3.0-only # # This file is part of the program "Hyperorg" which is released under GNU # General Public License v3 (GPLv3). # See folder LICENSES or go to . """The content module offering the Node class.""" from __future__ import annotations import logging import re import pathlib from collections import defaultdict import shlex from typing import Hashable, Iterable, Dict import dateutil.parser # python-dateutil from hyperorg import elements from hyperorg import textruns from hyperorg import parser _log = logging.getLogger(__name__) class Node: """Org-file body in a generalized and strcutured format. The body of an org-file with all its elements (paragraphs, lists, ...) and their structure is represented by an object of that clase. On base of one of its objects it can easiely transformed to another file-format (e.g. HTML). An object Content is the input for the Exporter class. The Org syntax reference can be found at https://orgmode.org/worg/dev/org-syntax.html. It is not an official document but there is not a more official. I was pointed to that document asking on the mailinglist. Development note: A node (with "d") in OrgRoam is an entity with an ID. A note (with "t") is something more broad. Escaping HTML entities (e.g. "&" into "&") is done here in that class (see Issue #47). This is not a clear solution because HTML related operations should be done in `Exporter`. The `Content` class should be independent from output format. In consequence the current solution is not for ever and will be refactored once. """ input_dir: pathlib.Path """Input directory where the Org-roam content was read from""" nodes: dict = {} """List of all Org-roam nodes in `Content` format""" heading_ids: dict = {} """Org-IDs of headings (subtrees)""" IDX_ALL = 'all' IDX_FILETAG = 'filetag' IDX_NO_TAG = '(no tags)' index_data: dict = { IDX_ALL: defaultdict(list), IDX_FILETAG: defaultdict(lambda: defaultdict(list)) } """Nested dictionary containing glossary like data.""" META_FIELDS_INHERIT = ['date', 'filetags'] """See `inherit_meta_fields_from()`. """ # RegEx to find headings # one or more asterix bullets followed by a blank _RE_PATTERN_HEADING = re.compile(r'^\*+ ') # RegEx to find ordered list items # one or more digits followed by . or ) followed with blank _RE_PATTERN_ORDERED_LIST = re.compile(r'^\s*\d+[\.|)]\s+') # RegEx to find unordered list items # zero or more blanks followed by followed by bullets (+-*), # followed by blank. # Attention: Check for heading pattern first! _RE_PATTERN_UNORDERED_LIST = re.compile(r'^\s*[\+\-\*]\s+') _RE_PATTERN_BLOCK_BEGIN = re.compile(r'^#\+(?i:begin)_(\S+)(?:\s(.+))?') _RE_PATTERN_BLOCK_END = r'^#\+(?i:end)_(?i:{})' _RE_PATTERN_LINK_LIKE = re.compile(r'^\[\[.*\]\]$') __slots__ = ( '_orgids_linking', 'filename', '_meta', # '_previous_properties', # '_remembered_paragraph_lines', # '_remembered_list_lines', 'title_org', 'title_runs', '_elements', # '_e', ) def __init__(self, body_lines: Iterable[str], filename: str, title_aliases: Iterable[str] = None, anchor_first_heading: bool = False): """Content ctor.""" if title_aliases is None: title_aliases = [] # self._orgids_linking = [] # Name of the original Org-roam file self.filename = filename # Node title self.title_org = '[no title]' self.title_runs = textruns.cut_into_runs(self.title_org) # Meta data about that Org-roam node self._meta = {} # # Parsed property lines (beginning with '#+') # self._previous_properties = [] # # Paragraph lines to join later into one # self._remembered_paragraph_lines = [] # # List item lines to join later into one # self._remembered_list_lines = [] # Title aliases (from :ROAM_ALIASES) if title_aliases: self._meta['aliases_org'] = [] self._meta['aliases_runs'] = [] for one_alias in title_aliases: self._meta['aliases_org'].append(one_alias) self._meta['aliases_runs'].append( textruns.cut_into_runs(one_alias)) # list of content elements self._elements = [] # # the current active element # self._e = None self.parse_lines(body_lines) # When this Content instance represent a subheading with its own OrgID if anchor_first_heading: if not isinstance(self._elements[0], elements.Heading): raise TypeError('First element need to be a heading.') self._elements[0].generate_anchor() def _on_link_created(self, linkrun): if linkrun.kind == textruns.Link.Kind.ORGID: self._orgids_linking.append(linkrun.target) def __getitem__(self, idx: int): return self._elements[idx] def __len__(self): return len(self._elements) def _parse_meta_lines(self, to_parse: list) -> list: """Parse from the beginning lines starting with '#+'. Only lines in the beginning of `to_parse` are parsed until a none '#+' line appears. The consequence is that later lines beginning with '#+' (e.g. '#+src_beginn') are irgnored here. Returns: The list without the parsed lines. """ for idx, line in enumerate(to_parse): try: name = re.findall(r'#\+(.+?): ', line)[0] self._meta[name] = line.replace(f'#+{name}: ', '') if name in ['filetags']: # shlex: see https://stackoverflow.com/a/79985/4865723 # re is a bit faster but harder to read. # see: https://stackoverflow.com/q/79968/4865723 tags = shlex.split(self._meta[name]) self._meta[name] = tags # next line continue if 'date' in name or 'time' in name: try: # remove brackets for (in)active dates val = self._meta[name].strip('<>[]') # parse self._meta[name] = dateutil.parser.parse(val) except dateutil.parser.ParserError: pass continue except IndexError: # rest of the lines return to_parse[idx:] # There are two cases when this happen. # 1. 'to_parse' was empty and the loop never iterates # 2. 'to_parse' iterated to the end without any IndexError. It means # that all lines are property lines. return [] def add_subheading_content(self, content): """Add child node to this one. """ content.inherit_meta_fields_from(self) # Content element, subtree/heading with own OrgId # self._finish_current_element() # self._e = content self._elements.append(content) # self._finish_current_element() def inherit_meta_fields_from(self, content: Node): """Some meta fields from ``content`` are copied into the own. Which fields are copied is specified by `META_FIELDS_INHERIT'. Args: content: The content object to copy from. """ for field in Node.META_FIELDS_INHERIT: try: val = content._meta[field] # pylint: disable=protected-access except KeyError: pass else: own_val = self._meta.get(field, None) # just copy if own_val is None: self._meta[field] = val continue if not isinstance(own_val, list): own_val = [own_val] if not isinstance(val, list): val = [val] self._meta[field].extend(val) @classmethod def add_all_backlinks(cls, backlinks: Dict[Hashable, Dict[Hashable, Iterable]]): """Create backlinks elements for all nodes. """ for orgid in backlinks: if orgid in cls.nodes: cls.nodes[orgid].add_backlinks(backlinks[orgid]) def add_backlinks(self, backlinks: Dict[Hashable, Iterable]): """Create backlink element for that node.""" # self._finish_current_element() for orgid in backlinks: # create org-file line item_string = f'- [[id:{orgid}][{Node.nodes[orgid].title_org}]]' # create backlink element if needed if (not self._elements or not isinstance(self._elements[-1], elements.Backlinks)): self._elements.append( elements.Backlinks(item=item_string)) else: self._elements[-1].add_item(item_string) # subheadings? for subheading_id in backlinks[orgid]: idx = Node.heading_ids[subheading_id][1] subheading_title = str(Node.nodes[orgid][idx][0]) # create org-file line item_string = f' - [[id:{subheading_id}][{subheading_title}]]' self._elements[-1].add_item(item_string) # self._finish_current_element() def parse_lines(self, to_parse: list): """Create Content elements based on text lines. Classify a list of lines and create Content elements (e.g. Content.Paragraph) of it. To differentiate a list item from a head when an asterix is used as bullet it is important to check for heading first. """ textruns.Link.event_link_created.register(self._on_link_created) to_parse = self._parse_meta_lines(to_parse) # the title was identified from a meta field first try: # Title as runs is used in the

element. # But when handling (back)links it might be easier using the # original org-syntax not parsed into runs. self.title_org = self._meta.pop('title') self.title_runs = textruns.cut_into_runs(self.title_org) except KeyError: pass self._elements.extend(parser.OrgContentParser(to_parse).result()) textruns.Link.event_link_created.deregister(self._on_link_created) # def _is_list_line(self, raw_line: str) -> bool: # """Does ``raw_line`` belongs to the current list or if there is a # list. # """ # # If there are no previous lines then this one can no belong to one. # if not self._remembered_list_lines: # return False # content = raw_line.lstrip() # line = ( # None, # no bullet # len(raw_line) - len(content), # content # ) # content_start_indices = [ # val[1] for val in self._remembered_list_lines] # if any([line[1] >= val for val in content_start_indices]): # self._remembered_list_lines.append(line) # return True # return False # def _finish_current_element(self): # # Paragraph lines? # if self._remembered_paragraph_lines: # self._e = elements.Paragraph(self._remembered_paragraph_lines) # self._remembered_paragraph_lines = [] # # list # if self._remembered_list_lines: # lines = Node._join_list_lines_to_item_lines( # self._remembered_list_lines) # self._remembered_list_lines = [] # self._e = elements.ListElement( # item=lines[0][1], ordered=lines[0][0]) # for line in lines[1:]: # self._e.add_item(line[1], ordered=line[0]) # # Element available # if self._e is not None: # # store that element # self._elements.append(self._e) # self._e = None # if self._previous_properties: # self._elements.append( # elements.Paragraph(self._previous_properties)) # self._previous_properties = [] # def _add_possible_figure(self, line): # """Try to add the line as a figure otherwise as a link. # """ # img_or_link = textruns.Image.to_image_or_linked_image(line[2:-2]) # if img_or_link: # if self._previous_properties: # props = elements.Figure.to_properties_dict( # self._previous_properties) # self._previous_properties = [] # else: # props = {} # self._e = elements.Figure(img_or_link, props) # else: # self._remember_paragraph_line(line) # def _add_heading(self, line): # """ # """ # self._finish_current_element() # self._e = elements.Heading(line) # @staticmethod # def _join_list_lines_to_item_lines(lines: list[tuple]) -> list: # """Remove line breaks and indentation links from list lines. # Args: # lines: A list of 4 item tuples. # Each tuple in ``lines`` consists of four items. The first is the # index of the beginning of the bullet character (includes numbers). # The seocnd item is the beginning index of the content after the # bullet. The third is the content itself. The fourth indicates if it # is an ordered or unordered list. # See also `Node._classify_list_line()`. # .. Example :: # - items # with new line # - next # Becomes .. Example :: # - items with new line # - next # Without this step it is not possible to correctly parse for runs. # """ # result = [] # for one_line in lines: # # has bullet (item start) # if one_line[0] is not None: # result.append( # ( # # (un)ordered # one_line[3], # # string incl. bullet # '{blank}{bullet} {content}'.format( # blank=' ' * one_line[0], # bullet='1.' if one_line[3] else '-', # content=one_line[2] # ) # ) # ) # else: # # refresh last item # result[-1] = ( # # (un)ordered # result[-1][0], # '{} {}'.format(result[-1][1], one_line[2]) # ) # return result # @staticmethod # def _classify_list_line(raw_line: str, ordered: bool) -> tuple: # """Convert that list line into its components and return them as a # 4-item tuple. The content of the items as follows. # 1. Index of the beginning of the bullet character. ``None`` if no # bullet is present. # 2. Index of the beginning of the content. # 3. The content (excluding leading space and bullet). # 4. Value of ``ordered``. # """ # bullet = raw_line.split()[0] # bullet_start_idx = raw_line.index(bullet) # content_start_idx = bullet_start_idx + len(bullet) + 1 # return [ # bullet_start_idx, # content_start_idx, # raw_line[content_start_idx:].lstrip(), # ordered # ] # def _add_list_item(self, raw_line: str, ordered: bool): # """Lines with a bullet in front. # """ # # begin of a list? # if not self._remembered_list_lines: # self._finish_current_element() # line = Node._classify_list_line(raw_line, ordered) # self._remembered_list_lines.append(line) # def _remember_paragraph_line(self, line): # """ # """ # if not self._remembered_paragraph_lines: # self._finish_current_element() # self._remembered_paragraph_lines.append(line) def get_orgid_links(self) -> list: """Give a list of OrgID's this content instance is linking to. Be aware that subheading content objects are ignored. Returns: List of OrgID's """ return self._orgids_linking hyperorg/src/hyperorg/elements.py000066400000000000000000000237411457624743500175610ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2023 Christian BUHTZ # # SPDX-License-Identifier: GPL-3.0-only # # This file is part of the program "Hyperorg" which is released under GNU # General Public License v3 (GPLv3). # See folder LICENSES or go to . """Collection of elements of on org file. In the concept of Hyperorg an element is a vertical run. See module `textruns` for further details. """ from __future__ import annotations import logging import uuid import re from typing import Union from dataclasses import dataclass from hyperorg import textruns _log = logging.getLogger(__name__) class Paragraph(textruns.Run): # pylint: disable=too-few-public-methods """A paragraph element.""" def __init__(self, lines: list = None): runs = textruns.cut_into_runs(' '.join(lines)) if lines else [] super().__init__(runs=runs) @dataclass(init=False) class Heading(textruns.Run): """A heading element.""" __slots__ = ('level', 'anchor', ) level: int anchor: str def __init__(self, label: str): stripped_label = label.lstrip() # lstrip from the second blank stripped_label = stripped_label[stripped_label.index(' '):].lstrip() runs = textruns.cut_into_runs(stripped_label) super().__init__(runs=runs) self.anchor = None """See `generate_anchor()` for details.""" # enum symbol idx_stop = len(label) - len(stripped_label) heading_symbol = label[:idx_stop].strip() # The title of a node is the first heading level. Because of # that the first content heading (one * bullet) is the second # level. """Heading level.""" self.level = len(heading_symbol) + 1 def generate_anchor(self): """Set a random and unique anchor string for that heading. That anchor is later use to link to that heading (e.g. ``Heading``. """ self.anchor = str(uuid.uuid4()) @dataclass(init=False) class ListElement(textruns.Run): """A list element including its entries. The list items are represented by `self.runs` (from `textruns.Run`). Each list item is of type `list()` even if it is only a `str`. One exception is a sub-list. """ __slots__ = ('ordered', '_indention', ) ordered: bool _indention: int def __init__(self, item: str, ordered: bool, indentation: int = None): super().__init__(runs=[]) self.ordered = ordered """Switch for ordered list.""" self._indention = indentation """Indentation. From begin of a line until its bullet.""" self.add_item(item) def add_item(self, item: str): """Add list entry. Args: item: The complete line in org content incl. indentation whitespaces and item marker (e.g. bullet). ordered: Unordered or ordered (numbered) list. """ if isinstance(item, list): self.add_item(item[0]) for item_part in item[1:]: self.append_to_last_item(item_part) return # split at whitespaces item_parts = item.split() # position of bullet indentation = item.index(item_parts[0]) # set indentation if not set yet if self._indention is None: # Indentation (to bullet). self._indention = indentation # higher indentation? if indentation > self._indention: # Active child? if isinstance(self[-1], ListElement): # add to existing child self[-1].add_item(item) else: # Check if item is (un)ordered and set this explicit child_ordered = item_parts[0][0].isdigit() # create new child list child = ListElement(item, child_ordered, indentation) self.runs.append(child) else: try: # lstrip from the second blank item_content = item[item.index(item_parts[1]):] except IndexError: # item without content e.g. "- " item_content = '' runs = textruns.cut_into_runs(item_content) self.runs.append(runs) def belongs_to_me(self, line: str) -> bool: """Checks if the indentation of 'line' belongs to this list element. """ try: return self.runs[-1].belongs_to_me(line) except AttributeError: pass # count whitespace blank_count = len(line) - len(line.lstrip()) # calculate content indentation if self.ordered: # e.g. '1. content' content_indention = 3 + self._indention else: # e.g. '- content' content_indention = 2 + self._indention if content_indention == blank_count: return True return False def append_to_last_item(self, line: str): """Append the 'line' to the last list item. """ if isinstance(self[-1], type(self)): # a child list self.runs[-1].append_to_last_item(line) elif isinstance(line, Block): # a block element self.runs[-1].append(line) else: blank = '' if isinstance(self.runs[-1][-1], Block) else ' ' new_runs = textruns.cut_into_runs(blank + line.lstrip()) self.runs[-1].extend(new_runs) @dataclass(init=False) class Block(textruns.Run): """Org syntax reference does name this a block. It does start with a line beginning with ``#+begin_NAME`` and end with a line ``#+end_NAME`` where ``NAME`` can be ``src``, ``example`` or something else. In org not all blocks are verbatim (e.g. ``begin_quote``). But this block here is meant to be verbatim. Open an issue please if you need support for other blocks. """ __slots__ = ('name', 'language') name: str language: str def __init__(self, name: str, content_lines: list[str], language: str = ''): super().__init__(runs=content_lines) self.language = language self.name = name @dataclass(init=False) class Figure(textruns.Run): """A Figure contain an image and optionally a link.""" _REX_PROPERTY = re.compile(r'^#\+(.*?): *(.*)$') _REX_LABEL_TARGET = re.compile(r'^\[\[((?:(?!]\[).)*)(?:]\[(.*))?]]$') """Credits: https://stackoverflow.com/a/75996345/4865723""" __slots__ = ('attributes', 'unknown') attributes: dict[str, str] unknown: list[str] @staticmethod def to_properties_dict(lines: list) -> dict: """Extract information from lines beginning with `#+CAPTION`, `#+ATTR_HTML` or other org properties. Empty lines not allowed. Args: dash_plus: List of lines taken from an org file. Raises: ValueError: If lines are empty. Returns: A dictionary containing the extracted information. """ result = {'attr': {}, 'caption': None, 'unknown': []} try: properties = [ Figure._REX_PROPERTY.match(line).groups() for line in lines] except AttributeError as exc: raise ValueError('Not all lines are org property lines!\n' '{}'.format('\n'.join(lines))) from exc for prop, val in properties: prop = prop.lower() # Caption if prop == 'caption': result['caption'] = val continue # Attributes if prop == 'attr_html': attr = ' ' + val attr = attr.split(' :')[1:] # first element is always empty for a in attr: k, v = a.split(' ', 1) result['attr'][k] = v continue # Something else result['unknown'].append(f'#+{prop.upper()}: {val}') return result def __init__(self, image_or_link: Union(textruns.Image, textruns.Link), properties: dict = None): """ The `line` is the full line (e.g. "[[image.png]]") from the org file without modification. The `properties` are all previous lines in the org file beginning (case insensitive) with `#+CAPTION` or `#+ATTR_HTML` and other org properties. """ if properties is None: properties = {} runs = [image_or_link] # Attributes self.attributes = properties.get('attr', {}) # Unknown org properties (e.g. '#+DOWNLOAD: xxx') self.unknown = properties.get('unknown', None) # Caption caption = properties.get('caption', None) if caption: caption = textruns.cut_into_runs(caption) runs.append(caption) super().__init__(runs=runs) self.attributes = self.image.set_attributes(self.attributes) @property def link(self) -> bool: """Return the link instance of that figure otherwise `None`.""" if isinstance(self[0], textruns.Link): return self[0] return None @property def image(self) -> textruns.Image: """Return the image instance of the figure.""" try: # first run of the Link if its label (the image) return self.link[0] except TypeError: # It is not a link so it must be an Image return self[0] @property def caption(self) -> list[textruns.RunsType]: """The figures caption as a list of runs. Returns: A list of runs. """ try: return self[1] except IndexError: return None class Backlinks(ListElement): """The backlinks of a node. Most of the time the backlinks are located in a section at the end of a node. """ def __init__(self, item: str): super().__init__(item=item, ordered=False) hyperorg/src/hyperorg/event.py000066400000000000000000000031121457624743500170540ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2023 Christian BUHTZ # # SPDX-License-Identifier: GPL-3.0-only # # This file is part of the program "Hyperorg" which is released under GNU # General Public License v3 (GPLv3). # See folder LICENSES or go to . """Observer-Pattern like implementation. Regarding that pattern an object of type `Event` is the "Subject" and every object calling `Event.register()` is an "Observer". See https://stackoverflow.com/a/48339861/4865723 """ class Event: """Instance of this event (aka subject) can notify its observers.""" def __init__(self): self._callbacks = [] def notify(self, *args, **kwargs): """Notify registered observers. The args, and kwargs are given to the observers. """ for callback in self._callbacks: callback(*args, **kwargs) def register(self, callback): """Register an observer (callback function). """ if callback not in self._callbacks: self._callbacks.append(callback) return callback def deregister(self, callback): """Deregister / remove an observer (callback function). """ self._callbacks.remove(callback) # @contextmanager # def keep_silent(self): # """A context manager function to suppress notification's of the # observers.""" # try: # self._silent_callbacks = self._callbacks # self._callbacks = [] # yield # finally: # self._callbacks = self._silent_callbacks hyperorg/src/hyperorg/exporter.py000066400000000000000000001124041457624743500176100ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2023 Christian BUHTZ # # SPDX-License-Identifier: GPL-3.0-only # # This file is part of the program "Hyperorg" which is released under GNU # General Public License v3 (GPLv3). # See folder LICENSES or go to . # pylint: disable=too-many-lines """Exporting runs and elements to HTML""" import sys import logging import pathlib import datetime import locale import os import copy from collections import Counter from typing import Union, Any, Dict, Iterable from html import escape as html_escape import hyperorg from hyperorg.content import Node from hyperorg import elements from hyperorg import textruns try: import pygments import pygments.lexers import pygments.formatters except ImportError: pygments = False _log = logging.getLogger(__name__) _TAB = ' ' * 4 _MSG_INSECURE_HTTP = 'For security reasons, HTTP URLs are not supported. ' \ 'Find an alternative HTTPS source.' class Exporter: # pylint: disable=too-few-public-methods """Class handling exporting runs and elements to HTML. Dev node: Consider to refactor and move most of it into module level. See Issue #111. """ _PLACEHOLDER_TITLE = '{{ title }}' _PLACEHOLDER_BODY = '{{ body }}' _PLACEHOLDER_FOOTER = '{{ footer }}' _PLACEHOLDER_LANG = '{{ language }}' _PLACEHOLDER_INDEX_LETTER_NAV = '{{ index letter nav }}' _PLACEHOLDER_NODE_NAV = '{{ node nav }}' output_dir: pathlib.Path """Output directory where the HTML files are written into.""" output_dir = None use_hardlinks = False def __init__(self, output_dir: pathlib.Path, use_hardlinks: bool = False): if use_hardlinks: _log.info('Using hardlinks instead of symlinks.') if not pygments: _log.info('Verbatim blocks without colors and syntax highlighting' ' because can not import package "pygments".') _log.info(f'Output to: {output_dir}') try: output_dir.mkdir(parents=True, exist_ok=True) except FileExistsError: _log.error(f'The output folder {output_dir} is an existing file. ' 'Use a folder instead.') sys.exit(1) Exporter.output_dir = output_dir Exporter.use_hardlinks = use_hardlinks self._footer_string = None self._hyperorg_css_content = None self._pygments_css_content = None @staticmethod def _as_html_filename(org_filename: str) -> str: """Translate a org-filename to its html pendant. It is assumed that 'org_filename' is relative (to 'Content.input_dir'). It is the same with the resulting html filename. The output folder is added elsewhere. """ return pathlib.Path(org_filename).with_suffix('.html') @staticmethod def _as_index_filename(base_name: str, mode: str) -> pathlib.Path: """Generate the name for an index HTML file. Args: base_name: Basis for the name (e.g. "MyTag") mode: Supplemental specifier (e.g. "filetag" or "year") Returns: The file path. """ return pathlib.Path(f'index__{mode}_{base_name}.html') def _clean_output_dir(self): """Remove all files from the output directory. """ logging.info('Delete all files and folders in the output directory.') out_dir = Exporter.output_dir # credits: https://stackoverflow.com/a/10886685/4865723 if list(out_dir.rglob('**/*.[oO][rR][gG]')): raise FileExistsError( f'There are ORG files in the output folder {out_dir}' '. Maybe you have confused this with the input folder?') # credits to myself # https://stackoverflow.com/a/72333396/4865723 file_objects = sorted(out_dir.rglob('**/*'), key=lambda v: (v.is_dir(), -len(v.parts))) logging.debug(f'{len(file_objects)} file objects') for e in file_objects: try: e.unlink() except IsADirectoryError: e.rmdir() def _set_css_content(self): """ """ hyperorg_css_file_path = Exporter.output_dir / 'style.css' pygments_css_file_path = Exporter.output_dir / 'pygments.css' def _keep_css_content(fp: pathlib.Path) -> str: try: # use style from last hyperorg run with fp.open('r', encoding='utf-8') as handle: logging.info(f'Keeping content of "{fp.name}".') result = handle.read() except FileNotFoundError: result = None return result # Hyperorg CSS self._hyperorg_css_content = _keep_css_content(hyperorg_css_file_path) if not self._hyperorg_css_content: # use default style logging.info('Generate Hyperorg default CSS.') from .stylecss import css_content # pylint: disable=C0415 self._hyperorg_css_content = css_content # Pygments CSS if pygments: self._pygments_css_content \ = _keep_css_content(pygments_css_file_path) if not self._pygments_css_content: logging.info('Generate Pygments CSS.') css = pygments.formatters.HtmlFormatter() \ .get_style_defs('pre') # remove
 tag style definitions
                css = css.split('\n')
                css = filter(lambda line: not line.startswith('pre { '), css)
                css = '\n'.join(css)

                self._pygments_css_content = css

        else:
            self._pygments_css_content = None

    def _write_css_content(self):
        """
        # Create a CSS file in the empty output directory
        """
        def _write_css_helper(fp: pathlib.Path, content: str):
            with fp.open('w', encoding='utf-8') as handle:
                logging.debug(f'Write "{fp}".')
                handle.write(content)

        _write_css_helper(Exporter.output_dir / 'style.css',
                          self._hyperorg_css_content)

        if self._pygments_css_content:
            _write_css_helper(Exporter.output_dir / 'pygments.css',
                              self._pygments_css_content)

    def export_to_html(self):
        """Convert all content (nodes) to HTLM files.

        The whole content in 'Content.nodes' is converted to HTML and then
        stored as HTML files into the output directory ('self._output_dir').
        """

        # Eventually read the existing CSS file's content (into RAM)
        self._set_css_content()

        # Wipe the whole output directory from previous generated files
        self._clean_output_dir()

        # Create a CSS file in the empty output directory
        self._write_css_content()

        #
        idx_items = Exporter._compute_indexes()

        logging.info(f'Start processing {len(Node.nodes)} nodes...')

        # each node
        for orgid, node in Node.nodes.items():
            _log.debug(f'Node: {node.title_org}')

            # generate HTML content
            html = self._as_html(node, idx_items)

            # output filename
            html_filename = Exporter.generate_href_value(orgid)
            fp_out = Exporter.output_dir / html_filename

            with fp_out.open('w', encoding='utf-8') as out_file:
                out_file.write(html)

        def write_index_file(idx, fp):
            _log.debug(f'Index: {fp}')
            html_code = self._node_index_to_html(idx, idx_items)
            fp.write_text(html_code, encoding='utf-8')

        # generate the index files

        # main index
        write_index_file(
            idx=Node.index_data[Node.IDX_ALL],
            fp=Exporter.output_dir / 'index.html')

        # filetag based indexes
        for label, fn, _ in idx_items[1:]:

            fn = Exporter._as_index_filename(fn, Node.IDX_FILETAG)

            write_index_file(
                idx=Node.index_data[Node.IDX_FILETAG][label],
                fp=Exporter.output_dir / fn
            )

        logging.info(f'Wrote {len(Node.nodes)} nodes as HTML files.')

    def _node_index_to_html(self, index_dict: Dict, index_items: list):
        """Generates index files like ``index.html``.

        Args:
            index_dict: Filenames and titles of the index entries.
            index_items: Items used to create navigation bar.
        """

        letter_nav, body = self._node_index_to_html_body(index_dict)

        # full HTML structure
        html = Exporter._get_html_template_index()

        # Language
        lang = locale.getlocale()[0].split('_')[0]  # e.g. 'de_DE' -> 'de'
        html = html.replace(Exporter._PLACEHOLDER_LANG, lang)

        # Navigation bar
        node_nav = Exporter._index_items_as_html(index_items)
        html = html.replace(Exporter._PLACEHOLDER_NODE_NAV, node_nav)

        # Index letters
        html = html.replace(Exporter._PLACEHOLDER_INDEX_LETTER_NAV, letter_nav)

        # Body
        html = html.replace(Exporter._PLACEHOLDER_BODY, body)

        # Footer
        html = html.replace(Exporter._PLACEHOLDER_FOOTER,
                            self._get_footer())

        return html

    def _node_index_to_html_body(self, index_dict: Dict) -> str:
        """Create a list with titles and links to all nodes in HTML.

        Args:
            index_dict: A dictionary index by alphabet letters with lists of
                tuples each with title and filename.

        Return:
            The HTML content (body).
        """

        body = ''
        nav = ''

        # sort dict by key
        index_dict = {key: index_dict[key] for key in sorted(index_dict)}

        # each index letter
        for letter in index_dict:
            # open letter-sec
            body += (f'{_TAB*3}
\n' f'{_TAB*3}
\n' # f'{_TAB*4}
' f'{_TAB*4}

{letter}

\n' f'{_TAB*4}
\n') # Back to Top - Link body += (f'{_TAB*5}Back to Top\n') # open list of titles body += f'{_TAB*5}
    \n' # node (title) reference for title, orgid, is_alias in sorted(index_dict[letter]): title_in_html = _html_from_runs(title) if is_alias: title_in_html = f'{title_in_html}' filename = Exporter.generate_href_value(orgid) body += (f'{_TAB*6}
  • ' f'{title_in_html}
  • \n') # close list and letter-sec body += (f'{_TAB*5}
\n' f'{_TAB*4}
\n' f'{_TAB*3}
\n') # add letter to index navigation nav += f'{_TAB*4}{letter}\n' return nav, body @staticmethod def _subtree_to_html(content: Node) -> str: """ """ return Exporter._body_and_meta(content, True) @staticmethod def _paragraph_to_html(p: elements.Paragraph) -> str: """Generate HTML representation of a `Content.Paragraph` element. Args: p: The paragraph element. Returns: The paragraph as HTML using

tag. """ result = _html_from_runs(p.runs) return f'

{result}

' @staticmethod def _figure_to_html(figure: elements.Figure) -> str: # linked image? link = figure.link if link: src = _html_from_link_run(link) # image else: src = _html_from_image_run(figure.image) src = f'{_TAB*3}{src}\n' # caption if figure.caption: cap = _html_from_runs(figure.caption) cap = f'{_TAB*3}
{cap}
\n' else: cap = '' # unknown properties ... if figure.unknown: tip = '\n'.join(figure.unknown) else: tip = '' # ... and unknown attributes (#+ATTR_HTML) ... if figure.attributes: if tip: tip = f'{tip}\n{figure.attributes}' else: tip = f'{figure.attributes}' # ... as tooltip if tip: tip = f' title="{tip}"' result = f'{_TAB*2}\n{src}{cap}{_TAB*2}' return result @staticmethod def _block_to_html(block: elements.Block) -> str: """Wrapper for `_html_from_block_run()`.""" return _html_from_block_run(block) @staticmethod def _heading_to_html(h: elements.Heading) -> str: """ """ if h.anchor: open_tag = f'' else: open_tag = f'' label = _html_from_runs(h.runs) return f'{open_tag}{label}' @staticmethod def _list_to_html(alist: elements.ListElement) -> str: """Generate HTML representation of a `Content.List` element. The argument `replace_links` is used to deactivate links-to-html conversion in sub-lists in case of a nested list. That links are converted by the parent list object only. Args: alist: The `Content.List` element. replace_links: Only for internal use. Returns: The list as HTML source (``
    `` or ``
      ``). """ tag = 'ol' if alist.ordered else 'ul' items_html = '' for item in alist: # child list? if isinstance(item, elements.ListElement): # remove closing li-tag from item before items_html = items_html[:-5] # child list as html child_html = Exporter._list_to_html(item) # glue together items_html += f'\n{child_html}\n' else: item = _html_from_runs(item) items_html += f'\n
    • {item}
    • ' return f'<{tag}>{items_html}\n' @staticmethod def _backlinks_to_html(backlinks: elements.Backlinks) -> str: """ """ html = Exporter._list_to_html(backlinks) return '\n'.join([ f'{_TAB*2}', f'{_TAB*2}
      ', f'{_TAB*3}
      ', f'{_TAB*3}

      Backlinks

      ', f'{html}' ]) @staticmethod def _meta_other_fields_to_html(val, label): """See `Exporter._meta_to_html()`.""" if isinstance(val, list): val = [f'"{v}"' if ' ' in v else v for v in val] val = ' '.join(val) val = val.strip() return ('
      ' f'{label}: {val}
      ') @staticmethod def _as_locale_conform_date_string( val: Union[datetime.datetime, Any]) -> str: """Return `val` as a datetime string conform to 'locale'. Args: val: Can be a `datetime.datetime` instance or anything else. Returns: A string representation of `val`. """ # When Emacs org(mode) generates the date value via # %U they are enclosed by brackets try: if val.startswith('[') and val.endswith(']'): # remove brackets val = val[1:-1] except AttributeError: pass # locale time string if it is a datetime object try: val = val.strftime('%c') except AttributeError: pass return val @staticmethod def _meta_to_html(content: Node, exclude_fields: list[str] = None) -> str: """Use the meta fields (`Content._meta`) and convert them to HTML. Args: content: A `Content` object. Returns: The HTML output. """ if exclude_fields is None: exclude_fields = [] # no meta data? if not content._meta: # pylint: disable=protected-access return '' result = [] meta = copy.deepcopy(content._meta) # pylint: disable=protected-access # exclude fields for exf in exclude_fields: meta.pop(exf, None) def _mdate_cdate_html_string(val, date_key, date_label) -> str: return (f'
      \n' '\t' f'{date_label}: \n' '
      ') # Modification and creation date. mcdate_present = False for date_key, date_label in [('mtime', 'Modified'), ('ctime', 'Created')]: try: val = meta[date_key] mcdate_present = True val = Exporter._as_locale_conform_date_string(val) result.append( _mdate_cdate_html_string(val, date_key, date_label)) except KeyError: pass if result: result.append('
      ') # Title aliases for alias_runs in meta.get('aliases_runs', []): alias = _html_from_runs(alias_runs) result.append( f'{_TAB*3}
      \n' f'{_TAB*4}Alias: {alias}\n' f'{_TAB*3}
      ' ) # each meta entry for name in meta: # ignore modification, creation date and aliases if name in ['mtime', 'ctime', 'aliases_runs', 'aliases_org']: continue # current value val = meta[name] #