From b9ded15881c039ac1dd65455cf4524f0e1289b8a Mon Sep 17 00:00:00 2001 From: Andros Fenollosa Date: Sat, 27 Dec 2025 10:39:25 +0100 Subject: [PATCH] Replace submodule with direct copy of one.el fork for Docker compatibility --- .gitmodules | 4 - one.el | 1 - one.el/LICENSE | 674 ++++++ one.el/README.org | 16 + one.el/docker/Dockerfile | 15 + one.el/docker/build.el | 8 + one.el/docker/compose.yaml | 8 + one.el/docs/assets/img/keep-learning.png | Bin 0 -> 28180 bytes one.el/docs/assets/one.css | 499 ++++ one.el/docs/docs.org | 1790 +++++++++++++++ one.el/docs/onerc.el | 26 + .../public/docs/getting-started/index.html | 128 ++ .../docs/how-does-one-el-work/index.html | 118 + .../public/docs/install-one-el/index.html | 67 + .../docs/public/docs/miscellaneous/index.html | 380 ++++ .../one-default-render-function/index.html | 417 ++++ .../index.html | 160 ++ .../public/docs/one-ox-headline/index.html | 23 + .../docs/public/docs/one-ox-links/index.html | 256 +++ .../public/docs/one-ox-plain-list/index.html | 99 + .../public/docs/one-ox-quote-block/index.html | 43 + .../public/docs/one-ox-src-block/index.html | 232 ++ one.el/docs/public/docs/one-ox/index.html | 129 ++ one.el/docs/public/img/keep-learning.png | Bin 0 -> 28180 bytes one.el/docs/public/index.html | 189 ++ one.el/docs/public/one.css | 499 ++++ one.el/one-tests.el | 819 +++++++ one.el/one.el | 2021 +++++++++++++++++ 28 files changed, 8616 insertions(+), 5 deletions(-) delete mode 100644 .gitmodules delete mode 160000 one.el create mode 100644 one.el/LICENSE create mode 100644 one.el/README.org create mode 100644 one.el/docker/Dockerfile create mode 100644 one.el/docker/build.el create mode 100644 one.el/docker/compose.yaml create mode 100644 one.el/docs/assets/img/keep-learning.png create mode 100644 one.el/docs/assets/one.css create mode 100644 one.el/docs/docs.org create mode 100644 one.el/docs/onerc.el create mode 100644 one.el/docs/public/docs/getting-started/index.html create mode 100644 one.el/docs/public/docs/how-does-one-el-work/index.html create mode 100644 one.el/docs/public/docs/install-one-el/index.html create mode 100644 one.el/docs/public/docs/miscellaneous/index.html create mode 100644 one.el/docs/public/docs/one-default-render-function/index.html create mode 100644 one.el/docs/public/docs/one-ox-fixed-width-and-example-block/index.html create mode 100644 one.el/docs/public/docs/one-ox-headline/index.html create mode 100644 one.el/docs/public/docs/one-ox-links/index.html create mode 100644 one.el/docs/public/docs/one-ox-plain-list/index.html create mode 100644 one.el/docs/public/docs/one-ox-quote-block/index.html create mode 100644 one.el/docs/public/docs/one-ox-src-block/index.html create mode 100644 one.el/docs/public/docs/one-ox/index.html create mode 100644 one.el/docs/public/img/keep-learning.png create mode 100644 one.el/docs/public/index.html create mode 100644 one.el/docs/public/one.css create mode 100644 one.el/one-tests.el create mode 100644 one.el/one.el diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 8f28c04..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "one.el"] - path = one.el - url = git@github.com:tanrax/one.el.git - branch = add-table-support diff --git a/one.el b/one.el deleted file mode 160000 index 19605b0..0000000 --- a/one.el +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 19605b02b27d345f37a8b6726c63ae2aa35c4d5b diff --git a/one.el/LICENSE b/one.el/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/one.el/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/one.el/README.org b/one.el/README.org new file mode 100644 index 0000000..b309839 --- /dev/null +++ b/one.el/README.org @@ -0,0 +1,16 @@ +~one.el~ a simple Static Site Generator for Emacs Lisp programmers and +org-mode users. + +Check the docs: https://one.tonyaldon.com. + +Note that the docs are built using ~one.el~ itself. To build them, +visit [[./docs/docs.org]] and call ~one-build~ command. This will build the +docs under ~./docs/public/~ subdirectory. + +To run the tests, visit [[./one-tests.el]] and call the ~eval-buffer~ +and ~ert~ commands like this: + +- M-x eval-buffer +- M-x ert RET RET + +Check the [[./LICENSE]]. diff --git a/one.el/docker/Dockerfile b/one.el/docker/Dockerfile new file mode 100644 index 0000000..06f6043 --- /dev/null +++ b/one.el/docker/Dockerfile @@ -0,0 +1,15 @@ +FROM debian:12-slim + +# set work directory +WORKDIR /usr/src/app + +# Print output +RUN export TERM=xterm + +# install software +RUN apt update +RUN apt install -y emacs-nox + +# Build +COPY build.el . +ENTRYPOINT emacs --batch --script build.el diff --git a/one.el/docker/build.el b/one.el/docker/build.el new file mode 100644 index 0000000..d4e8b64 --- /dev/null +++ b/one.el/docker/build.el @@ -0,0 +1,8 @@ +(progn + (require 'package) + (add-to-list 'package-archives + '("melpa" . "https://melpa.org/packages/") t) + (package-initialize) + (package-refresh-contents) + (package-install 'one) + (one-build)) diff --git a/one.el/docker/compose.yaml b/one.el/docker/compose.yaml new file mode 100644 index 0000000..86be3bd --- /dev/null +++ b/one.el/docker/compose.yaml @@ -0,0 +1,8 @@ + +services: + + one-el: + build: . + restart: "no" + volumes: + - .:/usr/src/app/ diff --git a/one.el/docs/assets/img/keep-learning.png b/one.el/docs/assets/img/keep-learning.png new file mode 100644 index 0000000000000000000000000000000000000000..3cb90c53a455507a9cae52d14acf3f1e8b2b2400 GIT binary patch literal 28180 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 p:last-child { + margin-bottom: 0; +} + +.one-hl-results { + background: #202d31 ; + border-left: 2px solid #c5c5c5; + display: block; + margin: auto; + padding: 0.5em 1em; + overflow: auto; + width: 98%; +} + +.one-hl-negation-char { color: #ff6c60} /* font-lock-negation-char-face */ +.one-hl-warning { color: #fd971f} /* font-lock-warning-face */ +.one-hl-variable-name { color: #fd971f} /* font-lock-variable-name-face */ +.one-hl-doc { color: #d3b2a1} /* font-lock-doc-face */ +.one-hl-doc-string { color: #d3b2a1} /* font-lock-doc-string-face */ +.one-hl-string { color: #d3b2a1} /* font-lock-string-face */ +.one-hl-function-name { color: #02d2da} /* font-lock-function-name-face */ +.one-hl-builtin { color: #b2a1d3} /* font-lock-builtin-face */ +.one-hl-type { color: #457f8b} /* font-lock-type-face */ +.one-hl-keyword { color: #f92672} /* font-lock-keyword-face */ +.one-hl-preprocessor { color: #f92672} /* font-lock-preprocessor-face */ +.one-hl-comment-delimiter { color: #8c8c8c} /* font-lock-comment-delimiter-face */ +.one-hl-comment { color: #8c8c8c} /* font-lock-comment-face */ +.one-hl-constant { color: #f5ebb6} /* font-lock-constant-face */ +.one-hl-reference { color: #f5ebb6} /* font-lock-reference-face */ +.one-hl-regexp-grouping-backslash { color: #966046} /* font-lock-regexp-grouping-backslash */ +.one-hl-regexp-grouping-construct { color: #aa86ee} /* font-lock-regexp-grouping-construct */ +.one-hl-number { color: #eedc82} /* font-lock-number-face */ + +.one-hl-sh-quoted-exec { color: #62bd9c} /* sh-quoted-exec */ + +.one-hl-ta-colon-keyword {color: #62b5e0;} /* ta-colon-keyword-face */ + + +.one-hl-org-code { color: #dedede; background: #31424a; } +.one-hl-org-block { color: #c5c5c5 ; background: #31424a; } +.one-hl-org-block-begin-line { color: #c3957e; } +.one-hl-org-block-end-line { color: #c3957e; } +.one-hl-org-meta-line { color: #8c8c8c;} +.one-hl-org-quote { color: #c5c5c5} +.one-hl-org-drawer { color: #d3b2a1; font-size: 0.9em; } +.one-hl-org-special-keyword { color: #c3957e; font-size: 0.9em; } +.one-hl-org-property-value { color: #d2934a; font-size: 0.9em; } +.one-hl-org-level-1 { font-size: 1.7em; text-decoration: underline; } +.one-hl-org-level-2 { font-size: 1.4em; text-decoration: underline; } +.one-hl-org-level-3 { font-size: 1.2em; text-decoration: underline; } +.one-hl-org-level-4 { font-size: 1.1em; text-decoration: underline; } +.one-hl-org-level-5 { font-size: 1.0em; text-decoration: underline; } +.one-hl-org-level-6 { font-size: 1.0em; text-decoration: underline; } +.one-hl-org-level-8 { font-size: 1.0em; text-decoration: underline; } +.one-hl-org-level-8 { font-size: 1.0em; text-decoration: underline; } + + +/* -------- scrollbar -------- */ + +::-webkit-scrollbar { + width: 1em; + height: 1em; +} + +::-webkit-scrollbar-track { + background: #202d31; +} + +::-webkit-scrollbar-thumb { + background: #31424a; + border-radius: 0.5em; +} + +::-webkit-scrollbar-thumb:hover { + background: #31424a; +} + +/* -------- specific to the default render functions -------- */ + +.header { + color: #ffffff; + font-size: 2em; + font-weight: bold; + padding: 0 16px 0 16px; + background: #151515; + width: 100%; + height: 3.5rem; + position: fixed; + top: 0; + left: 0; + border-bottom: 1px solid #1d272b; + display: flex; + justify-content: center; + align-items: center; +} + +.header > a { + color: inherit; + cursor: pointer; + text-decoration: none; +} + +.header > a:visited { + color: inherit; +} + +.content { + margin: 3.5rem auto; + padding-top: 1.8rem; + max-width: 740px; + padding: 0 16px; +} + +.title { + text-align: center; + padding: 1.8rem 0; +} + +.title-empty { + padding-top: 1rem; +} + +/* -------- one-default-home -------- */ + +#home { + margin: 5rem 0 1.5rem 0; +} + +/* -------- one-default-home-list-pages -------- */ + +#home-list-pages { + margin: 5rem 0 1.5rem 0; +} + +#pages ul { + padding: 0; + list-style: none; +} + +#pages a { + display: block; + line-height: 1.2em; + font-size: 1.2em; + color: #dedede; + border-bottom: 1px solid #1d272b; + padding: 1em 0.3em; +} + +#pages a:hover { + text-decoration: none; + background: #31424a; + color: #ffffff; +} + +/* -------- one-default, one-default-with-toc, one-default-with-sidebar, one-default-doc -------- */ + +.nav { + border-top: 1px solid #c5c5c5; + margin-top: 3em; + padding: 2em 0; + display: flex; + justify-content: center; + gap: 0.5em; + font-weight: bold; +} + +.nav a { + display: block; + background: #dedede; + border-radius: 6px; + padding: 0.2em 0.8em; + color: #151515; + width: 20%; + text-align: center; +} + +@media (max-width:600px) { + .nav a { + width: auto; + } +} + +/* -------- one-default-with-toc, one-default-doc -------- */ + +.toc { + display: flex; + justify-content: center; + margin-bottom: 1.8rem; + color: #d1d1d1; +} + +.toc > div { + padding: 0 1em; +} + +.toc a { + color: #d1d1d1; +} + +.toc > div > div:first-child { + text-decoration: underline 1px; + text-align: center; + font-size: 1.2em; + margin-bottom: 16px; +} + +/* --------- one-default-with-sidebar, one-default-doc --------- */ + +#sidebar-header { + color: #ffffff; + font-size: 2em; + font-weight: bold; + padding: 0 16px 0 16px; + background: #151515; + width: 100%; + height: 3.5rem; + position: fixed; + top: 0; + left: 0; + border-bottom: 1px solid #1d272b; + display: flex; + justify-content: center; + align-items: center; +} + +#sidebar-header > a { + color: inherit; + cursor: pointer; + text-decoration: none; +} + +#sidebar-header > a:visited { + color: inherit; +} + +#hamburger { + cursor: pointer; + height: 1em; + fill: #dedede; + display: none; + font-weight: normal; + margin-right: 0.3em; +} + +#sidebar-content { + margin: 3.5rem auto; + display: flex; + margin-left: auto; + margin-right: auto; + max-width: 1140px; + width: 100%; + padding: 1em 16px; +} + +#sidebar { + border-right: 2px solid #31424a; + top: 4.5rem; + position: sticky; + padding-top: 2.2em; + padding-bottom: 6em; + width: 250px; + max-height: 100vh; + overflow-y: auto; +} + +#sidebar a { + display: block; + color: #dedede; +} + +#sidebar a:hover { + text-decoration: none; +} + +#sidebar ul { + list-style: none; + padding:0; +} + +#sidebar li { + padding: 0.5em 0.6em; +} + +#sidebar li:hover { + background: #31424a; +} + +article { + padding: 0 1.5em; + max-width: 640px; + width: 100%; +} + +#sidebar-left { + width: 0; + height: 100%; + position: fixed; + z-index: 3; + top: 0; + left: 0; + transition: 0.25s; + background: #2c444f; + overflow: hidden; /* to make the children disappear when width is 0 */ + overflow-y: auto; +} + +#sidebar-left > div:first-child { + height: 3.5rem; + font-size: 2em; + font-weight: bold; + border-bottom: 1px solid #b8b8b8; + padding-left: 16px; + margin-bottom: 16px; + display: flex; + align-items: center; +} + +#sidebar-left > ul { + padding: 0 16px 0 16px; +} + +#sidebar-left > ul ul { + padding-left: 0.8em; + margin-left: 3px; + border-left: 1px solid #b8b8b8; +} + +#sidebar-left a { + color: #dedede; + text-decoration: none; +} + +#sidebar-left li { + padding: 0.5em 0; + list-style-type: none; +} + +#sidebar-main { + display: none; + top: 0; + right: 0; + width: 100%; + height: 100%; + position: fixed; + background: #080808; + opacity: 0.80; + z-index: 2; +} + +@media (max-width: 840px) { + #hamburger { + display: block; + } + #sidebar { + display: none; + } + #sidebar-content { + justify-content: center; + } + #sidebar-header { + justify-content: left; + } + article { + padding: 0; + } +} diff --git a/one.el/docs/docs.org b/one.el/docs/docs.org new file mode 100644 index 0000000..8114f9a --- /dev/null +++ b/one.el/docs/docs.org @@ -0,0 +1,1790 @@ +* one.el +:PROPERTIES: +:ONE: one-default-with-sidebar +:CUSTOM_ID: / +:END: +** Static Site Generator for Emacs Lisp programmers + +[[youtube:GGP2mxZn4mY]] + +Have you ever wanted to write a blog: + +- contained in a unique org file, +- rendered with only one Emacs command, +- that can be modified by writing Emacs Lisp code (and CSS too), +- with "html templates" that are plain Emacs Lisp data, +- with no config file, +- and no dependencies on external static site generators? + +If so, you might be interested in ~one.el~ a simple *Static Site +Generator* for *Emacs Lisp* programmers and *org-mode* users. + +To get started right away check [[#/docs/install-one-el/][Install one.el]] and [[#/docs/getting-started/][Getting started]] +pages. + +You can find the code here: https://github.com/tonyaldon/one.el. + +Athough ~one.el~ uses org-mode not all the org elements are useful to +build technical blog sites (see [[#/#why][Why one.el?]]). So only a few org +elements have an transcoder function implemented in [[#/docs/one-ox/][one-ox]], the org +backend used by ~one.el~ to build the default website (see [[#/docs/one-default-render-function/][one-default +render function]]). Please check [[#/docs/one-ox/#org-elements-not-supported][Org elements not supported]] before +relying on ~one.el~. + +In ~one.el~, the following org document defines a website with 3 pages +that we build by calling ~one-build~ command while we are visiting it: + +#+BEGIN_SRC org +,* My website +:PROPERTIES: +:ONE: one-default-home +:CUSTOM_ID: / +:END: + +Welcome to my website! + +,* Blog post 1 +:PROPERTIES: +:ONE: one-default +:CUSTOM_ID: /blog/page-1/ +:END: + +My first blog post! + +,* Blog post 2 +:PROPERTIES: +:ONE: one-default +:CUSTOM_ID: /blog/page-2/ +:END: + +My second blog post! +#+END_SRC + +Note that if we want to use the default css style sheet we can add it +by calling ~one-default-add-css-file~ before building the website. + +The path ~/~ in the first ~CUSTOM_ID~ org property tells ~one.el~ that the +page "My website" is the home page. That page is rendered using +~one-default-home~ render function, value of ~ONE~ org property of the +same headline. + +The path ~/blog/page-1/~ in the second ~CUSTOM_ID~ org property tells +~one.el~ that we want to render "Blog post 1" page in such a way +that when we serve our website locally at ~http://localhost:3000~ for +instance, that page is served at ~http://localhost:3000/blog/page-1/~. +How that page is rendered is determined by the value of ~ONE~ org +property of the same headline which is ~one-default~, a render +function. + +The same goes for the last page "Blog post 2". + +As you might have noticed, a ~one.el~ website is an org file where the +pages are the headlines of level 1 with the org properties ~ONE~ and +~CUSTOM_ID~ set. Nothing more! + +~ONE~ is the only org property added by ~one.el~. Its value, an Emacs Lisp +function which returns an HTML string, for a given page determines how +~one.el~ renders that page. + +Paths of pages are set using ~CUSTOM_ID~ org property. + +With that said, if you want to try it you can check [[#/docs/install-one-el/][Install one.el]] and +[[#/docs/getting-started/][Getting started]] pages. + +** Why one.el? +:PROPERTIES: +:CUSTOM_ID: /#why +:END: + +I wrote ~one.el~ because I didn't find an existing static site generator +with the following requirements: + +- I'm not looking for a solution for every type of websites, only for + technical blog sites which are basically chunks of code surrounded + by text, +- I want something simple that I understand and that I can modify + only by writting some Emacs Lisp, +- I want websites to be written to a single org file, +- I want something with no dependencies other than emacs packages + that are not bridges to feed other static site frameworks, +- I want something with no configuration options, if you want to + modify something you write Emacs Lisp code and +- Finally, I want an Emacs solution for an Emacs user. + +Following those requirements led me to ~one.el~, an opiniated static +site generator for Emacs Lisp programmers and Org mode users that +works well if you want to build websites like + +- [[https://minibuffer.tonyaldon.com][minibuffer]] ([[https://github.com/tonyaldon/minibuffer.tonyaldon.com][source]]): learn Emacs Lisp one sexp at a time, +- [[https://posts.tonyaldon.com][Elisp posts]]: some articles about Emacs Lisp, +- [[https://jack.tonyaldon.com][jack]]: HTML generator library for Emacs Lisp, +- [[https://one.tonyaldon.com][one.el]]: documentation of one.el package, +- [[https://lnroom.live][LNROOM]]: learn how to hack on Core Lightning and +- https://tonyaldon.com. + +all built with ~one.el~. + +* Install one.el +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/install-one-el/ +:END: +** Manually + +~one.el~ depends on [[https://jack.tonyaldon.com/][jack]] and [[https://github.com/hniksic/emacs-htmlize][htmlize]] packages that are available on +[[https://melpa.org/][Melpa]]. Once you have them installed you can add ~one.el~ to your +~load-path~ and require it like this: + +#+BEGIN_SRC emacs-lisp +(add-to-list 'load-path "/path/to/one.el/") +(require 'one) +#+END_SRC + +** With package-install + +~one.el~ is also available on [[https://melpa.org/][Melpa]] so you can install it like this: + +#+BEGIN_SRC text +M-x package-install one +#+END_SRC + +** With straight.el + +If you're using [[https://github.com/radian-software/straight.el][straight.el]], to install ~one.el~ you just have to add +this sexp to your init file: + +#+BEGIN_SRC emacs-lisp +(straight-use-package + '(one :type git :host github :repo "tonyaldon/one.el" + :build (:not compile))) +#+END_SRC + +Note that ~:build (:not compile)~ is important. It tells ~straight.el~ +not to byte compile ~one.el~. Something happened in ~straight.el~ +between commits ~3eca39d~ and ~b3760f5~ which broke byte compilation of +~one.el~ if done by ~straight.el~. + +** Let's go + +Now you can create a new website by calling ~one-default-new-project~ +(preferably in an empty directory) and you can build it by calling +~one-build~ command. + +If this is the first time you try ~one.el~ reading [[#/docs/getting-started/][Getting started]] +page might be helpful. + +* Getting started +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/getting-started/ +:END: +** Start a new project + +By calling ~one-default-new-project~ command (preferably in an empty +directory) we produce a new ~one.el~ project with the following +structure: + +#+BEGIN_SRC text +. +├── assets +│ └── one.css +└── one.org +#+END_SRC + +Once done we can build the website under the directory ~./public/~ +by calling ~one-build~ command while we are in the file ~one.org~. Our +project's structure is now: + +#+BEGIN_SRC text +. +├── assets +│ └── one.css +├── one.org +└── public + ├── blog + │ ├── default + │ │ └── index.html + │ ├── default-home-list-pages + │ │ └── index.html + │ ├── one-default-doc + │ │ └── index.html + │ ├── one-default-with-sidebar + │ │ └── index.html + │ └── one-default-with-toc + │ └── index.html + ├── index.html + └── one.css +#+END_SRC + +** Modify the content with live reloading + +To get our website up and running, we serve the files in ~./public/~ +subdirectory using [[https://browsersync.io][brower-sync]] (any webserver serving files is OK). +Once we have it installed, to start a webserver with live reloading, +we run the following commands (in a terminal): + +#+BEGIN_SRC text +$ cd public +$ browser-sync start -s -w --files "*" +#+END_SRC + +Assuming the port ~3000~ isn't used we have our website served at +~http://localhost:3000~. + +Now we can modify the content of ~one.org~ file and see the changes +reflected in the browser after we rebuild/re-render the whole website +or part of it using the following commands ~one-build~, ~one-render-pages~ +and ~one-render-page-at-point~ or the asynchronous version of those +commands ~one-build-async~, ~one-render-pages-async~ and +~one-render-page-at-point-async~. + +** CSS style sheet + +When we call ~one-build~ (or ~one-build-async~) command the pages of the +website are rendered in the directory ~./public/~ and the files in +~./assets/~ directory are copied into ~./public/~ subdirectory. + +When we build a ~one.el~ website with the default render functions and +the default CSS style sheet (this is the case if we used +~one-default-new-project~ as we did above) the style sheet that applies +is ~./public/one.css~ file which is a copy of ~./assets/one.css~ file. + +So in that case, to modify the website's layout we just have to modify +the file ~./assets/one.css~ and copy it in ~./public/~ directory either +with ~one-build~, ~one-build-async~ or ~one-copy-assets-to-public~. + +*** Modify the CSS style sheet with live reloading + +To get the file ~./assets/one.css~ copied into ~./public/~ directory each +time we modify it we can use [[https://eradman.com/entrproject/][entr]] utility like this (being at the root +of our project): + +#+BEGIN_SRC text +$ ls assets/one.css | entr -s 'cp assets/one.css public/' +#+END_SRC + +Combined with ~browser-sync~ live reloading I think we get a decent +programmer experience. + +*** Source blocks + +When we use the default render functions and the default CSS style +sheet, the org content is exported into HTML strings using ~one-ox~ org +export backend. Consequently, ~src-block~ elements are highlighted +using [[https://github.com/hniksic/emacs-htmlize][htmlize]]. + +See [[#/docs/one-ox-src-block/][one-ox | src-block]] for more information. + +* How does one.el work? +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/how-does-one-el-work/ +:END: + +In an org file containing all the pages of our website we can build +the website under ~./public/~ subdirectory by calling either ~one-build~ +or ~one-render-pages~ commands. + +The only difference between those two commands is that before +producing the HTML pages calling ~one-render-pages~, ~one-build~ command +cleans the subdirectory ~./public/~ and copies the content of ~./assets/~ +subdirectory into ~./public/~ subdirectory. + +So let's focus on ~one-render-pages~ command. + +For each page of our website, the function ~one-render-pages~ uses +the render function set in ~ONE~ org property of the page to produce the +HTML string representing the page and stores it in an ~index.html~ file +whom path is determined by ~CUSTOM_ID~ org property of the page. + +Render functions are at the heart of ~one.el~ mechanism. They +determined how pages are rendered. Specifically, render functions are +regular Elisp functions that takes 3 arguments + +- ~page-tree~: corresponding to the parsed tree of the org entry defining + the page, +- ~pages~: the list of pages, +- ~global~: a plist of global informations that are computed once + in ~one-render-pages~ (see ~one-add-to-global~) before rendering the + pages + +and return HTML strings. + +For instance, the following ~hello-world~ function + +#+BEGIN_SRC emacs-lisp +(defun hello-world (page-tree pages global) + "

Hello world!

") +#+END_SRC + +defines a valid render function. We can use it to build a website +like this. In an empty directory, we create a file named ~one.org~ with +the following content: + +#+BEGIN_SRC org +,* The home page +:PROPERTIES: +:ONE: hello-world +:CUSTOM_ID: / +:END: +,* Blog post 1 +:PROPERTIES: +:ONE: hello-world +:CUSTOM_ID: /blog/page-1/ +:END: +#+END_SRC + +We visit that file and call ~one-build~ command. It produces the +following files + +#+BEGIN_SRC text +. +├── one.org (already there) +└── public + ├── blog + │ └── page-1 + │ └── index.html + └── index.html +#+END_SRC + +and both files ~./public/blog/page-1/index.html~ and +~./public/index.html~ have the same content: + +#+BEGIN_SRC html +

Hello world!

+#+END_SRC + +Therefore if we serve the website in ~./public/~ directory at +~http://localhost:3000~ we can access the two "Hello world!" pages +at ~http://localhost:3000/blog/page-1/~ and ~http://localhost:3000~. + +That's it! This is how ~one.el~ works under the hood. + +~one.el~ comes with predefined render functions, a custom CSS style +sheet and a custom [[#/docs/one-ox/][org export backend]] which are used all together to +build that documentation for instance. + +See [[#/docs/getting-started/][Getting started]] to start a new project with those defaults. + +See [[#/docs/one-default-render-function/][one-default render function]] to take inspiration and write your own +render functions. + +* one-default render function +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/one-default-render-function/ +:END: + +In [[#/docs/how-does-one-el-work/][How does one.el work?]] page we saw that render functions are at +the heart of ~one.el~ mechanism. They determine how pages are +rendered. + +We saw that + +#+BEGIN_SRC emacs-lisp +(defun hello-world (page-tree pages global) + "

Hello world!

") +#+END_SRC + +defines a valid render function that can be used to render pages of a +~one.el~ website by setting ~ONE~ org property to ~hello-world~ like this +for instance: + +#+BEGIN_SRC org +,* The home page +:PROPERTIES: +:ONE: hello-world +:CUSTOM_ID: / +:END: +,* Blog post 1 +:PROPERTIES: +:ONE: hello-world +:CUSTOM_ID: /blog/page-1/ +:END: +#+END_SRC + +~one.el~ comes with several default render functions that can be used +instead of the dummy ~hello-world~ function: + +- ~one-default-home~: org content, +- ~one-default-home-list-pages~: org content followed by the list in + reverse order of the pages of the website, +- ~one-default~: org content with navigation buttons at the bottom to go + to the previous page, the next page or a random one, +- ~one-default-with-toc~: same as ~one-default~ but with a table of + content at the top of the page and +- ~one-default-with-sidebar~: same as ~one-default~ but with a sidebar + listing all the pages in the website, +- ~one-default-doc~: same as ~one-default-with-sidebar~ but with a table + of content at the top of the page. + +Those default render functions use [[#/docs/one-ox/][one-ox]] custom org export backend and +~one-default-css~ custom CSS style sheet. + +If we want to start a new project using these defaults, we can use +~one-default-new-project~ command (see [[#/docs/getting-started/][Getting started]]). + +If you plan to write your own render functions you may find the +following sections interesting. + +*** The org document + +Let's consider the following org document in a file named ~one.org~ for +instance: + +#+BEGIN_SRC org +,* Home +:PROPERTIES: +:ONE: one-default-home +:CUSTOM_ID: / +:END: +,* Page 1 +:PROPERTIES: +:ONE: one-default +:CUSTOM_ID: /blog/page-1/ +:END: +,** Headline foo 1 + +[[#/blog/page-2/][Link to Page 2]] + +,** Headline foo 2 +,*** Headline bar + +Some content. + +,*** Headline baz +:PROPERTIES: +:CUSTOM_ID: /blog/page-1/#baz +:END: + +,#+BEGIN_SRC emacs-lisp +(message "foo bar baz") +,#+END_SRC + +,* Page 2 +:PROPERTIES: +:ONE: one-default +:CUSTOM_ID: /blog/page-2/ +:END: + +[[#/blog/page-1/#baz][Link to Headline baz in Page 1]] + +#+END_SRC + +Let's generate the file ~./assets/one.css~ that contains the content of +~one-default-css~ string by calling ~one-default-add-css-file~ command. + +Our project structure is now: + +#+BEGIN_SRC text +. +├── assets +│ └── one.css +└── one.org +#+END_SRC + +*** Build the website + +Now, while vising the file ~one.org~ we call ~one-build~ which builds +"Home", "Page 1" and "Page 2" pages under the directory ~./public/~ such +that our project tree is now: + +#+BEGIN_SRC text +. +├── assets +│ └── one.css +├── one.org +└── public + ├── blog + │ ├── page-1 + │ │ └── index.html + │ └── page-2 + │ └── index.html + ├── index.html + └── one.css +#+END_SRC + +*** Home + +The page "Home" has been generated: + +- in the file ~./public/index.html~ respecting the path information ~/~ in + ~CUSTOM_ID~ org property and +- its HTML content has been created using ~one-default-home~ render + function specified in ~ONE~ org property. + +~./public/index.html~ (pretty printed for the demonstration): + +#+BEGIN_SRC html + + + + + + Home + + +
Home
+
+
+
+ + +#+END_SRC + +*** Page 1 + +The page "Page 1" has been generated: + +- in the file ~./public/blog/page-1/index.html~ respecting the path + information ~/blog/page-1/~ in ~CUSTOM_ID~ org property and +- its HTML content has been created using ~one-default~ render function + specified in ~ONE~ org property. + +~./public/blog/page-1/index.html~ (pretty printed for the demonstration): + +#+BEGIN_SRC html + + + + + + Page 1 + + + +
+
+

Page 1

+
+
+
+

Headline foo 1

+ +
+ +
+

Headline foo 2

+
+

Headline bar

+

Some content.

+
+ +
+

Headline baz

+
+
(message "foo bar baz")
+
+
+
+ +
+ + +#+END_SRC + +*** Page 2 + +The page "Page 2" has been generated: + +- in the file ~./public/blog/page-2/index.html~ respecting the path + information ~/blog/page-2/~ in ~CUSTOM_ID~ org property and +- its HTML content has been created using ~one-default~ render function + specified in ~ONE~ org property. + +~./public/blog/page-2/index.html~ (pretty printed for the demonstration): + +#+BEGIN_SRC html + + + + + + Page 2 + + + +
+
+

Page 2

+
+ + +
+ + +#+END_SRC + +*** How was "Page 1" built? + +When we called ~one-build~ in ~one.org~ buffer, the whole buffer was +parsed with the function ~one-parse-buffer~ and a list of pages was +built from that parsed tree and looked like this: + +#+BEGIN_SRC emacs-lisp +((:one-title "Home" + :one-path "/" + :one-render-page-function one-default-home + :one-page-tree (headline (:raw-value "Home" ...) ...)) + (:one-title "Page 1" + :one-path "/blog/page-1/" + :one-render-page-function one-default + :one-page-tree (headline (:raw-value "Page 1" ...) ...)) + (:one-title "Page 2" + :one-path "/blog/page-2/" + :one-render-page-function one-default + :one-page-tree (headline (:raw-value "Page 2" ...) ...))) +#+END_SRC + +Let's call ~pages~ that list of pages. + +Then for each ~page~ in ~pages~ the function ~one-render-page~ was called +with ~page~, ~pages~ and ~global~ (see ~one-add-to-global~ variable) as +arguments. + +Finally, in ~one-render-page~ the function ~one-default~ or +~one-default-home~ was called with the arguments ~page-tree~, ~pages~ and +~global~ to create the HTML content of each page whom path under the +directory ~./public/~ was determined by the value of ~:one-path~ property +in ~page~ and ~page-tree~ was the value of ~:one-page-tree~ property in +~page~. + +Focusing on "Page 1", the function ~one-default~ was called with the +arguments ~page-tree~, ~page~ and ~global~ with ~page-tree~ being the +following parsed tree of the headline defining "Page 1": + +#+BEGIN_SRC emacs-lisp +(headline + (:raw-value "Page 1" + :CUSTOM_ID "/blog/page-1/" + :ONE "one-default" + :parent (org-data ...) + :one-internal-id "one-9c81c230b6" + ...) + (section (...) (property-drawer ...)) + (headline + (:raw-value "Headline foo 1" + :one-internal-id "one-4df8d962d9" + ...) + (section (...) (paragraph ...))) + (headline + (:raw-value "Headline foo 2" + :one-internal-id "one-9d89da8271" + ...) + (headline + (:raw-value "Headline bar" + :one-internal-id "one-95fa001487" + ...) + (section + (...) + (paragraph (...) #("Some content. " 0 14 (:parent #4))))) + (headline + (:raw-value "Headline baz" + :CUSTOM_ID "/blog/page-1/#baz" + :one-internal-id "baz" + ...) + (section + (...) + (property-drawer ...) + (src-block + (:language "emacs-lisp" + :value "(message \"foo bar baz\")" + ...)))))) +#+END_SRC + +In ~one-default~ the org content of "Page 1" was exported into a HTML +string using ~org-export-data-with-backend~ and [[#/docs/one-ox/][one-ox]] custom org export +backend. Then this HTML string was used in a data structure +representing the HTML page. Finally, ~jack-html~ (see [[https://jack.tonyaldon.com/][jack]]) transformed +that data structure into a HTML string which was written on the file +~./public/blog/page-1/index.html~: + +#+BEGIN_SRC emacs-lisp +(defun one-default (page-tree pages _global) + "Default render function. + +See `one-is-page', `one-render-pages' and `one-default-css'." + (let* ((title (org-element-property :raw-value page-tree)) + (path (org-element-property :CUSTOM_ID page-tree)) + (content (org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox nil)) + (website-name (one-default-website-name pages)) + (nav (one-default-nav path pages))) + (jack-html + "" + `(:html + (:head + (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1")) + (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css")) + (:title ,title)) + (:body + (:div.header (:a (@ :href "/") ,website-name)) + (:div.content + (:div.title + ,(if (not (string= path "/")) + `(:div.title (:h1 ,title)) + '(:div.title-empty))) + ,content + ,nav)))))) +#+END_SRC + +* Miscellaneous +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/miscellaneous/ +:END: +** Page at point + +If we need to render only the page at point, meaning the headline of +level 1 with ~ONE~ and ~CUSTOM_ID~ org properties set, we can use the +commands ~one-render-page-at-point~ and ~one-render-page-at-point-async~. + +** onerc.el file + +We can use an Emacs Lisp file called ~onerc.el~ to customize our +website. It must be in the same directory of the org file containing +the content of our website. + +This file is loaded first in ~one-render-pages~ before rendering the +webpages. + +This is a good place to set ~one-add-to-global~ and ~one-hook~ variables +or to define our own render functions. + +** one-add-to-global +:PROPERTIES: +:CUSTOM_ID: /docs/miscellaneous/#one-add-to-global +:END: + +Render functions takes 3 arguments: + +- ~page-tree~: the parsed tree of the page being rendered, +- ~pages~: the list of pages, +- ~global~: a plist of global informations that are computed once + in ~one-render-pages~ before rendering the pages using + ~one-add-to-global~ variable. + +That means that if a render function needs extra informations, we can +use ~one-add-to-global~ variable to pass those informations to the +render function. + +Specifically, elements in ~one-add-to-global~ list are plist with the +following properties: + +- ~:one-global-property~: a keyword that is used as proprety + in the ~global~ argument passed to the render functions, +- ~:one-global-function~: a function that takes two arguments ~pages~ + (list of pages, see ~one-list-pages~) and ~tree~ + (see ~one-parse-buffer~). That function is called once in + ~one-render-pages~ and its result is used as the value of + the property ~:one-global-property~ in the ~global~ argument + passed to the render functions. + +For instance, if ~one-add-to-global~ is set to + +#+BEGIN_SRC emacs-lisp +((:one-global-property :one-tree + :one-global-function (lambda (pages tree) tree))) +#+END_SRC + +then ~global~ local variable will be set to + +#+BEGIN_SRC emacs-lisp +((:one-tree tree)) +#+END_SRC + +where ~tree~ is the value returned by ~one-parse-buffer~ function. + +** one-hook + +Each function in ~one-hook~ is called once in ~one-render-pages~. + +Those functions take three arguments: + +- ~pages~: list of pages (see ~one-list-pages~), +- ~tree~: see ~one-parse-buffer~, +- ~global~: see [[#/docs/miscellaneous/#one-add-to-global][one-add-to-global]]. + +As those functions take ~global~ argument they are called after +that argument has been let binded using ~one-add-to-global~. + +*** feed.xml example + +This hook is used to build ~feed.xml~ file of [[https://minibuffer.tonyaldon.com][minibuffer.tonyaldon.com]] +website. You can check ~onerc.el~ file of +[[https://github.com/tonyaldon/minibuffer.tonyaldon.com][tonyaldon/minibuffer.tonyaldon.com]] repository to see how it is done. + +*** robot.txt and sitemap.txt + +If we want to add a ~sitemap.txt~ file to our website we can do so using +~one-hook~. + +**** robot.txt + +First we need to indicate in a ~robots.txt~ where our ~sitemap.txt~ is +located. + +Assuming our website is ~https://example.com~ and our ~sitemap.txt~ file +is at the root of it, we can add the following ~robots.txt~ file in the +~assets~ directory (~./assets/robots.txt~): + +#+BEGIN_SRC text +User-Agent: * +Allow: / +Sitemap: https://domain.com/sitemap.txt +#+END_SRC + +**** sitemap.txt + +Now in ~onerc.el~ file: + +1) we set our domain with protocol in the variable ~domain~, +2) then we define ~make-sitemap~ function which will create the file + ~sitemap.txt~ in the ~public~ directory (~./public/sitemap.txt~) each + time be build our website, +3) Finally, to tell ~one.el~ to actually create ~sitemap.txt~ file using + ~make-sitemap~ function each time be build our website, we add it + to ~one-hook~: + +#+BEGIN_SRC emacs-lisp +(defvar domain "https://example.com" + "Domain with protocol to be used to produce sitemap file. + +See `make-sitemap'.") + +(defun make-sitemap (pages tree global) + "Produce file ./public/sitemap.txt + +Global variable `domain' is used as domain with protocol. +This function is meant to be added to `one-hook'." + (with-temp-file "./public/sitemap.txt" + (insert + (mapconcat 'identity + (mapcar + (lambda (page) + (let* ((path (plist-get page :one-path)) + (link (concat domain path))) + link)) + pages) + "\n")))) + +(add-hook 'one-hook 'make-sitemap) +#+END_SRC + +Thanks [[https://github.com/tanrax][@tanrax]] for the code snippet (see [[https://github.com/tonyaldon/one.el/issues/6][issue #6]]). + +** Async commands + +The function ~one-render-pages-async~ and ~one-build-async~ spawn an +~emacs~ subprocess in order to build html pages asynchronously. The +arguments passed to ~emacs~ depends on ~one-emacs-cmd-line-args-async~ value. + +By default, when ~one-emacs-cmd-line-args-async~ is ~nil~, we run ~emacs~ +in "batch mode", we load the user's initialization file and we +evaluate a specific sexp that builds html pages. Specifically, we +pass the following ~command~ (~emacs~ file name followed by command line +arguments) to ~make-process~ function like this: + +#+BEGIN_SRC emacs-lisp +(let* ((emacs (file-truename + (expand-file-name invocation-name invocation-directory))) + (command `(,emacs "--batch" + "-l" ,user-init-file + "--eval" ,sexp)) + (sexp ...)) + (make-process + :name ... + :buffer ... + :command command)) +#+END_SRC + +If ~one-emacs-cmd-line-args-async~ is non-nil, we no longer load the user's +initialization file and replace ~"-l" ,user-init-file~ in ~command~ above +by the elements of ~one-emacs-cmd-line-args-async~. For instance, if +~one-emacs-cmd-line-args-async~ is equal to + +#+BEGIN_SRC emacs-lisp +'("-l" "/path/to/some-elisp-file.el") +#+END_SRC + +then ~command~ becomes + +#+BEGIN_SRC emacs-lisp +(let* (... + (command `(,emacs "--batch" + "-l" "/path/to/some-elisp-file.el" + "--eval" ,sexp)) + ...) + ...) +#+END_SRC + +** Extend one-ox org backend +:PROPERTIES: +:CUSTOM_ID: /docs/miscellaneous/#extend-one-ox-org-backend +:END: + +When we use the default render functions, the org content of the +webpages is exported using [[#/docs/one-ox/][one-ox]] org backend like this + +#+BEGIN_SRC emacs-lisp +(org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox nil) +#+END_SRC + +where ~page-tree~ is the parsed tree of the headline containing the page +being rendered (see [[#/docs/one-default-render-function/][one-default render function]]). + +While ~one-ox~ exports enough org elements for my use cases (see [[#/#why][Why +one.el?]]) this might not be the case for you. + +I think this is not a big problem because we can extend ~one-ox~ +(precisely we can derive a new org backend from ~one-ox~ org backend) +with other transcoder functions for the org elements that miss +transcoder functions. + +Let's see how we can do that with an example. + +*** Extend one-ox with horizontal-rule org elements + +Lines consisting of only dashes (at least 5) are parsed by the org +parser as ~horizontal-rule~ org elements. ~one-ox~ doesn't provide a +transcoder function for ~horizontal-rule~ so we can't use it directly if +we want to have them exported as ~
~ tags in our website. + +In that section we see how to derived an org backend ~one-ox-with-hr~ +from ~one-ox~ org backend that exports ~horizontal-rule~ org elements +with ~
~ tags. + +To do that we define a transcoder function ~my-horizontal-rule~ which +takes 3 arguments (not used) and return the string ~"
"~: + +#+BEGIN_SRC emacs-lisp +(defun my-horizontal-rule (_ _ _) "
") +#+END_SRC + +Then we use that function in the ~:translate-alist~ alist in the body of +the function ~org-export-define-derived-backend~ to define ~one-ox-with-hr~ +org backend: + +#+BEGIN_SRC emacs-lisp +(org-export-define-derived-backend 'one-ox-with-hr 'one-ox + :translate-alist + '((horizontal-rule . my-horizontal-rule))) +#+END_SRC + +Then we can export the org content of the webpages (including the +~horizontal-rule~) using ~one-ox-with-hr~ org backend like this + +#+BEGIN_SRC emacs-lisp +(org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox-with-hr nil) +#+END_SRC + +where ~page-tree~ is the parsed tree of the headline containing the page +being rendered. + +Now that we saw how to derive ~one-ox-with-hr~ org backend and use it, +let's build a website with only a home page with two ~horizontal-rule~. + +In an empty directory let's add the following files: + +- ~one.org~: + + #+BEGIN_SRC org + ,* Home page + :PROPERTIES: + :ONE: my-render-function + :CUSTOM_ID: / + :END: + + foo + + ----- + + bar + + ----- + + baz + #+END_SRC + +- ~onerc.el~: + + #+BEGIN_SRC emacs-lisp + (defun my-horizontal-rule (_ _ _) "
") + + (org-export-define-derived-backend 'one-ox-with-hr 'one + :translate-alist + '((horizontal-rule . my-horizontal-rule))) + + (defun my-render-function (page-tree pages _global) + "" + (let* ((title (org-element-property :raw-value page-tree)) + (content (org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox-with-hr + nil))) + (jack-html + "" + `(:html + (:head (:title ,title)) + (:body + (:h1 ,title) + ,content))))) + #+END_SRC + +Now while visiting ~one.org~ file we call ~one-build~ to build our website +with ~
~ tags. + +* one-ox +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/one-ox/ +:END: +** Org export backend used by the default render functions + +~one.el~ (specifically the default render functions) uses its own org +export backend called ~one-ox~ to export the org content of the pages +into HTML strings. + +For instance, the render function ~one-default~ takes as first argument +~page-tree~ which is the current page being rendered (~page-tree~ is the +org parsed data structure representing the page) and exports it as an +HTML string using ~org-export-data-with-backend~ function and ~one-ox~ +export backend and uses it to render the HTML page: + +#+BEGIN_SRC emacs-lisp +(defun one-default (page-tree pages _global) + "..." + (let* (... + (content (org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox nil)) + ...) + (jack-html + "" + `(:html + (:head ...) + (:body ... (:div.content ... ,content ,nav)))))) +#+END_SRC + +This org backend is taylor for ~one.el~ usage. So it doesn't try to +export all the org elements unlike ~html~ backend and when the org +elements are exported they differ from what we can expect from ~html~ +backend. + +For instance ~headline~ elements don't take into account markups +neither links. + +Another example are the ~link~ elements. They don't support org fuzzy +links and links to local files that are not in the subdirectories +~./public/~ or ~./assets/~ raise errors. + +You can read how the supported org elements are exported by ~one-ox~ org +backend in the following page: + +- [[#/docs/one-ox-headline/][one-ox | headline]], +- [[#/docs/one-ox-src-block/][one-ox | src-block]], +- [[#/docs/one-ox-quote-block/][one-ox | quote-block]], +- [[#/docs/one-ox-fixed-width-and-example-block/][one-ox | fixed-width and example-block]], +- [[#/docs/one-ox-links/][one-ox | links]] and +- [[#/docs/one-ox-plain-list/][one-ox | plain-list]]. + +** Org elements not supported +:PROPERTIES: +:CUSTOM_ID: /docs/one-ox/#org-elements-not-supported +:END: + +The org elements that are not supported are the following: +~center-block~, ~clock~, ~drawer~, ~dynamic-block~, ~entity~, ~export-block~, +~export-snippet~, ~footnote-reference~, ~horizontal-rule~, ~inline-src-block~, +~inlinetask~, ~keyword~, ~latex-environment~, ~latex-fragment~, ~line-break~, +~node-property~, ~planning~, ~property-drawer~, ~radio-target~, ~special-block~, +~statistics-cookie~, ~table~, ~table-cell~, ~table-row~, ~target~, ~timestamp~, +~verse-block~. + +Note that "not supported" means they are not rendered by default by +~one.el~ but we can still use them or even extend ~one-ox~ org export +backend to take some of them into account. + +Why doesn't ~one.el~ support all org elements? + +1. I don't need those org elements to write my technical blogs: + + - I don't do math. No support for Latex, + - I don't use table. No support for tables, + - etc. + +2. ~one-ox~ org backend is used only by the default render functions, so + if you need more org elements you can either use another org + backend or extend ~one-ox~ org backend and use this other org backend + in your own render functions (See [[#/docs/miscellaneous/#extend-one-ox-org-backend][Extend one-ox org backend]]). + +* one-ox | headline +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/one-ox-headline/ +:END: + +Note that markups and links are not exported if used in headlines, only +the raw value string. + +So don't use them in headlines. + +* one-ox | src-block +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/one-ox-src-block/ +:END: +** Code highlighting with htmlize +*** Description + +~one-ox~ highlights code via the function ~one-ox-htmlize~ that uses +[[https://github.com/hniksic/emacs-htmlize][htmlize]] to do the work. + +For a given piece of code ~X~ in a certain language ~Y~, ~X~ will be +highlighted as it would be in the emacs mode ~Z~ used to edit ~Y~ code. + +For instance, ~clojure-mode~ is used to highlight Clojure code and +~sh-mode~ is used to highlight Bash code. + +Attributes of a face (like ~background-color~ or ~foreground-color~) +are not taken directly. A generated name for the face is produced and +used as the CSS class for the parts of the code ~X~ that are highlighted +with that face. + +For instance, in ~sh-mode~, the word ~echo~ is highlighted with the face +~font-lock-builtin-face~. So, the word ~echo~ in a piece of Shell (or +Bash) code will be transformed into: + +#+BEGIN_SRC html +echo +#+END_SRC + +The whole piece of code ~X~, once the previously described operations +have been done, is wrapped: + +1) for a normal block with the component: + + #+BEGIN_SRC html +
...
+ #+END_SRC + +2) for a result block with the component: + + #+BEGIN_SRC html +
...
+ #+END_SRC + + See section [[#/docs/one-ox-src-block/#org-keywords-results-and-attr_one_results][org keyword RESULTS]]. + +*** Example with Bash code + +For instance, the following org src-block, containing some ~bash~ code: + +#+BEGIN_SRC org +,#+BEGIN_SRC bash +echo "list file's extensions in current dir:" +for f in `ls`; do + echo ${f##*.} +done +,#+END_SRC +#+END_SRC + +is exported as follow: + +#+BEGIN_SRC html +
echo "list file's extensions in current dir:"
+for f in `ls`; do
+    echo ${f##*.}
+done
+ +#+END_SRC + +and rendered like this: + +#+BEGIN_SRC bash +echo "list file's extensions in current dir:" +for f in `ls`; do + echo ${f##*.} +done +#+END_SRC + +Note that ~one-ox-htmlize~ has produced and used the following CSS +classes (listed with their corresponding emacs faces): + +#+BEGIN_SRC text +# from font-lock +one-hl-builtin --> font-lock-builtin-face +one-hl-keyword --> font-lock-keyword-face +one-hl-string --> font-lock-string-face +one-hl-variable-name --> font-lock-variable-name-face + +# specific to sh-mode +one-hl-sh-quoted-exec --> sh-quoted-exec +#+END_SRC + +You might have notice the pattern used for ~font-lock~ faces and the one +used for mode specific faces. + +~one.el~ provides a default style sheet (~one-default-css~) that has the +CSS classes defined for all the ~font-lock~ faces (faces starting by +~font-lock-~) but not the specific faces used by each prog mode. + +You can add the CSS classes specific to the prog modes you use as you +go and need them. + +** Org keyword RESULTS +:PROPERTIES: +:CUSTOM_ID: /docs/one-ox-src-block/#org-keywords-results-and-attr_one_results +:END: + +Result blocks are preceded by a line starting with ~#+RESULTS:~. Blocks +that are not result blocks are normal blocks. + +When exported, normal blocks and result blocks differ only by their +CSS classes: + +- ~one-hl one-hl-block~ for normal blocks, +- ~one-hl one-hl-results~ for result blocks. + +This way result blocks can be rendered with a different style +than normal blocks as we can see in the following example. + +*** Example using org keyword 'RESULTS' + +The following org snippet: + +#+BEGIN_SRC org +,#+BEGIN_SRC bash :results output +ls +,#+END_SRC + +,#+RESULTS: +: assets +: docs.org +: public +#+END_SRC + +is exported by ~one-ox~ as follow: + +#+BEGIN_SRC html +
ls
+
assets
+docs.org
+public
+#+END_SRC + +and is rendered by ~one-ox~ with the first block (normal block) having a +different style from second block (result block): + +#+BEGIN_SRC bash :results output +ls +#+END_SRC + +#+RESULTS: +: assets +: docs.org +: public + +** Code blocks inside list + +Lists can contain source blocks as we can see in the following org +snippet + +#+BEGIN_SRC org +1. item 1 + + ,#+BEGIN_SRC emacs-lisp + (message "src-block in item 1") + ,#+END_SRC + +2. item 2 +3. item 3 +#+END_SRC + +which is exported by ~one~ as follow + +#+BEGIN_SRC html +
    +
  1. +

    item 1

    +
    (message "src-block in item 1")
    +
  2. +
  3. item 2

  4. +
  5. item 3

  6. +
+#+END_SRC + +and is rendered by ~one-ox~ like this: + +1. item 1 + + #+BEGIN_SRC emacs-lisp + (message "src-block in item 1") + #+END_SRC + +2. item 2 +3. item 3 + +* one-ox | quote-block +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/one-ox-quote-block/ +:END: + +Blocks defined with ~#+BEGIN_QUOTE ... #+END_QUOTE~ pattern are +quote-block. + +They are exported by ~one-ox~ in a ~
...
~ +component with the CSS class ~one-blockquote~. + +The following org snippet: + +#+BEGIN_SRC org +,#+BEGIN_QUOTE +A quitter never wins and a winner never quits. —Napoleon Hill +,#+END_QUOTE +#+END_SRC + +defines a quote and is exported by ~one-ox~ as follow + +#+BEGIN_SRC html +

A quitter never wins and a winner never quits. —Napoleon Hill

+#+END_SRC + +and looks like this + +#+BEGIN_QUOTE +A quitter never wins and a winner never quits. —Napoleon Hill +#+END_QUOTE + +* one-ox | fixed-width and example-block +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/one-ox-fixed-width-and-example-block/ +:END: +** Description + +A line starting with a colon ~:~ followed by a space defines a +~fixed-width~ element. A ~fixed-width~ element can span several +lines. + +Blocks defined with ~#+BEGIN_EXAMPLE ... #+END_EXAMPLE~ pattern are +~example-block~ elements. + +Both ~fixed-width~ and ~example-block~ blocks are treated as [[#/docs/one-ox-src-block/][src-block]] in +~text-mode~. So: + +1. they are highlighted as ~text-mode~ would do, +2. they are exported in ~
...
~ components + (indentation and newlines are respected) and +3. the CSS classes used depend on the block's type: + + - normal blocks use ~one-hl one-hl-block~ CSS classes and + - result blocks use ~one-hl one-hl-results~ CSS classes (see [[#/docs/one-ox-src-block/#org-keywords-results-and-attr_one_results][org + keyword RESULTS]]). + +** Example + +The following org snippet + +#+BEGIN_SRC org +Here is a ~fixed-width~ element (one line): + +: I'm a fixed-width element + +~fixed-width~ elements can also be used within lists: + +- item 1 + + : fixed-width element + +- item 2 + + ,#+BEGIN_SRC bash :results output + printf 'multiline fixed-width element\nthat is also a result block,\nso has a different style.' + ,#+END_SRC + + ,#+RESULTS: + : multiline fixed-width element + : that is also a result block, + : so has a different style. + +Although I don't often use ~example-block~ elements, here is one: + +,#+BEGIN_EXAMPLE +This is + an example! +,#+END_EXAMPLE +#+END_SRC + +is exported by ~one~ as follow + +#+BEGIN_SRC html +

Here is a fixed-width element (one line): +

+ +
I'm a fixed-width element
+ + +

fixed-width elements can also be used within lists: +

+ +
  • item 1 +

    + +
    fixed-width element
    +
  • + +
  • item 2 +

    + +
    printf 'multiline fixed-width element\nthat is also a result block,\nso has a different style.'
    + +
    multiline fixed-width element
    +that is also a result block,
    +so has a different style.
    +
  • +
+ +

Although I don't often use example-block elements, here is one: +

+ +
This    is
+        an    example!
+#+END_SRC + +and looks like this: + +Here is a ~fixed-width~ element (one line): + +: I'm a fixed-width element + +~fixed-width~ elements can also be used within lists: + +- item 1 + + : fixed-width element + +- item 2 + + #+BEGIN_SRC bash :results output + printf 'multiline fixed-width element\nthat is also a result block,\nso has a different style.' + #+END_SRC + + #+RESULTS: + : multiline fixed-width element + : that is also a result block, + : so has a different style. + +Although I don't often use ~example-block~ elements, here is one: + +#+BEGIN_EXAMPLE +This is + an example! +#+END_EXAMPLE + +* one-ox | links +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/one-ox-links/ +:END: +** http, https, mailto links + +Web links (starting by ~http~ or ~https~) and links to message +composition (starting by ~mailto~) are exported as we expect. + +For instance the following link + +#+BEGIN_SRC org +http://tonyaldon.com +#+END_SRC + +is exported as follow + +#+BEGIN_SRC html +http://tonyaldon.com +#+END_SRC + +and rendered like this: http://tonyaldon.com. + +This following link with a description + +#+BEGIN_SRC org +[[https://tonyaldon.com][Tony Aldon (https)]] +#+END_SRC + +is exported as follow + +#+BEGIN_SRC html +Tony Aldon (https) +#+END_SRC + +and rendered like this: [[https://tonyaldon.com][Tony Aldon (https)]]. + +This ~mailto~ link + +#+BEGIN_SRC org +[[mailto:tony@tonyaldon.com][send me an email]] +#+END_SRC + +is exported as follow + +#+BEGIN_SRC html +send me an email +#+END_SRC + +and rendered like this: [[mailto:tony@tonyaldon.com][send me an email]]. + +** Custom ID links + +In ~one.el~, ~CUSTOM_ID~ org property is used to defined the path of pages +or the path to specific heading in pages. + +Considering the following org document + +#+BEGIN_SRC org +,* Home Page +:PROPERTIES: +:ONE: one-default-home +:CUSTOM_ID: / +:END: + +- [[#/blog/page-1/]] +- [[#/blog/page-1/#headline-1]] + +,* Page 1 +:PROPERTIES: +:ONE: one-default +:CUSTOM_ID: /blog/page-1/ +:END: +,** headline 1 in Page 1 +:PROPERTIES: +:CUSTOM_ID: /blog/page-1/#headline-1 +:END: +#+END_SRC + +the link ~[[#/blog/page-1/]]~ in "Home Page" targets "Page 1" page +and the link ~[[#/blog/page-1/#headline-1]]~ in "Home Page" targets the +heading "headline 1 in page Page 1" in the "Page 1" page. + +Those paths define valid web urls starting at the root of the website +if we respect the following rules for ~CUSTOM_ID~ values: + +1. we use only url-encoded characters, +2. we start them with a ~/~ and end them with ~/~ excepted for the home + page which is a single ~/~, +3. we use ~#~ character to start the last part of the path when we are + targeting a heading tag with its ~id~ being the last part after the ~#~ + character. + +The benefits of these "rules/conventions" are: + +1. when we export ~custom-id~ links using ~one-ox~ org backend we can + leave them as they are and +2. the navigation between pages inside emacs using ~custom-id~ links + works out-of-the-box. + +*** Example of a link to a page + +The following link + +#+BEGIN_SRC org +[[#/docs/one-ox-plain-list/][one-ox | plain-list]] +#+END_SRC + +is exported to this anchor tag that links to the page ~/docs/one-ox-plain-list/~: + +#+BEGIN_SRC html +one-ox | plain-list +#+END_SRC + +and is rendered like this [[#/docs/one-ox-plain-list/][one-ox | plain-list]]. + +*** Example of a link to a heading in a page + +The following link + +#+BEGIN_SRC org +[[#/docs/one-ox-plain-list/#unordered-lists][unordered lists heading in the page about plain-list]] +#+END_SRC + +is exported to this anchor tag that links to the heading with the ~id~ +set to ~unordered-lists~ on the page ~/docs/one-ox-plain-list/~: + +#+BEGIN_SRC html +unordered lists heading in the page about plain-list +#+END_SRC + +and is rendered like this [[#/docs/one-ox-plain-list/#unordered-lists][unordered lists heading in the page about +plain-list]]. + +** Fuzzy links + +I don't use ~fuzzy~ links. So, if there is a ~fuzzy~ link +in the document, that means I wrote the link wrong. + +Broken links are bad user experience. I don't like them. + +So I decided that ~one-ox~ raises an error (hard-coded) when we try to +export a fuzzy link to HTML. + +For instance, the following ~fuzzy~ link: + +#+BEGIN_SRC org +[[fuzzy search]] +#+END_SRC + +raise an error like the following: + +#+BEGIN_SRC emacs-lisp +(one-link-broken "fuzzy search" "fuzzy links not supported" "goto-char: 5523") +#+END_SRC + +** File links +*** Links to local files in assets and public directories + +Links to local files in ~./assets/~ and ~./public/~ directories like + +#+BEGIN_SRC org +[[./assets/foo/bar.txt][Bar file]] +[[./public/foo/baz.txt][Baz file]] +#+END_SRC + +are exported with the prefixes ~./assets~ and ~./public~ of the path +removed like this: + +#+BEGIN_SRC html +Bar file +Baz file +#+END_SRC + +*** Local file links that raise one-link-broken error + +Any file link that doesn't point to a file in ~./assets/~ or ~./public/~ +subdirectories raises an ~one-link-broken~ error when we try to +export it with ~one-ox~ org backend + +For instance if we try to export using ~one-ox~ org backend the +following link to the file ~foo.txt~ in the directory ~/tmp/~ + +#+BEGIN_SRC org +[[/tmp/foo.txt]] +#+END_SRC + +which is not in ~./public/~ subdirectory nor in ~./assets/~ subdirectory +we will get an error like the following: + +#+BEGIN_SRC emacs-lisp +(one-link-broken "/tmp/" "goto-char: 26308") +#+END_SRC + +*** Links to images + +Links to local files in ~./assets/~ and ~./public/~ directories whom path +matches ~one-ox-link-image-extensions~ regexp are exported with an ~img~ +tag. + +For instance the following link to an image in ~./assets/img/~ directory + +#+BEGIN_SRC org +[[./assets/img/keep-learning.png][Keep Learning]] +#+END_SRC + +is exported as follow + +#+BEGIN_SRC html +Keep Learning +#+END_SRC + +and rendered like this + +[[./assets/img/keep-learning.png][Keep Learning]] + +* one-ox | plain-list and item +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /docs/one-ox-plain-list/ +:END: + +Only unordered and ordered lists are supported. + +** Unordered lists +:PROPERTIES: +:CUSTOM_ID: /docs/one-ox-plain-list/#unordered-lists +:END: + +The following org snippet (unordered list): + +#+BEGIN_SRC org +- a thing, +- another thing, +- and the last one. +#+END_SRC + +is exported by ~one-ox~ as follow + +#+BEGIN_SRC html +
    +
  • +

    a thing,

    +
  • +
  • +

    another thing,

    +
  • +
  • +

    and the last one.

    +
  • +
+#+END_SRC + +and is rendered like this: + +- a thing, +- another thing, +- and the last one. + +** Ordered list + +The following org snippet (unordered list): + +#+BEGIN_SRC org +1. first, +2. second, +3. third. +#+END_SRC + +is exported by ~one-ox~ as follow + +#+BEGIN_SRC html +
    +
  1. +

    a thing,

    +
  2. +
  3. +

    another thing,

    +
  4. +
  5. +

    and the last one.

    +
  6. +
+#+END_SRC + +and is rendered like this: + +1. first, +2. second, +3. third. diff --git a/one.el/docs/onerc.el b/one.el/docs/onerc.el new file mode 100644 index 0000000..287f1fd --- /dev/null +++ b/one.el/docs/onerc.el @@ -0,0 +1,26 @@ +;; We do this because we want org-mode links in source block +;; (with org-mode content) to be visible (link and description parts). +;; You can use (remove-hook 'org-mode-hook #'visible-mode) to remove it. +(add-hook 'org-mode-hook #'visible-mode) + +(require 'ol) + +(defun one-ox-link-youtube (link description type info) + "Export youtube link `[[youtube:youtube-id]]'." + (let ((youtube-embed-link (concat "https://www.youtube.com/embed/" link))) + (jack-html + `(:iframe + (@ + :style "width:100%;aspect-ratio: 16 / 9;" + :src ,(concat youtube-embed-link "?" + ;; "autoplay=1" + "&fs=1" ; show the fullscreen button + "&modestbranding=1" ; don't show YouTube logo in bottom right + "&rel=0" ; show only related video from my channel + "&widget_referrer=https://one.tonyaldon.com" ; analytics + ) + :title "YouTube video player playing " + :frameborder "0" + :allow "fullscreen;accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture;web-share"))))) + +(org-link-set-parameters "youtube" :export #'one-ox-link-youtube) diff --git a/one.el/docs/public/docs/getting-started/index.html b/one.el/docs/public/docs/getting-started/index.html new file mode 100644 index 0000000..5657aea --- /dev/null +++ b/one.el/docs/public/docs/getting-started/index.html @@ -0,0 +1,128 @@ +Getting started \ No newline at end of file diff --git a/one.el/docs/public/docs/how-does-one-el-work/index.html b/one.el/docs/public/docs/how-does-one-el-work/index.html new file mode 100644 index 0000000..493c2f1 --- /dev/null +++ b/one.el/docs/public/docs/how-does-one-el-work/index.html @@ -0,0 +1,118 @@ +How does one.el work? \ No newline at end of file diff --git a/one.el/docs/public/docs/install-one-el/index.html b/one.el/docs/public/docs/install-one-el/index.html new file mode 100644 index 0000000..21966e6 --- /dev/null +++ b/one.el/docs/public/docs/install-one-el/index.html @@ -0,0 +1,67 @@ +Install one.el \ No newline at end of file diff --git a/one.el/docs/public/docs/miscellaneous/index.html b/one.el/docs/public/docs/miscellaneous/index.html new file mode 100644 index 0000000..d2feb75 --- /dev/null +++ b/one.el/docs/public/docs/miscellaneous/index.html @@ -0,0 +1,380 @@ +Miscellaneous \ No newline at end of file diff --git a/one.el/docs/public/docs/one-default-render-function/index.html b/one.el/docs/public/docs/one-default-render-function/index.html new file mode 100644 index 0000000..0cccb89 --- /dev/null +++ b/one.el/docs/public/docs/one-default-render-function/index.html @@ -0,0 +1,417 @@ +one-default render function \ No newline at end of file diff --git a/one.el/docs/public/docs/one-ox-fixed-width-and-example-block/index.html b/one.el/docs/public/docs/one-ox-fixed-width-and-example-block/index.html new file mode 100644 index 0000000..2163e5a --- /dev/null +++ b/one.el/docs/public/docs/one-ox-fixed-width-and-example-block/index.html @@ -0,0 +1,160 @@ +one-ox | fixed-width and example-block \ No newline at end of file diff --git a/one.el/docs/public/docs/one-ox-headline/index.html b/one.el/docs/public/docs/one-ox-headline/index.html new file mode 100644 index 0000000..ab24527 --- /dev/null +++ b/one.el/docs/public/docs/one-ox-headline/index.html @@ -0,0 +1,23 @@ +one-ox | headline \ No newline at end of file diff --git a/one.el/docs/public/docs/one-ox-links/index.html b/one.el/docs/public/docs/one-ox-links/index.html new file mode 100644 index 0000000..28a6500 --- /dev/null +++ b/one.el/docs/public/docs/one-ox-links/index.html @@ -0,0 +1,256 @@ +one-ox | links \ No newline at end of file diff --git a/one.el/docs/public/docs/one-ox-plain-list/index.html b/one.el/docs/public/docs/one-ox-plain-list/index.html new file mode 100644 index 0000000..91e9bea --- /dev/null +++ b/one.el/docs/public/docs/one-ox-plain-list/index.html @@ -0,0 +1,99 @@ +one-ox | plain-list and item \ No newline at end of file diff --git a/one.el/docs/public/docs/one-ox-quote-block/index.html b/one.el/docs/public/docs/one-ox-quote-block/index.html new file mode 100644 index 0000000..e0de427 --- /dev/null +++ b/one.el/docs/public/docs/one-ox-quote-block/index.html @@ -0,0 +1,43 @@ +one-ox | quote-block \ No newline at end of file diff --git a/one.el/docs/public/docs/one-ox-src-block/index.html b/one.el/docs/public/docs/one-ox-src-block/index.html new file mode 100644 index 0000000..5543c94 --- /dev/null +++ b/one.el/docs/public/docs/one-ox-src-block/index.html @@ -0,0 +1,232 @@ +one-ox | src-block \ No newline at end of file diff --git a/one.el/docs/public/docs/one-ox/index.html b/one.el/docs/public/docs/one-ox/index.html new file mode 100644 index 0000000..d930fb8 --- /dev/null +++ b/one.el/docs/public/docs/one-ox/index.html @@ -0,0 +1,129 @@ +one-ox \ No newline at end of file diff --git a/one.el/docs/public/img/keep-learning.png b/one.el/docs/public/img/keep-learning.png new file mode 100644 index 0000000000000000000000000000000000000000..3cb90c53a455507a9cae52d14acf3f1e8b2b2400 GIT binary patch literal 28180 zcmeHwXH-*Nv~5taE1wFeNKpZiCL+CKp^1Pr>7XJtR3Y?cMY@3WrnE@!y(&U1K&YX2 zsR2R@p(byi#P8m5|GZ!K$9wn67?K2X_St){wdR^@t{n_`sH$-4IKy!m40cLM@ty_@ zcEk?`J0yDaF!;$__m{ii;h2M>o)Zjqx(WJ6g|h$r4E*q-vz)H8roFkd>yzhZFjrUC z8&-C3C(|bmW;g7gTO=$?GQeP$VM_OIYq=#Zj(EOLGV^X+9jn4kFJ2p9OL%tfKzQtT z_7`l1FE78l`IIT0>uQ8L*Hf&~kSZdxw_P-8zI0RATEQTvte2h-kt<-F^OWf@wfc&N z?1|5`3v72T&lD%LjqV;Xd%*j8aiC;&WN&SIFK*Fu)FNe(xH_{djn-tia3S^>m?-oW z>OP%%gz^+7>v^>C&r_wp&tQM|0QPqeKs@~QgTH$K!Qx*(_`3&x{Q%m5zkcv{56Eut z7Zd-+0@)G%=ElF6_!krZTQN~9h#8F6Xd{@Nq1$URzvay$ev>ZkoB&e;ANl7{h2F?N zU8-HSRnd3KWv`x(G?%s-MPgjq@}AHoObU2R?jePaR}qj6zBlcvd~@G>9)!VY90NaJeknqR%;uz0dEJtp0MHA%v=;J_W>y3R)Rc z!TjE+LH77)>#>j_>ZBr7o|lEn{G^|2q!A*doXy{#IC5A^K;TS`Lgv(`eN9JP)4A2A z6t!0j=WI7L3M;{2rZ%+kx;5!%;n7;Adbq_IJCAEm2`gHA zUL@z?8qcfCqe5p~ml(ooTvWd$zY?3ZD`)6^C(tMOv^!hh#@Hwf$(#2rTP{ksGxuJd zzDE~UzQtk^8TR0t8cR~JCrfg0g6}PQ*F8dI3>^&SM@@F6ScQbQ3+jPSCp%s>#0J)- zt+VVU7p2nkiPlLZyWZDHEH0++U5h+Ynx@SqcT)Hle?JejK*Q|@Uul7Jj)eTxt-(6~ zNvsTW&=3(WnHf6Kn-2zDec4?zRczth{Pv5r*5FOoX^n4z_4jk1uDCcT z(_7@&+Pr2imOnHa%#B_QK^%+<9jP!36g5Tgw(A-Njn!51G4~5xB<&@IZ*4KLuCA=? z)m?XZdZYox(Pi^tQfQXYj2KENF%Iw%^uT7g5wpVHOz#pNG<(+9%Sf+|oVfxfdOa9+ z5(c}e=91XUvpAYk19vt~9R+Ub*0$Z&Uz8`}p|5*;!0v7k*D6MNXUdgtIY^E4Sdh4p zvpZ?wdEWe}w7Z^=vFmLU!@4pNh7;pfd*5DI*X|d~t(GhAPEENo2Fs8JiEeX^UlVH{ zB~=&Gk8LeQ@t1xUIO8!Q%RcHo9pN@zRkvD>UJk*LY-(8v&<+kCaQl4-2K$aw4{=+) zIPzwM)Kqb*0>2LY+YQUUzZ$=uU^_5bHNrKmQR}R%W>}OZL0nE~T8$yq%gA%kx14Py z=0D&2;&i^58@&`lEs9Xj`7k-?@`&k?$#WI!B({BTW(7NEc+oz>PgleHfm+k6{pg; zAh(4&U%BPx+L$MZX)hBi)@#qC^ z58zCVj&~p=vMkA|psyIgovH}?_DL*xtlHd>y?$eGdbJfPG$@!o`9b-NFMg7pDX5Wy zFQ+BOMCO74rbps=!?qT%lYIg6KQD%UFQJ#hRPLgPXy6cqE5{mv8Emz<Vv58v*jkCcGe2U!3&=yE8Z!hh-?Tv*Ld67?$}_W`1_L?)zTWsX_uaj#0QpH6JaXR~Odf z^k(^N;wUuCr4+=T&HOGZhOgYC>=fi>)&r6UV6gSOl(CqG25X8pEKh~CeQWTtFU^4G zOl#~7&;^C%Tqyfo!&p%Fc$xcVO?HM`Ujd)pZgkg)0f2MHWg`o{RrO| zsv6X05a$KcDts8Vaqeq@PJ!-EAT%K-Ny}I8a(XA>h1OQnA(@J&!0j6diBlHX*(RY& z832m%j#LT-aj&+V2usOJp{W4o>7y+jl!^@M6R|;_g27r(kkLI9#HC?bM(kymb+wks z`jJRpqusX_LZ<43)m>dTJZ3by!k(BEw0lAV=YFNq+<>1?@|i|K^kgc&`~?V*LEM&i z7qF*gq_T+cQ}>csR1VE(*WkjKc^Ykj=G{Jtg61$B>P( z$_Z`AYZ_YW3V|G@v|3=u!sUH3sSiP%?P%*AdD+}zbpUs?$tl7DLLVUFnokyPMg z&fn?r$$j(ffLyD58Ev1fi0hd%=(y?ORZ`w!!M8#7UNWw*vBH2GwSMU}@J!QR-m~tw z8!6z}w8YA5RO4;Ls><+B($%Y&Q(BYX^je!s`p#F35bih)X6y679|bbE@Y@5!ruu{M zW}C_ajNtL<{Lx3PV$}ebzVtgeRA<2XWZHhVH5sjxrkQ-%f2o2Z3^e$2$6RffP^wMj zhOA*J0)77|XOWkOdOO{mo4((X*Tie2bK_WJgLU`ArO);G#=OmDXa{rbgm3{p->mho zg|yvmFlSMHay$j)b6VnYuFMz0BwSTXotO0Bmx_wV%Io|kY^OV^D^IA|eM_|q>-3jE zyvh4EYwdlTFC5I*-Tf!{Zh2d+j+SrOu=V~R%@_ul@ZGy049mU?YDrk?oa3*3)75E~ zkd}6D%_3Zfv$KwJQE>yqY@$}!tlc6PTVXO_HW?b#mQ(S~dBmi|i)vnR|JoB6=KKw3febHKmq#}i>{G=(hEdtB7%RQ4cMv&Alf`2minQY?rjiz-m2B9NO3a1_9+D4>k zRKDd%Z(0slc&;5I5u3&WL6!`yFz#Y$*lsmfKneGl1jq|LT@3JlRC&ag{~~U0vVLnp z%Kxd&8tFW-fH-UEHHVp~oEXd~GE;Hn-~DaYTe<&*gE#^O1EvClAudSnYWazbKmDnkrZftMKwr^OxUL=H3b0^KmXw z5A1$~ATllZ`y;0UX_kGuwstG)(z;hS(YO#-FCTmG)>sYBr_-9PM&YBr>o2zUM$*(S zfH3*KqGh#qzHn((l9bRQ|24O40ZAbvJjeCLR-)l zXEc0!F$c;knG%AxdI&V_JJp0y!MzU_YvtXVa;{&*zUkL#(b%@ zxGzgRpP9vA=lNa7Qo4uBe3onwy~lM=%c$A;7CM0~G3eYF6M#_5?JMLSZmcw? z_(S{Z@Zqq>&(&JZY6+h)$S>yU+dHKUfy;BBc=tepP{!;?6O35?XPh+;W(=~@sK+ep zY`$2l5%-W?MRxdSqfwG2Kb4zf1t}Tq;vnkWVHl}tG)X~8FJ+$8keySVbN=Hc%T4n! z1G#g0!Ds)Wa}Ji5dsSS?7H@gYG{UjvLA}5#A&4VDzy$R}`dlj2Z8=osVb}1=j<<4# zvS-Y!KI&bX^Uhn=ulK#6a&wa?i8jLb>>^Y{Ih@yr&$EB-)$v`Xfx+Hl+UWm13ow_y zjhu0*!kk#W93U&ZK$i*80oeFS93MYm2QWX{ zjYgq;m6`nQk4m20o<856zwcy93)i)#ZPZ@+5c|r*lZ2^amopYo%C;`%R2g|V#Hb^f z{P6p@pdHRZR!Ai|46s4i_+8pY@Bn-NZ5c7XQNV^55p9lFRCL#)nl^VSc<^h}VZ_A~ zsmp6zgV9Y@Abd6V+_IC=R&{rkzq;C4fHcV`P&*Vjyk#SOn1_a0dj+Eq0i+5RoA!{C%mh(K369l5M`Xqb250k`g$QmwY55f-CWaheF&P9pQf89a8YoZ zYvJ#gNBYO-1WGDRN>w`D?u6vo#`x%snouR)vQ9V`n#ZX|pC?c4aE$}PI+*BdD2l<| zT2nUfZ9H+$Nkg}$8d__rXx5IL6$t9?wzbCn7OZ}It`T}y;@6=Q(4*olH9OnqdvU{j zgXM#YyDYOAa08=n7ARkK>Vv|*O?k`AWUr9zP9A}xdaZrsg= z3E2$$gvn(Hpkf9(_}!f9}KM|9|pQ1{dFtjt$JEwwJT*ts%gx!Y+4A1)Y4 zK}&A*zfNAkxfsQqWkh~eQFPiet1%HpgQU>v_ikTf8KtK zH!d3a@s*sG@~%N>w?2xxkaS-axm5%^&0w_ifNbGL&OZ}>{dW2YV)UC0D!H#?vLUsd zv#f3DynRR>t&?pQd~*!PokrB*O%wzi3pW1G@yFl+rl_H#_6xD7hr;gb^oV7YzdE~q zp};7?TKqa%Q~moKH#^h5G$5-mzxGse==iaL8!(>YJ1Vx8n}%QXvr|@p2Xp!j&7kNy zuB5RxQf88P=a1`gv0mnlUufkVXDy>`w4ML_O;Y2R-Dvq%F-&;tz7kLJ<6co{7AVp6 zyx-6hm<|tEE@{U!DRnhD>Mm-*=J@!LQOgcKlRSaA5xr@qT44pO&HeU_f;pNDc~(#sqj5+b(M&y27htvl;66=0K!HkH7%{;E#KfJfrEB zpm7qv)hI!abFbqwD~*SGZEksDDs4iR$JVJ;Ss1MlmsR>sE}z@*%Dm*lSmw6m_G`K9 zcAc$HL-eJ*i)53j)h3Wlq3W?vmz?)xIX~F1)Xf^LMrF@OARb!byjw!ss<^M&IP|NI z&rgl}xTmzh$f>ppPCptaa@6>}qWILu$mFrnT>3Pjj-bpLPVSzPtN5n0(esU9zAB9U z>)pRF(l@XB9ohWZvGTIVcS%EOb^JkO6=fI3*(iQ9m%fl$iCGGbZN1U@tK&_xn3P9L zrnm5bXS~Ek8?N+p|eD&W0xz9 zD`$;JyVBde{^8<-C7})+i=Dbz8i7#igAX=7p1!FZqoK+iVRp;4&oRNCoCC(yD4y0q zjkdRc1aCDzwQ*kljZl7}1@{Y5DYE&KV3J6>+QT4K+%{87^yD$CCS#o1!!X{H^Ie+H zrBV_?pO+knm;AmPHMII#%VT)gz7;afxXHhxYKOSMKOUr-kH1*wu@M|G(snvyK7NR= zwXdH!x^EG)i*`V(&_a~!i2^4lD4msFQ7+qCxTT8UH10dXg1T@kNO77aL!F9TD> zDyrEmxdGV#XcO*3H`$78tFeckC_c#v{Gfb|ND)#T($e-l`sj`z?O5z z&D;!}r_HII$9muBm#$TAYFB#>Iy&G!W`?wOZG=BjxfGtE`PSS=V^L~F{?)?O5rgU4 znI`l7-&fu1BdRWQ+$ETdm;~W+Iwtg8ae++HEPHc^-F2*vq-gp>d0Bw81Us9DJj-LL zYi|W??=KsnME4=EY}EU+1#)0xHQ>38ze^uC44?Gg{%t6=rb<}8Ds9Zt4X?R+je2v7 z>WY+WH_RIw&Q(|29T)kXTI^!{Egntesol?W`+F-qr}!*)^Oieaew60H8B^63+{zXq zR9G$?#o+>^+%2P%NxLtq=O^yxjZL*G{U*BKR=F6TrRi^G{j-qAyZ*ZJ-62sc#-B$m zAL33jWjj{*x#-_kouyCl20jrlc5U%_Ji2`PGJymoEQf!ofx4NgA$!J|wu|;o$F~MA zx{uuOW1nB;4Ke44J>f}>U(q>H=4gMf!6#1@yi2Im*(+O+o3>+)cqN2;Z_XEavaRdG zlQ@Rn0=PZgfAiQv)7tWRyD)nzK~BGnuw@NmQ{NHGk>z(pWV)&^SBSGU!aK8+^EHJgEJwgukHD4dY^dNJ*5 zJU>^irQ|2q)1}sg)z4g)OtbBRxoJ;+21chAqr#(4SUZ#6%Ws_Gnrj?fctR~zqwI>W z3EAn4S!4RqWn{v533DFxw)d>c+|ohbLCuSrW_KU-syLb*KDdyO@abjZNxB&`OEAJ) z!l@qtl}*QAdN}!_Q>HS4Wyd4?xb^Dv%$+I(!F*w)KRLn9ZF%2(#65uEI`6YMc(HE7 z_mK_)JByK<^srHWMW2*B>yVv)!t9VB;$E}`(t1eWaad0QJLO^LS#m?16ZPrdNDSTa zy(&e5`Dme6{p<14x+rxf`?=Bk&!M|mD$m|%BD00WkEvHO`;?a+RrpMoGESQ#8oY@3 z97LdNtyI73TYMn^9g}+fS=or5?(Q{4U-*cfD#bgP|7>jcf@_U0jlCNFM|(#G(wQ{c zWrRxem~XmvV&pC<$$3V-XmFfjO8|UnUH_#{C!B~x?DcK2q67L7#(N)?*fZv}M~e;&>R}1@dO9NBdiFz$t_-ChrUyVw z07>lg@$(}l2YHo9q>|pidrZe84*(?N`tCTBw?fi0vh_GH3_s|3B#Lft>sXhq>+UcfWPbF|^F=*U zeWv_wPWNotC4E_{#n-3`!R#y{Z`Cj|$@~+n8y%Xsf#lyc<^J%GmkiEUuTI>TOPyC& z6hqJyvh@l!iLMnypNiH4}M1!v5V4rh+i5DdQTU zjtJ+GV35DK zEbj#AJenK1OEkhMQw@8AFxvUj-HYLehDoQQY>Y!=(N3lCa*5P{>r$|Bs3cM>z9V9n zYvZS?Z}K7LcovPzBXQOR^c92@(++B<#7>|RD8;u|rW z&%ZeISUX=6C&KT8oXAkNHCJ}xQ2M7Ec^#WMo+3Ti2BI#UY(!ZG3e|)8Gvw|@&Qg9gnpM(_Z5(vcb zM@B0$3iz~HxK{UvLD$A*X=ZJ>HED@(PZNGeM9MIWMam>C7OxL&0L;qYeh8`;VMnCu z^7Le?bblW0H>>pUNRF;IM!s&U)0Dh2vX1nSs;s(gB9RtTzui2_)v8Ryap&6w=A;Xz z_PNMyEaaOnWvvwM@0WlC08KtBfnftL;5mQEVpGH9Jno9ieJD0v6gCEz-x-fw>?B&_ zOlp$dc4dPyPqt}wG|9Z@v$5|Y5gwFnYIS{pLZR^XA1CH22(s#~f7gD2PWac&{F~_l z3jTPHED7#}OZw7W0x`Cq`=F#1*!^6{^qDSIQ`c?*h@JV7ro6#QkyITQxzg#!@UFzK zxwfH@iE?U83i@86r{~tX*C60WzeXr2>OI?pm3bSa2PoE zWq2d>!)prGhmPoPu)wYHYwhMbv(SdgDje9~!7EQxkP|{z+xX~h-Yu_uqyTUepIr}c zqznnrF%=dogN3@X208W{bd4!yZnqWJ)qeFRu0HY3zpgA6H(L7TNVcg5&LL-1swXwi zUwQ}oDZWSCU98&VA$Vi=q9gRi+(L!1?7D(mJ6V1C+S7>k1{XCiZ@+d1+x1gt#!QSd z01ACL^Ztgkn2vJh^n8$MQ)p60fpsu8ATPw@u~XWpM9*aFT?n{8{werip+8R^c3voa zkOZ=}ILserguz^^ToxtUa* zLVudtSzkRGm;?h@hCKd+^|OZehY6=$=3gWB&;=tevSk|Vg!q}V1F~{&W#27xjMA2q zstFUlcznD;xY$H7COIO_inU~8dtyShfIqC|+@J|vce`nxegjgjV95?sPAhYTEUl#I zQbCU}2Q4_yr#3U*K|`0xfbS01KLR>1{p@g$gv4FF+njui4{e?9bXIDOG3q$M@3eKk zUSK^Y*IlR=bnhJG1RQTC z9mE!4K8?6FsB^KG&p2=QHz{oHQdh^`$F-7!KhZxP7C4Nj^_8-J%8Y((oiHV?%hdZK zzEa<`HXoW{1PcX68qbAWevP7Qn!kKDDs*MADm9lYyEywZf~Cd0^YVoDdckm)lPDqsc$P;;Pj9`m zF&{7OT^$${B=q##(Nm0~9i123+SYD+lT8>-Y5Zq${$e1_mBy5_^6R z6~@x{>8^h%yzfR-R1{~pqAqIxTNW}o+hlL@d@?#OFRyoRdz~&s-pHMJg0J?YgXrSV z5}74~v7P90+aXMv#Riiu+kC9Ri<5ge60zVg4fEpmFw5Tf5iU+n$LWEw{bg6gd`3+5 z+M=34oky9^PIrpOtsL%Kw{8h~?>HMdcgn@{o4#!cWy0>XE-u=HFiYvDDTHCbr-szF z`s=-SOA**dwj(Y#=moo`QF>gnis z9L*siF|>%6!qu*C?5(3~))rG!uTKB|T?wY{lxo?2G;=eC`R!?@Z17|Btg+`(Sw3B( z_wFWs5iLJf?=2PAttTfZSG~9POURe#b&5rLSiGGBa;tDwg@wr=zGyb!H1MgG5a^!q zLU8AeIrXX6U@401S^GDjFp!ODe#)<-T=nwh%MaYX>{e^Pe_+mN7~9$9t4b^hCb^j^qvjv}D@OjtBN#vR?7C066u{uBHKtl)NWRZ|BFqyuzq^J^A^8 zvwC|bLCB&#F@5`5=_V~L?HpKL-}Y=u!H|7}k$oLL+SqM^T55lzJKt^v%)(EGjpVyZ zuF|xCv**CsIaK0*50;E*>FR!_I(S$YwG#F4dis1f5<5oPA1kx!zBU3pl|jHf7{R0F zHF3gZw;l%mp?#}CK0T0~0C+Zd93iQ5=Q#&ahRBbKXX z7nW;q7_%57b0$coQ3@neIYj}x^Nr2T&H2(}Dp`ic$iT-px0KF!wF&iI|L3T#!t*u| z1W`#zwoC^(a|;U}o;P%Ofe8K};I{HuYNO-s4w2A+`!0p~VNF;b!JGlZzE3B|u%Do& zrsmYby5=@5FLj}7aY+fjiG}!(Fy`B^L%`WHq3FDKHagj344;C5LnD712_P1<@{H^B zHIb@ip_A1~60Z6RU^v+bO6k`10D&ljqffuN!ysap2!h>!E5%q<+alRLli72TKZHT( zr+6TDe*KO$_Nby`ccx1Bw}5kbrs=Pp&{8_kU5}qQk(XAv{5_mCJ13_k6hN*Yn;e-> zq+b+icr0n&8vDVxB5q+@v-jN@){WgUlJU7~4+=vkD<>zPLLP|We?LjjtN)DY%Dj=+ zYO{W|v(@09Kw@_Tn588R8K>Wy4;xY?EpS+B?jLgE;^yJ$S)2YPfefmYa_cEiT^`FJKY6T#da!{pvRMdh$RSJO%;9HyfpBxZKu*!BlVB$mDXWOiKi7 zj&yvwTV^wG3&f?af@4oi{RTUJX{b;9u!gR&aq^9t#aq}5N>*QAAKRJc^UZ2)eE`Os zJOoKlgA}d^mU-f_%m<{;mQZ?eq^h{m*zPDH*Hn~?@iZ3fTw&;w8AaPx@t1r&#_5GFYzPT`rYmYCHBAj8jUH#1sX3dwcVxBzkpZm1!G)j4$LAV1h3SfRU^_Q{{es zALkXVwx9eK5HD;Sj|fg*gUrq<9}LESNfx^;X>iie?h8&~j*Wl)dIw~S>NOiam3YA) z;&%pregEE}=CgM0&7CV?(=LiVPd9H*w1Rv@v(eXa)O|K%Nq-UGWOim|i#ViqMqK!J z>vvL%OjO(HX=yE2#=l_ryGCRAO`TVp&l{i|dLr1Bb6GDp8##QTuHTYRNIvf0RJ~L_ zlxrG0P-N;)*>^E=O>(*IAE;a|7c(V6|}#G#rv_tS~D@UXa8Fr%2pY_exQ;w0+OtJ_BI(@|tk zx?%YF@l8vnrq;qIP0rmaB6_Hs&HIoq%^jsINd9cVYq%?HcfKb#MO6S<&7EKQRs3Zd zJ&*3k+*}^)1x0{$r@8^?*@8iMD~3}YL~WRwXvr6gQ)KP*@V9Rd%cZHKQZAkL7cy_X zfJ%YCIUwrZnk+S>!cE!P6yJKp$@I$Rpz{U_H~B$WLzxM~-&%A4d{7p~M5$Z4 z-Wec$Q6ZO*z^`C;0+G9;W-$bh>anQe_1QYIUFq6%BkFseh! zpAR)fg=SS(4^phf_-Ap}*4BpZ`D4qirw+fWukIG@O+sMwFlQKrKyLK${iFcWWU0f% z9qfhVpJ;R+U;t)JO^nzS;5Z!@$-18`gCq7(A&btn-POJi+3liJC!sjH5UO8l84kr+ zf!8#*72^DIl#X#$ZLK6Q8QDPparGzIvZM?#UPIf*ryj7BJX6ty;$Js3LCOK!QRuV3 zGynAjUuoz>+%LX{VevZ}<3vcacRu`Q{=@oIODIG+2!QDinyN*i;?L zZV;5tpFa=Z+gio&M|=%slE`HR7hk5Pa3rg5AqU)mk1q5Tq)eg9gsr;Rs1HzKFKm?@ zJ9kZ2!C_da&(M^KigS5Ag;c95z5NV(A;0E#1n^+CkdYc!12Qfi|442gbiKPoT{!k= z-6vP4z?h4Rt2p$9%=#4P`sx%44zdj7YI{B@&hqkaZ`5x+1rRZ@wK|oZn_EgoKw4P} z0)|&ti=$HJQBRjmBd@6xhx*A}0w{|(q_b65`)c<_)$gw%( zQ(z(kDB(kkM8~%?Z3)O12ahnY?-_u~NmG>Z5!>0K(8Y3VB54*dD! zM<+-{WzVPYW3T6nVslG!SxY&Y!*cTTRS^gTL|xa`*Nu`YoMu&ZQb2nbMxyLcaIRj7 zd|6o;hTo$VOYYV6y7dQ8A0t+86ZxoxhzJjBcC?n=2^@)vyu{}_IYxGYx2O-A12@qpca5OBYYh>}+`t+#4I4>gN>{5Tx z)NA|u#||Cfx_kLBH9}p+|($|mA z&CCFOayAKAsHcI5ZO})Bvz@wdv(c5hkd4={@?pdNl097*DlhG_T*xI6RRQzX4nX2^ z(?q%W_|%Ze9@&VKN?m|1j4X^q3E_cZwWVvpuu=i=&06mu$dS<=T|SA3!z&9iDm<<)Z0;<|u9xA@yp zx2aS6`(q^UFr6|M36}@(zWn{)Xb-Tr5a+h)&a?vV3Nd0p6^8b<$t~gk+$0~FE0>NQ z5(JTAJ6by+9+;Iv+(0U+!vO0T5`PH?dHEhyFms%QYyRQWjGsY%0CsDIvJ$H$7L&I# z*jJwkpb(Y96mSo$`I_xusZy81o0X0EZXwT2o3`YQxwtJ5pya;`AY}j$YyY`66yuuYS83Q zQvq-wIxa3NC%^iq_}ernjJvn-cs#^OjW~T}l-bU9RwJ+*Q!FH1r`h&i;HAz(wiV#; zVA)`?SrErXaKmvLN{geq?bq{-rHU~OL~&`Pw;D_0B1mdtt{8YhojKF(35R~L2mgVX zijF!)h^RYPe0e-i06)BOL%3}PAc^5F0Vk5+JKji@r(bs8gtXI#XgVblJ562~1?D?= zSqc<3f1+GjmWQjlOXALQ_{q#5R*8ChUI%m|>er$QwyJ->-36siu2Z`hO8hNrU|?Vm zQjMi#TOo|OH(NWWbp{e@q$onoM+5L{$+}1cuu?=isQ1w3_q%{gMTLJc;P|EKz_PwO z=1<(;#gdU0aM))*&SH;-7E15$_M3)ro(+gK2ZO17#x`IGP^Q6! zcR^*5Y$5{9Ni={=o@FcC^a6#2gysRQj7Tpoz6Fvk+FW_~AP+lgeh9=e_Gq$hrNbvE zA%Q$1IgN|F!NqkeVSfi(kkAIvmqZEze>AA&@l#e-j#6Y}Qwmr7wvPCg2vZF`AbRnZ z+eDW#AAZBfc1sQ52?fQap6bEuN2+^mguxKwhbUsIQ8cLAK_%}djO)%3tusDoWN5fJ zU;FGsT$~ltZD63X(D8ubVM8OA0W&BstNSAlXfzKt~gil)^ZvY#piIXlcE zIn*9q{%6I;*ve}D7>P8s>sNQ{+4q0CZ(Y6k%)-JzVP)@kMFzWawA+rwNupDi5{DJ2 zxFbf#c;Q%>t`rTOfU|&P3?Rj_ukGl3Cc6___*;6A}9|i+4 zI>A>lEr4Z!3d;Z`8%UUjYs5+95D{EQ6{P!{j>r z30#Q1ZgdM!^pb%@BH(d;ER-BApX{?^)n_pbuoy7?tSNMoqK1>{ zxyE@Pn!*@YX`d{!5S@~Ek^5-8)_%r&9LOC!yn!2X2W-d=xkdHI$F{8-<-ggr0lnf-$!nMTw&v)rV2~(TKf?%=9Kx$s@h_Grfsm+y@V{rU7(bj z9qjX72l}xlHD!E0t3f;4Xy1IG&y<|>Kh4y&gU7bbaJ-Ve{1FglY^cO?VdzU*7a&G0 z^feI6^bHL1(wx7q)#F=QTI@)}S(?ZN&#~QPaHHAt4O`?cn{2!(HPn;zQ~d>qEhy@w z5?p~HfkNUtgF4M5o}QU`4PC5Aw0ee)>aFbj%PbB~Qq1;7DR z1lV4In&1wE{)aXC8($n0a$PpWUPuDifV^}2G`NTJYjVW}6SwV3S4wV&<388}GGJwu z1tB7O?Id6UZ{EC-(mxY_@|s$*P4S(Y9H8)(J#9V?h~lS@A7#%QIfp~#^`C^^S%Q{^ zGo{_t)6+9AVGVybU;}Q_$eBoHq8(O0iiz8%H7-kQ*MFF}8NEc7WGKg!yC0Bb+TTLS zV1VXgTODyJ%eE!>qQ~x1MHy5`U0*u>X0e_7UYh!U^<;fOtLTxs`}K##^*+SJU`IW1 zWuf6(K&}KNkM8K9TtI*TXP%y&<=7W@%%STDxc8E-1I%WHngWJNkI3K`dVD-odik&_ ztcEXKaqfuB7FCY!3qiB81JxNB&ChOt3&sWiTyXaI8$r8aQ8fz}=dXQFK~`XQ6W1@Z zj>6;{fhLaMnHVMF1gFiVA*3$PjCyJqU>?`z`;eK% zAnA=bqL@I}>q0TtQ-NDsx1hYf_kvUm^;bGL>9HwOt8O(Z=K}6IgUlRu)|*X0Kojr} zyt8@r=j`mR5RuW(P#(DsJE=$4`03&I|2pYgj+6J)W)Yh{CP6|S!v_~@fxdakzP9tX zu?2_CK(U-fig&rO*Qy+3K!_C(=TnG3#NSp~3R>S10lds+AM(Y2&&nj-0=Nl=b0*pA z$e@+Lc)7YfnK-Xm337~lu!93FRZlN2`i7A;6E)`|@LrF#9~>(6MeK39Nsz_}&_*y0 z(&zt8kVrQp50`RSktqKH;^MLeFE8&-yKDVaLSzY08&&V0A1I7bWOK&V5%f_v%@7B4 zpPqsywD*FNUi~4&0m_@3cu7ffz8uveUfia^+S0N^r(s7MBJLPT~YYWhOw9ZokUqw&t(BQK#XxVWYJ-gF}?=vaDWBY(o zFs#g12PJZ-z7AKsm-8sEIP^(oQBkLjFnO3gzdtjpH70ohi^*LLSM|07+SCsvv1O{| zDo{A;DLBZ1`vlT)=bBz_9`PmZClO~qj%n%WB(yDeBg6hg4Tai?|HdC1ZyzP?@&{=) z8aG_?5rl&HXukW_gujY1PkKF2tM?~pe1UwD3KFD^3yHYjMoH3tINOy1wVLSYXqWkQ z-s9sxA~`&zyY1Vh2!><%2DaALT}?DHHc;9aC^oy#b{0tI7J9H8sxqGBY>V2L;+-l@k(^X8_d&dJ3zwZ}m^Pkj0$)BZg zh!LU}t*LAr4~PQY6_i=YXnX<~gcGQjD2Fy6z~uqq2MDdA#cfxjw6wM3+RVuSyyr&& z$H8fk=m3u}B2JP(KcHuLI2XWujD~kP*i1O69+KOg@7UVr-M@cddYcp%7Z(qz45jGX zuMR_G2jc2KyejGlqn?A6j%h$ed;%f^q&7>i;nnNy5_xHEGXR^-OFW-Xf2qOM=Rlyi zy$aIkT&nz~8wJpA6Qk1IB=aG@T#~sU)p&%4xkqNNNhWc{w4in^P(Jv= zlL(8^TK9ThaAL@{VV8;erpIqkM0LrK)@9)1jW00jl8!6mw@U`5P+_=Dv^U@o_?ZNI za95BlDW^1&|1i881v;`}7X{>jLh2jiI|>M{kz#kf7O$U-vXW8&BzNBO1hv2pnzW}D zIbiIzJsneKflg*l;<@J$bnhyJkHl3LQm1$z$IoBCfIv@o+?`~W12!j1Ekzo1hwxrx zib_iXSz87{%lET{EA;gA*@cB2-EkvA0C512v!6YNy)Feb{sM5E2y_a%`-@C#5IYid z-=-Y%bc^m(jQP|6QdPK%YcLTRagGGdj@-1$8}vay+c5+>m_VG2&)$}c+8U(Z|G3da z?r{12o<^R_x0?fXtKRL2QV`(W3?cDtDI)gC12%k;Yu{Oo>W3J%;6Tsxm<-^^HFaxQ zV}4v90tr1Z)ig?0y9P1{S(|*xy0EaY0cyS2wiMt2U<5%C5$q>_W+1uHx&j2-c{(L) zu{cuGXCqA50Q0!qOF6k=d5+ov1xZY-qUIyj2=fv(DZEysXJTC`NVIZS&p=R90^AF2Q-3~`MJ68z z;Xn`77$*C>+t*^)GFY{$%)WEUHdu_T8f7Y54<;OR)8s>nL&G=riotQ7QE!4#x2HA8 zv-%Mr#>|20WCvuJ!E)OKAV}Db)sHxjAjJ39!G}6wM2cKKE})Vqs{ZA12avEYfd-TZ zegSB6XI9+|3mS;k1A7APqIW@W%6z@9(0kXVyYB+={b{5h7K<(OCHa(WpP=V?2$1)` z6pG^l+w;$I5Ez^u zPr`YjDBE+6vOUs~Da+-EgcRc(1Qt`|(rX4qi_6lW8Gygsj0`!#h-(V8C{PzT69HTG z@)Z#S%i5!qa*Y;inpyM0GD*=l`1tzTlO)<^(4dkB>bxzfMycsg3vX@$tE) zmDXUBGS?5j@bkgvn9MJhpoQDWe4dVyYZ}fRJ>;crUEOAHRyEBq2a2w?M6qt0dYxpq z$zzb9g_1oEgeweaSUzQH(w1;pyh(C8d)gm?mLkl77MHe*OSu(vTK!)sW-kOlQ#J@G z&{b+lSBm@)K9N$P1HefjNydIuvU}V3-My+93Ls!yJL0^)ksCA-Sot|N$;WRC^T{a+1xK}W2hwQxQo ziyrg94V5`kyoxQ>PqRqd_yo>%-)Pu-E+it7(CV|0h14=J8Jj_g?GFiwiHSiC$<5^v zI8*Z((6-cIJIim-?A0zdd;P*mF8ZLjR>IGNG?E_^?5ckdxCBtJi&fe z&!qIUnaksM%R|ZO8$wW}vCv1=Xvzw|5=jf1r;e1XOBBJ_I!t#t}^QeG9o{ zdHfV$pFU`{TCCeuP18$l|Bg&m&ir7hRpbw^0xc~qa?_4nbOELVU^WS0^LZ05bqI>?eEPj}_7NzW zgiKpbk`E#Q_d$o=f-J_Qt*wnY6Cn?nwymvgP0Cb-gl*k;MPDnx7EURQta6tWVL0vH z)hEC$7XiKNkigZNGBpW-6f{`)wZpnTer&uQ+DHSPA~ zQYx*qw5QfaVZi{D20*BFui}TAM=J-(K$}htj#bRfm3?n(>jlb`)cq+IA8tR_<>9Qm z{-@*G__(;%M&N_dX1}8K6l4QEHwP?KNVU~Vpf#!ksjrav(@#=HdJ`YdfIX^|mWD?I zX;?%4usvP1_4oHzfGl4TI`J83p55E?&yQ~b3f1S&;$sO`#lsL{J^d53ISe&TObTL* zJ${h2|M$1HvZ1QPi;q4Cq3e+u2@V3d$QT2ju71`RD5muWNo@?4_H1X5sZyn(m_6pM z0ASVwIUMX4ki)^1iWKR8r4G!gc?z5W1Jk1G@ZsjK`&|*$%+Agp*!*a&MOo@cesS~RqN>#w*h8GrugVr zjJPDEp%ZVJ1^y7%YM^Tb^k}dekt0p0LdThj0`gkwwf!8RQvfCIoPY6F0GawYWRCo* z83uAwFTWBregr{dlk6h0T~p+KoL52Gxq|u3x&xxDtWE0&+<-y=baTfI-vkkRVX>-( z3ZRUDCc6;W*J1$6%w8+EL9_uuOlBM!=Q2(B)bc z5#WEeJ~rRpo%z|;hQArI0cs~PVk;;smj+5utFtc8KwIMYo4qp2tn~D6&cvQ9O`*q) zuNHZ zHU6`t!ggrpr_<0m&}ah85j|TypX55qFC~`}9{x$pX$Dwh5agz+5=N>|`uz*5#W${G(}6tguc0%hSSNY5hYuP@p%#MLmmfP0h^m`Xupx?YbuHhzv>lWNgi3EN^l-@dYQ z+Kf+k?4YF6%mcJeq`{h}`w>Fj)@0Bi*kO<(K14;$$29C^P^M+|XIg_b>%vTB1P>ra z)aZ5D4V9||-1}NKThVOK0hm~?j)_nb3s^o7l)+{~v0QF90uFI<^3VraH3!mx^_Dr$ zYvdW#U?}_f;m>|1!lcy=xQli-=F`BL4Y&B_wsVc3VIl4??gvNP2eF(O2YPZ2hcPj)o+_GAA}@J={JA&^!QxE=bRi(L5BU-FuL>SJ>xR~83V^^ToH!}FbNC5 zbu!nXMO_pyk2PP>fWvcuS4ji9C1zUA1|p%s%- zg8tS#aV$(P5f;5yb=U)Bh#6|->bw^|D_@TELRu|I>)?hUWhMfXLAgz2%&P?V%MQ%( zPu?8&9H!bkzf?zWoT86UO)L|qT0|YF1O>Z&{dT6+TTmvr*JloEfb&Pp-W%%VnL%>h zcmMz`rLi4MMFcV7+STxPn~MXcg~*+ zwE>b9!n+;0T?C6>#8EMeI($?Gj1TyPLAg!b)1p|Y!RR{XtpieIap(m3UM9q~m@?40 z!d_3A2Y_9@ke%Z^@#cK|Ud=JEn)z3h9e<>$NlqNBR;7poZX0u*ppBhuzG^L1(#$!| zZ`yJlFvj&NyOJB=GXSbo$jJR&0vzO5C^)*NX$#q+qN5i;tG@%B!O2Za90$p*6FNs= z3xont>k@{Y!VqlWc3YH|x>v|9Ktv2y9U!z-D0Jpa(yb#>kDdUtFf+4S>@NbIGXz1t zkM+{a6E|)q*BDtF<;NBhOm>a#PKqv5P z!z&iIajJFSWI&Z08z`XwA0~lMMjD3&oa69_Z~CR-0-QCuZ3k{VmbLii-qlY~wmAv- zYgeV?)B^#xB`AG@nlsPz@*9v3@(ioG#NW;W!6s)NEn_0HJAi(W|4J0-vH1-|52&pM zSP_R;ux2Azq{BHfRrAWrld$jBHYo7#bH#Y*{nV#xG)~Fh{0Uk!pmzj726ILd#rb%n z2jq_UXaj!W3}ni8!er~EUColP+VT-QTiafU)P zR#|?2U)kI>AuJAbPt1OM${#uAE5*rZ?2Xr6ml4?AT*CIu0=?8d8V~#?Uz!At8FjzE za#xb%4djUz;FDj)#TpEAvKLsP)?sgrtG$|eRaIGPR+du5s7Fpq7z=JN82z;9EvZCM zSlAl$aG;tlQzao2IyeKU(Q{{mCLna-x2D3dUyiUW_M{|1J<8|5c*Oz1V)}Ji38XET z4R>gzQZIn~!Jc~;S}r)KwiCv8ehzN2aVu^goSW9K|McaVutw9z+pnAfnYRX6h(W?7 z7t;H|xiTOkl<%$s0ksUA#{+eT5w3w$KkBuv0v&-1IAj7z9)41kk1K%EiWi(w#Da5R zq7$e)pWd&nt@VJFfuk|kMMQK!i4I~@^X}bOpkM<8iXcMRA(w?O$x#GmwpC&&m<>3D zj?Qx^ecG*Uu3ZL?f4=^i1PC9tABUtf5uLbPiyP?Qx-)OEYx|sGjmwQ zJf^Q%YlK#WtYpF-{a1JRbN-leiS(Z%>Hh;?t&j#w4nk_R-)$m>uyvmmJUd1?fDiui z!4*Gone.el \ No newline at end of file diff --git a/one.el/docs/public/one.css b/one.el/docs/public/one.css new file mode 100644 index 0000000..892d876 --- /dev/null +++ b/one.el/docs/public/one.css @@ -0,0 +1,499 @@ +@import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400&family=Noto+Sans:wght@400;700&display=swap'); + +html, body, p, ol, ul, li, dl, dt, dd, +blockquote, figure, fieldset, legend, textarea, +pre, iframe, hr, h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; +} + +*, *::before, *::after { + box-sizing: border-box; +} + +p, blockquote, ul, ol, code, +dl, table, pre, details { + margin-bottom: 16px; + margin-top: 0; +} + +ul { + padding-left: 2em; + list-style: disc; +} + +ul ul { + margin-top: 0; + margin-bottom: 0; +} + +ol { + padding-left: 2em; + list-style: decimal; +} + +li p:first-of-type { + margin: 0; +} + +li p { + margin: 16px 0; +} + +li code { + margin: 16px 0; +} + +html { + scroll-padding-top: 4rem; /* because we use a sticky header */ +} + +body { + background: #151515; + color: #dedede; + font-family: "Noto Sans",sans-serif; + font-size: 106%; + line-height: 1.5; + word-wrap: break-word; +} + +h1 { + font-size: 2em; +} + +h2, h3, h4, h5, h6 { + padding-bottom: 0.3em; + margin-top: 24px; + margin-bottom: 16px; + font-weight: bold; + line-height: 1.25; +} + +h2, h3 { + border-bottom: 1px solid #1d272b; +} + +h2 {font-size: 2em;} +h3 {font-size: 1.5em;} +h4 {font-size: 1.25em;} +h5 {font-size: 1em;} +h6 {font-size: .875em;} + +a { + color: #ffd787; + cursor: pointer; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +a:visited { + color: #ffd787; +} + +img { + width: 100%; + height: auto; + border-radius: 6px; +} + +/* ------- '.one' classes used by 'one-ox' org backend ------- */ + +.one-hl { + font-family: 'Fira Mono', monospace; + font-size: 80%; + border-radius: 6px; +} + +.one-hl-inline { + background: #31424a; + padding: 0.2em 0.4em; + margin: 0; + white-space: break-spaces; +} + +.one-hl-block { + background: #161f22; + color: #c5c5c5; + display: block; + overflow: auto; + padding: 16px; + line-height: 1.45; +} + +.one-blockquote { + background: #202d31; + border-left: 0.3em solid #31424a; + margin: 0px auto 16px; + padding: 1em 1em; + width: 90%; +} + +.one-blockquote > p:last-child { + margin-bottom: 0; +} + +.one-hl-results { + background: #202d31 ; + border-left: 2px solid #c5c5c5; + display: block; + margin: auto; + padding: 0.5em 1em; + overflow: auto; + width: 98%; +} + +.one-hl-negation-char { color: #ff6c60} /* font-lock-negation-char-face */ +.one-hl-warning { color: #fd971f} /* font-lock-warning-face */ +.one-hl-variable-name { color: #fd971f} /* font-lock-variable-name-face */ +.one-hl-doc { color: #d3b2a1} /* font-lock-doc-face */ +.one-hl-doc-string { color: #d3b2a1} /* font-lock-doc-string-face */ +.one-hl-string { color: #d3b2a1} /* font-lock-string-face */ +.one-hl-function-name { color: #02d2da} /* font-lock-function-name-face */ +.one-hl-builtin { color: #b2a1d3} /* font-lock-builtin-face */ +.one-hl-type { color: #457f8b} /* font-lock-type-face */ +.one-hl-keyword { color: #f92672} /* font-lock-keyword-face */ +.one-hl-preprocessor { color: #f92672} /* font-lock-preprocessor-face */ +.one-hl-comment-delimiter { color: #8c8c8c} /* font-lock-comment-delimiter-face */ +.one-hl-comment { color: #8c8c8c} /* font-lock-comment-face */ +.one-hl-constant { color: #f5ebb6} /* font-lock-constant-face */ +.one-hl-reference { color: #f5ebb6} /* font-lock-reference-face */ +.one-hl-regexp-grouping-backslash { color: #966046} /* font-lock-regexp-grouping-backslash */ +.one-hl-regexp-grouping-construct { color: #aa86ee} /* font-lock-regexp-grouping-construct */ +.one-hl-number { color: #eedc82} /* font-lock-number-face */ + +.one-hl-sh-quoted-exec { color: #62bd9c} /* sh-quoted-exec */ + +.one-hl-ta-colon-keyword {color: #62b5e0;} /* ta-colon-keyword-face */ + + +.one-hl-org-code { color: #dedede; background: #31424a; } +.one-hl-org-block { color: #c5c5c5 ; background: #31424a; } +.one-hl-org-block-begin-line { color: #c3957e; } +.one-hl-org-block-end-line { color: #c3957e; } +.one-hl-org-meta-line { color: #8c8c8c;} +.one-hl-org-quote { color: #c5c5c5} +.one-hl-org-drawer { color: #d3b2a1; font-size: 0.9em; } +.one-hl-org-special-keyword { color: #c3957e; font-size: 0.9em; } +.one-hl-org-property-value { color: #d2934a; font-size: 0.9em; } +.one-hl-org-level-1 { font-size: 1.7em; text-decoration: underline; } +.one-hl-org-level-2 { font-size: 1.4em; text-decoration: underline; } +.one-hl-org-level-3 { font-size: 1.2em; text-decoration: underline; } +.one-hl-org-level-4 { font-size: 1.1em; text-decoration: underline; } +.one-hl-org-level-5 { font-size: 1.0em; text-decoration: underline; } +.one-hl-org-level-6 { font-size: 1.0em; text-decoration: underline; } +.one-hl-org-level-8 { font-size: 1.0em; text-decoration: underline; } +.one-hl-org-level-8 { font-size: 1.0em; text-decoration: underline; } + + +/* -------- scrollbar -------- */ + +::-webkit-scrollbar { + width: 1em; + height: 1em; +} + +::-webkit-scrollbar-track { + background: #202d31; +} + +::-webkit-scrollbar-thumb { + background: #31424a; + border-radius: 0.5em; +} + +::-webkit-scrollbar-thumb:hover { + background: #31424a; +} + +/* -------- specific to the default render functions -------- */ + +.header { + color: #ffffff; + font-size: 2em; + font-weight: bold; + padding: 0 16px 0 16px; + background: #151515; + width: 100%; + height: 3.5rem; + position: fixed; + top: 0; + left: 0; + border-bottom: 1px solid #1d272b; + display: flex; + justify-content: center; + align-items: center; +} + +.header > a { + color: inherit; + cursor: pointer; + text-decoration: none; +} + +.header > a:visited { + color: inherit; +} + +.content { + margin: 3.5rem auto; + padding-top: 1.8rem; + max-width: 740px; + padding: 0 16px; +} + +.title { + text-align: center; + padding: 1.8rem 0; +} + +.title-empty { + padding-top: 1rem; +} + +/* -------- one-default-home -------- */ + +#home { + margin: 5rem 0 1.5rem 0; +} + +/* -------- one-default-home-list-pages -------- */ + +#home-list-pages { + margin: 5rem 0 1.5rem 0; +} + +#pages ul { + padding: 0; + list-style: none; +} + +#pages a { + display: block; + line-height: 1.2em; + font-size: 1.2em; + color: #dedede; + border-bottom: 1px solid #1d272b; + padding: 1em 0.3em; +} + +#pages a:hover { + text-decoration: none; + background: #31424a; + color: #ffffff; +} + +/* -------- one-default, one-default-with-toc, one-default-with-sidebar, one-default-doc -------- */ + +.nav { + border-top: 1px solid #c5c5c5; + margin-top: 3em; + padding: 2em 0; + display: flex; + justify-content: center; + gap: 0.5em; + font-weight: bold; +} + +.nav a { + display: block; + background: #dedede; + border-radius: 6px; + padding: 0.2em 0.8em; + color: #151515; + width: 20%; + text-align: center; +} + +@media (max-width:600px) { + .nav a { + width: auto; + } +} + +/* -------- one-default-with-toc, one-default-doc -------- */ + +.toc { + display: flex; + justify-content: center; + margin-bottom: 1.8rem; + color: #d1d1d1; +} + +.toc > div { + padding: 0 1em; +} + +.toc a { + color: #d1d1d1; +} + +.toc > div > div:first-child { + text-decoration: underline 1px; + text-align: center; + font-size: 1.2em; + margin-bottom: 16px; +} + +/* --------- one-default-with-sidebar, one-default-doc --------- */ + +#sidebar-header { + color: #ffffff; + font-size: 2em; + font-weight: bold; + padding: 0 16px 0 16px; + background: #151515; + width: 100%; + height: 3.5rem; + position: fixed; + top: 0; + left: 0; + border-bottom: 1px solid #1d272b; + display: flex; + justify-content: center; + align-items: center; +} + +#sidebar-header > a { + color: inherit; + cursor: pointer; + text-decoration: none; +} + +#sidebar-header > a:visited { + color: inherit; +} + +#hamburger { + cursor: pointer; + height: 1em; + fill: #dedede; + display: none; + font-weight: normal; + margin-right: 0.3em; +} + +#sidebar-content { + margin: 3.5rem auto; + display: flex; + margin-left: auto; + margin-right: auto; + max-width: 1140px; + width: 100%; + padding: 1em 16px; +} + +#sidebar { + border-right: 2px solid #31424a; + top: 4.5rem; + position: sticky; + padding-top: 2.2em; + padding-bottom: 6em; + width: 250px; + max-height: 100vh; + overflow-y: auto; +} + +#sidebar a { + display: block; + color: #dedede; +} + +#sidebar a:hover { + text-decoration: none; +} + +#sidebar ul { + list-style: none; + padding:0; +} + +#sidebar li { + padding: 0.5em 0.6em; +} + +#sidebar li:hover { + background: #31424a; +} + +article { + padding: 0 1.5em; + max-width: 640px; + width: 100%; +} + +#sidebar-left { + width: 0; + height: 100%; + position: fixed; + z-index: 3; + top: 0; + left: 0; + transition: 0.25s; + background: #2c444f; + overflow: hidden; /* to make the children disappear when width is 0 */ + overflow-y: auto; +} + +#sidebar-left > div:first-child { + height: 3.5rem; + font-size: 2em; + font-weight: bold; + border-bottom: 1px solid #b8b8b8; + padding-left: 16px; + margin-bottom: 16px; + display: flex; + align-items: center; +} + +#sidebar-left > ul { + padding: 0 16px 0 16px; +} + +#sidebar-left > ul ul { + padding-left: 0.8em; + margin-left: 3px; + border-left: 1px solid #b8b8b8; +} + +#sidebar-left a { + color: #dedede; + text-decoration: none; +} + +#sidebar-left li { + padding: 0.5em 0; + list-style-type: none; +} + +#sidebar-main { + display: none; + top: 0; + right: 0; + width: 100%; + height: 100%; + position: fixed; + background: #080808; + opacity: 0.80; + z-index: 2; +} + +@media (max-width: 840px) { + #hamburger { + display: block; + } + #sidebar { + display: none; + } + #sidebar-content { + justify-content: center; + } + #sidebar-header { + justify-content: left; + } + article { + padding: 0; + } +} diff --git a/one.el/one-tests.el b/one.el/one-tests.el new file mode 100644 index 0000000..b4eae5c --- /dev/null +++ b/one.el/one-tests.el @@ -0,0 +1,819 @@ +;;; require + +(require 'one) +(require 'ert) +(require 'cl) ; flet + +;;; macro from org-mode repository + +;; testing/org-test.el +(defmacro org-test-with-temp-text (text &rest body) + "Run body in a temporary buffer with Org mode as the active +mode holding TEXT. If the string \"\" appears in TEXT +then remove it and place the point there before running BODY, +otherwise place the point at the beginning of the inserted text." + (declare (indent 1)) + `(let ((inside-text (if (stringp ,text) ,text (eval ,text))) + (org-mode-hook nil)) + (with-temp-buffer + (org-mode) + (let ((point (string-match "" inside-text))) + (if point + (progn + (insert (replace-match "" nil nil inside-text)) + (goto-char (1+ (match-beginning 0)))) + (insert inside-text) + (goto-char (point-min)))) + (font-lock-ensure (point-min) (point-max)) + ,@body))) + +;; testing/lisp/test-ox.el +(defmacro org-test-with-parsed-data (data &rest body) + "Execute body with parsed data available. +DATA is a string containing the data to be parsed. BODY is the +body to execute. Parse tree is available under the `tree' +variable, and communication channel under `info'." + (declare (debug (form body)) (indent 1)) + `(org-test-with-temp-text ,data + (org-export--delete-comment-trees) + (let* ((tree (org-element-parse-buffer)) + (info (org-combine-plists + (org-export--get-export-attributes) + (org-export-get-environment)))) + (org-export--prune-tree tree info) + (org-export--remove-uninterpreted-data tree info) + (let ((info (org-combine-plists + info (org-export--collect-tree-properties tree info)))) + ,@body)))) + +;;; utils + +(ert-deftest one-escape-test () + (should (string= (one-escape "<") "<")) + (should (string= (one-escape ">") ">")) + (should (string= (one-escape "&") "&")) + (should (string= (one-escape "\"") """)) + (should (string= (one-escape "'") "'")) + (should (string= (one-escape "regular text") "regular text")) + (should (string= (one-escape "<...>...&...\"...'") "<...>...&..."...'"))) + +;;; one-ox tests + +;; (global-set-key (kbd "C-") (lambda () (interactive)(ert "one-ox-section-markup-plain-list-test"))) + +;;;; headline, section, paragraph, etc. + +(ert-deftest one-ox-headline-test () + (let ((get-headline + (lambda (rv tree) + (car (org-element-map tree 'headline + (lambda (e) + (when (string= (org-element-property :raw-value e) rv) + e))))))) + (should + (string= + (org-test-with-temp-text "* headline 1\n** headline 2 +:PROPERTIES: +:CUSTOM_ID: /path/to/page/#id-test +:END:" + (let* ((tree (one-parse-buffer)) + (headline (funcall get-headline "headline 2" tree))) + (one-ox-headline headline "
contents
" nil))) + "

headline 2

contents
")) + (should + (string= + (org-test-with-temp-text "* headline 1\n** headline 2 +:PROPERTIES: +:CUSTOM_ID: /path/to/page/#id-test +:END:" + (let* ((tree (one-parse-buffer)) + (headline (funcall get-headline "headline 2" tree))) + (one-ox-headline headline nil nil))) + "

headline 2

")) + (should + (string-match-p "id=\"one-.*\"" + (org-test-with-temp-text "* headline 1\n** headline 2 +:PROPERTIES: +:no-custom-id: so a random :one-internal-id id is set by one-parse-buffer +:END:" + (let* ((tree (one-parse-buffer)) + (headline (funcall get-headline "headline 2" tree))) + (one-ox-headline headline nil nil))))))) + +(ert-deftest one-ox-section-markup-plain-list-test () + ;; section, paragraph, plain-text, bold, italic, strike-through, underline, subscript, superscript + (should (string= (one-ox-section nil "section" nil) "
section
")) + (should (string= (one-ox-section nil nil nil) "")) + (should (string= (one-ox-paragraph nil "paragraph" nil) "

paragraph

")) + (should (string= (one-ox-plain-text "<...>...&" nil) "<...>...&")) + (should (string= (one-ox-bold nil "bold" nil) "bold")) + (should (string= (one-ox-italic nil "italic" nil) "italic")) + (should (string= (one-ox-strike-through nil "strike-through" nil) "strike-through")) + (should (string= (one-ox-underline nil "underline" nil) "underline")) + (should (string= (one-ox-no-subscript nil "foo" nil) "_foo")) + (should (string= (one-ox-no-superscript nil "bar" nil) "^bar")) + ;; plain-list, item + (let ((ordered-list + (org-test-with-temp-text "1) first +2) second +3) third" + (org-element-at-point))) + (unordered-list + (org-test-with-temp-text "- first +- second +- third" + (org-element-at-point))) + (other-list + (org-test-with-temp-text "- first :: description 1 +- second :: description 2 +- third :: description 3" + (org-element-at-point)))) + (should (string= (one-ox-plain-list ordered-list "contents" nil) "
    contents
")) + (should (string= (one-ox-plain-list unordered-list "contents" nil) "
    contents
")) + (should-error (one-ox-plain-list other-list "contents" nil))) + (should (string= (one-ox-item nil "item" nil) "
  • item
  • "))) + +(ert-deftest one-ox-code-and-verbatim-test () + ;; code and verbatim nodes + (let ((code (org-test-with-temp-text "before the ~inline code~" + (org-element-context))) + (verbatim (org-test-with-temp-text "before the ~verbatim~" + (org-element-context)))) + (should (string= (one-ox-code code nil nil) + "inline code")) + (should (string= (one-ox-verbatim verbatim nil nil) + "verbatim")))) + +;;;; blocks + +(ert-deftest one-ox-blocks-test () + ;; `one-ox-htmlize' + ;; note that in `sh-mode', `echo' word has the face `font-lock-builtin-face', + ;; and strings have the faces `font-lock-string-face'. + ;; normal blocks + (should (string= (one-ox-htmlize "echo \"Hello world!\"" "bash") + (concat "
    "
    +                           "echo "
    +                           "\"Hello world!\""
    +                           "
    "))) + ;; results blocks + (should (string= (one-ox-htmlize "echo \"Hello world!\"" "bash" t) + (concat "
    "
    +                           "echo "
    +                           "\"Hello world!\""
    +                           "
    "))) + + ;; `one-ox-src-block' + (let ((src-block (org-test-with-temp-text " +#+BEGIN_SRC bash +echo \"Hello world!\" +#+END_SRC" + (org-element-context)))) + (should (string= (one-ox-src-block src-block nil nil ) + (concat "
    "
    +                             "echo "
    +                             "\"Hello world!\""
    +                             "
    ")))) + + ;; `one-ox-example-block' + (let ((example-block (org-test-with-temp-text " +#+BEGIN_EXAMPLE +A simple example +#+END_EXAMPLE" + (org-element-context))) + (example-block-results-1 (org-test-with-temp-text " +#+RESULTS: +#+BEGIN_EXAMPLE +A simple example +#+END_EXAMPLE" + (org-element-context)))) + (should (string= (one-ox-example-block example-block nil nil) + (concat "
    "
    +                             "A simple example"
    +                             "
    "))) + (should (string= (one-ox-example-block example-block-results-1 nil nil) + (concat "
    "
    +                             "A simple example"
    +                             "
    ")))) + ;; `one-ox-fixed-width' + (let ((fixed-width (org-test-with-temp-text " +: I'm a multiline fixed width +: yes I am!" + (org-element-context))) + (fixed-width-results-1 (org-test-with-temp-text " +#+RESULTS: +: I'm a multiline fixed width +: yes I am!" + (org-element-context)))) + (should (string= (one-ox-fixed-width fixed-width nil nil) + (concat "
    "
    +                             "I'm a multiline fixed width\nyes I am!"
    +                             "
    "))) + (should (string= (one-ox-fixed-width fixed-width-results-1 nil nil) + (concat "
    "
    +                             "I'm a multiline fixed width\nyes I am!"
    +                             "
    ")))) + + ;; `one-ox-quote-block' + (should (string= (one-ox-quote-block nil "I'm a quote. —Tony Aldon" nil) + "
    I'm a quote. —Tony Aldon
    "))) + +;;;; links + +(ert-deftest one-ox-link--custom-id-https-mailto-test () + "link type: custom-id, https, mailto" + (let ((backend + (org-export-create-backend + :transcoders + '((section . (lambda (_e c _i) c)) + (paragraph . (lambda (_e c _i) c)) + (link . one-ox-link))))) + (should + (string= + (org-test-with-temp-text "[[#foo][bar]]" + (org-export-as backend)) + "bar\n")) + (should + (string= + (org-test-with-temp-text "[[#foo]]" + (org-export-as backend)) + "foo\n")) + (should + (string= + (org-test-with-temp-text "https://tonyaldon.com" + (org-export-as backend)) + "https://tonyaldon.com\n")) + (should + (string= + (org-test-with-temp-text "[[https://tonyaldon.com][Tony Aldon]]" + (org-export-as backend)) + "Tony Aldon\n")) + (should + (string= + (org-test-with-temp-text "mailto:aldon.tony.adm@gmail.com" + (org-export-as backend)) + "mailto:aldon.tony.adm@gmail.com\n")) + (should + (string= + (org-test-with-temp-text "[[mailto:aldon.tony.adm@gmail.com][my email]]" + (org-export-as backend)) + "my email\n")))) + +(ert-deftest one-ox-link--fuzzy-test () + "link type: fuzzy" + (let ((backend + (org-export-create-backend + :transcoders + '((section . (lambda (_e c _i) c)) + (paragraph . (lambda (_e c _i) c)) + (link . one-ox-link))))) + (should-error + (org-test-with-temp-text "[[fuzzy search]]" + (org-export-as backend))) + (should-error + (org-test-with-temp-text "[[*fuzzy search]]" + (org-export-as backend))))) + +(ert-deftest one-ox-link--file-public-and-assets-test () + ;; relative file links starting with ./public or ./assets + (let ((backend + (org-export-create-backend + :transcoders + '((section . (lambda (_e c _i) c)) + (paragraph . (lambda (_e c _i) c)) + (link . one-ox-link))))) + (should + (string= + (org-test-with-temp-text "[[./public/blog/page-1.md][Page 1 in markdown]]" + (org-export-as backend)) + "Page 1 in markdown\n")) + ;; images + (should + (string= + (org-test-with-temp-text "[[./assets/images/one.png]]" + (org-export-as backend)) + "

    \"/images/one.png\"

    \n")) + (should + (string= + (org-test-with-temp-text "[[./assets/images/one.png][one image]]" + (org-export-as backend)) + "

    \"one

    \n")))) + +(ert-deftest one-ox-link--custom-type-test () + ;; link type with an export function defined with `org-link-set-parameters' + (org-link-set-parameters + "foo" + :export (lambda (path desc backend info) + (when (eq backend 'one-ox) + (format "%s" + (concat "foo::::" path) + desc)))) + (let ((backend + (org-export-create-backend + :transcoders + '((section . (lambda (_e c _i) c)) + (paragraph . (lambda (_e c _i) c)) + (link . one-ox-link))))) + (should + (string= + (org-test-with-temp-text "[[foo:bar][My foo type link]]" + (org-export-as backend)) + "My foo type link\n"))) + ;; remove specific link added with `org-link-set-parameters' + (pop org-link-parameters)) + +;;; pages + +(ert-deftest one-internal-id-test () + (let ((get-headline + (lambda (rv tree) + (car (org-element-map tree 'headline + (lambda (e) + (when (string= (org-element-property :raw-value e) rv) + e))))))) + (should + (string= + (org-test-with-parsed-data "* headline 1 +** headline 2 +:PROPERTIES: +:CUSTOM_ID: /#id-test +:END:" + (one-internal-id (funcall get-headline "headline 2" tree))) + "id-test")) + (should + (string= + (org-test-with-parsed-data "* headline 1 +** headline 2 +:PROPERTIES: +:CUSTOM_ID: /path/to/page/#id-test +:END:" + (one-internal-id (funcall get-headline "headline 2" tree))) + "id-test")) + (should + (string-prefix-p "one-" + (org-test-with-parsed-data "* headline 1 +:PROPERTIES: +:CUSTOM_ID: /path/to/page/ +:END:" + (one-internal-id (funcall get-headline "headline 1" tree))))) + (should + (string-prefix-p "one-" + (org-test-with-parsed-data "* headline 1 +** headline 2" + (one-internal-id (funcall get-headline "headline 2" tree))))))) + +(ert-deftest one-is-page-test () + (should + (let (headline) + (equal + (org-test-with-temp-text "* page 1 +:PROPERTIES: +:ONE: render-function +:CUSTOM_ID: /path/to/page-1/ +:END:" + (setq headline (org-element-context)) + (one-is-page headline)) + `(:one-title "page 1" + :one-path "/path/to/page-1/" + :one-render-page-function render-function + :one-page-tree ,headline)))) + (should-not + (org-test-with-temp-text "** NOT A PAGE BECAUSE AT HEADLINE LEVEL > 1 +:PROPERTIES: +:ONE: render-function +:CUSTOM_ID: /path/to/page-1/ +:END:" + (let* ((headline (org-element-context))) + (one-is-page headline)))) + (should-not + (org-test-with-temp-text "* NO PROPERTY ONE +:PROPERTIES: +:CUSTOM_ID: /path/to/page-1/ +:END:" + (let* ((headline (org-element-context))) + (one-is-page headline)))) + (should-not + (org-test-with-temp-text "* NO PROPERTY CUSTOM_ID +:PROPERTIES: +:ONE: render-function +:END:" + (let* ((headline (org-element-context))) + (one-is-page headline))))) + +(ert-deftest one-list-pages-test () + ;; list pages + (should + (equal + (org-test-with-temp-text "* page 1 +:PROPERTIES: +:ONE: render-function-1 +:CUSTOM_ID: /path/to/page-1/ +:END: +* NOT A PAGE +* page 2 +:PROPERTIES: +:ONE: render-function-2 +:CUSTOM_ID: /path/to/page-2/ +:END: +* Headline level 1 +** NOT A PAGE (because at headline level 2) +:PROPERTIES: +:ONE: render-function-3 +:CUSTOM_ID: /path/to/page-3/ +:END: +* NO PROPERTY ONE +:PROPERTIES: +:CUSTOM_ID: /some/path/ +:END: +* NO PROPERTY ONE +:PROPERTIES: +:ONE: render-function-4 +:END:" + (let* ((pages (one-list-pages (one-parse-buffer))) + (page-1 (car pages)) + (page-2 (cadr pages))) + (list (length pages) + (plist-get page-1 :one-path) + (plist-get page-1 :one-render-page-function) + (car (plist-get page-1 :one-page-tree)) + (plist-get page-2 :one-path) + (plist-get page-2 :one-render-page-function) + (car (plist-get page-2 :one-page-tree))))) + '(2 + "/path/to/page-1/" render-function-1 headline + "/path/to/page-2/" render-function-2 headline))) + ;; narrow to the first element + (should + (equal + (org-test-with-temp-text "* page 1 +:PROPERTIES: +:ONE: render-function-1 +:CUSTOM_ID: /path/to/page-1/ +:END: +* page 2 +:PROPERTIES: +:ONE: render-function-2 +:CUSTOM_ID: /path/to/page-2/ +:END:" + (org-narrow-to-element) + (let* ((pages (one-list-pages (one-parse-buffer))) + (page-1 (car pages))) + (list (length pages) + (plist-get page-1 :one-path) + (plist-get page-1 :one-render-page-function) + (car (plist-get page-1 :one-page-tree))))) + '(1 "/path/to/page-1/" render-function-1 headline)))) + +(ert-deftest one-render-page-test () + (let* ((one-tree + (org-test-with-temp-text "* Page foo bar +:PROPERTIES: +:ONE: render-function +:CUSTOM_ID: /foo/bar/ +:END: +* Page foo bar baz +:PROPERTIES: +:ONE: render-function +:CUSTOM_ID: /foo/bar/baz/ +:END:" + (one-parse-buffer))) + (page (one-is-page (nth 2 one-tree))) + (pages (one-list-pages one-tree)) + (global (list :one-tree one-tree))) + (flet ((render-function + (page-tree pages global) + (message "%S" pages) + (concat "-- page --\n" + ":ONE " (org-element-property :ONE page-tree) "\n" + ":CUSTOM_ID " (org-element-property :CUSTOM_ID page-tree) "\n" + "-- pages --\n" + ":one-title " (plist-get (car pages) :one-title) "\n" + ":one-title " (plist-get (cadr pages) :one-title) "\n" + "-- global --\n" + ":one-tree " (symbol-name (car (plist-get global :one-tree)))))) + (let* ((temp-dir (file-name-as-directory + (expand-file-name + (make-temp-file "one-" 'dir)))) + (default-directory temp-dir)) + ;; we are testing `one-render-page' + (one-render-page page pages global) + (should + (string= + (with-current-buffer (find-file-noselect "public/foo/bar/index.html") + (buffer-substring-no-properties (point-min) (point-max))) + "-- page -- +:ONE render-function +:CUSTOM_ID /foo/bar/ +-- pages -- +:one-title Page foo bar +:one-title Page foo bar baz +-- global -- +:one-tree org-data")) + (delete-directory temp-dir t))))) + +(ert-deftest one-render-pages-test () + ;; test variables `one-add-to-global' and `one-hook', + ;; and also that `onerc.el' file is loaded + (flet ((render-function-1 (page-tree pages global) + (org-element-property :raw-value + (nth 2 (plist-get global :one-tree)))) + (render-function-2 (page-tree pages global) + (plist-get global :foo)) + ;; we define it below in the file "onerc.el" + ;; but we want it to be local to the test + (render-function-3 nil) + (global-function (pages tree) "I'm BAR") + ;; create page ./public/tag1/index.html and ./public/tag2/index.html + (tag-hook (pages tree global) + (let ((tag-table (make-hash-table :test 'equal))) + (dolist (page pages) + (let ((path (plist-get page :one-path)) + (tags (org-element-property + :tags + (plist-get page :one-page-tree)))) + (dolist (tag tags) + (puthash (substring-no-properties tag) + (push path (gethash tag tag-table)) tag-table)))) + (maphash + (lambda (tag page-paths) + (let* ((path (concat "./public/" tag "/")) + (file (concat path "index.html"))) + (make-directory path t) + (with-temp-file file + (insert (mapconcat #'identity (sort page-paths 'string<) "\n"))))) + tag-table)))) + (let* ((temp-dir (file-name-as-directory + (expand-file-name + (make-temp-file "one-" 'dir)))) + (default-directory temp-dir) + (_ (with-temp-file (concat temp-dir "onerc.el") + (prin1 '(defun render-function-3 (page-tree pages global) + (plist-get global :bar)) + (current-buffer)))) + (one-add-to-global + '((:one-global-property :one-tree + :one-global-function (lambda (pages tree) tree)) + (:one-global-property :foo + :one-global-function (lambda (pages tree) + (org-element-map tree 'headline + (lambda (elt) (org-element-property :FOO elt)) + nil t))) + (:one-global-property :bar + :one-global-function global-function))) + (one-hook '(tag-hook))) + (org-test-with-temp-text "* Some global information +:PROPERTIES: +:FOO: FOO +:END: +* Page 1 :tag1: +:PROPERTIES: +:ONE: render-function-1 +:CUSTOM_ID: /page-1/ +:END: +* Page 2 :tag2: +:PROPERTIES: +:ONE: render-function-2 +:CUSTOM_ID: /page-2/ +:END: +* Page 3 :tag1:tag2: +:PROPERTIES: +:ONE: render-function-3 +:CUSTOM_ID: /page-3/ +:END:" + (one-render-pages)) + (should + (string= + (with-current-buffer (find-file-noselect "public/page-1/index.html") + (buffer-substring-no-properties (point-min) (point-max))) + "Some global information")) + (should + (string= + (with-current-buffer (find-file-noselect "public/page-2/index.html") + (buffer-substring-no-properties (point-min) (point-max))) + "FOO")) + (should + (string= + (with-current-buffer (find-file-noselect "public/page-3/index.html") + (buffer-substring-no-properties (point-min) (point-max))) + "I'm BAR")) + (should + (string= + (with-current-buffer (find-file-noselect "public/tag1/index.html") + (buffer-substring-no-properties (point-min) (point-max))) + "/page-1/\n/page-3/")) + (should + (string= + (with-current-buffer (find-file-noselect "public/tag2/index.html") + (buffer-substring-no-properties (point-min) (point-max))) + "/page-2/\n/page-3/")) + (delete-directory temp-dir t)))) + +(ert-deftest one-page-at-point-test () + (let (one-path-1 one-path-2 one-path-3 one-path-4 one-path-5) + (should + (equal + (org-test-with-temp-text "no page at point + +* foo +:PROPERTIES: +:ONE: foo +:CUSTOM_ID: /foo/ +:END: + +some content here + +* foo bar +:PROPERTIES: +:ONE: foo-bar +:CUSTOM_ID: /foo/bar/ +:END:" + (setq one-path-1 (one-page-at-point)) + (forward-line 2) + (setq one-path-2 (one-page-at-point)) + (forward-line 6) + (setq one-path-3 (one-page-at-point)) + (forward-line 2) + (setq one-path-4 (one-page-at-point)) + (forward-line 2) + (setq one-path-5 (one-page-at-point)) + (list one-path-1 one-path-2 one-path-3 one-path-4 one-path-5)) + '(nil "/foo/" "/foo/" "/foo/bar/" "/foo/bar/"))))) + +;;; default + +(ert-deftest one-default-pages-test () + (should + (equal + (one-default-pages + '((:one-title "HOME" :one-path "/") + (:one-title "FOO-1" :one-path "/foo-1/") + (:one-title "FOO-2" :one-path "/foo-2/"))) + '(:ul + (:li (:a (@ :href "/") "HOME")) + (:li (:a (@ :href "/foo-1/") "FOO-1")) + (:li (:a (@ :href "/foo-2/") "FOO-2"))))) + (should + (equal + (one-default-pages + '((:one-title "HOME" :one-path "/") + (:one-title "FOO-1" :one-path "/foo-1/") + (:one-title "FOO-2" :one-path "/foo-2/")) + "/.+") + '(:ul + (:li (:a (@ :href "/foo-1/") "FOO-1")) + (:li (:a (@ :href "/foo-2/") "FOO-2"))))) + (should + (equal + (one-default-pages + '((:one-title "HOME" :one-path "/") + (:one-title "FOO-1" :one-path "/foo/foo-1/") + (:one-title "FOO-2" :one-path "/foo/foo-2/") + (:one-title "BAR" :one-path "/bar/foo/") + (:one-title "BAZ" :one-path "/baz/foo/")) + "^/foo/") + '(:ul + (:li (:a (@ :href "/foo/foo-1/") "FOO-1")) + (:li (:a (@ :href "/foo/foo-2/") "FOO-2"))))) + (should-not + (one-default-pages '((:one-title "HOME" :one-path "/")) "/.+"))) + +(ert-deftest one-default-website-name-test () + (should + (string= + (one-default-website-name + '((:one-title "HOME" :one-path "/") + (:one-title "FOO-1" :one-path "/foo-1/") + (:one-title "FOO-2" :one-path "/foo-2/"))) + "HOME")) + (should-not + (one-default-website-name + '((:one-title "FOO-1" :one-path "/foo-1/") + (:one-title "FOO-2" :one-path "/foo-2/"))))) + +(ert-deftest one-default-nav-test () + ;; Two pages different from the home page are expected + (should + (equal + (one-default-nav + "/foo-1/" + '((:one-path "/") + (:one-path "/foo-1/"))) + '(:div.nav (:a (@ :href "/") "PREV") nil nil))) + (should + (equal + (one-default-nav + "/" + '((:one-path "/") + (:one-path "/foo-1/"))) + '(:div.nav nil nil (:a (@ :href "/foo-1/") "NEXT")))) + (let* ((nav (one-default-nav + "/foo-2/" + '((:one-path "/") + (:one-path "/foo-1/") + (:one-path "/foo-2/") + (:one-path "/foo-3/") + (:one-path "/foo-4/")))) + (random (nth 2 nav))) + (should (equal (nth 1 nav) '(:a (@ :href "/foo-1/") "PREV"))) + (should (equal (nth 3 nav) '(:a (@ :href "/foo-3/") "NEXT"))) + (should-not (string= (nth 2 (nth 1 random)) "/foo-2/")) + (should (member (nth 2 (nth 1 random)) '("/" "/foo-1/" "/foo-3/" "/foo-4/"))) + (should (equal (nth 2 random) "RANDOM")))) + +(ert-deftest one-default-list-headlines-test () + (should + (equal + (org-test-with-temp-text "* page 1 +:PROPERTIES: +:CUSTOM_ID: /path/to/page-1/ +:END: +** headline 1.1 +:PROPERTIES: +:CUSTOM_ID: /path/to/page-1/#id-11 +:END: +** headline 1.2 +*** headline 1.2.1 +** headline 1.3 +*** headline 1.3.1 +* page 2 +:PROPERTIES: +:CUSTOM_ID: /path/to/page-2/ +:END: +** headline 2.1 +*** headline 2.1.1 +" + (let* ((tree (one-parse-buffer)) + (headlines (one-default-list-headlines tree)) + (headline-1 (car headlines)) + (headline-2 (cadr headlines))) + (list (length headlines) + (substring-no-properties (plist-get headline-1 :id) 0 4) + (plist-get headline-1 :level) + (plist-get headline-1 :title) + (plist-get headline-2 :id) + (plist-get headline-2 :level) + (plist-get headline-2 :title)))) + '(9 + "one-" 1 "page 1" + "id-11" 2 "headline 1.1")))) + +(global-set-key (kbd "C-") (lambda () (interactive) (ert "one-default-toc-test"))) +(ert-deftest one-default-toc-test () + (should + (equal + (one-default-toc + '((:level 2 :id "id-bar-1" :title "bar-1") + (:level 3 :id "id-bar-1.1" :title "bar-1.1") + (:level 3 :id "id-bar-1.2" :title "bar-1.2") + (:level 4 :id "id-bar-1.2.1" :title "bar-1.2.1") + (:level 4 :id "id-bar-1.2.2" :title "bar-1.2.2") + (:level 5 :id "id-bar-1.2.2.1" :title "bar-1.2.2.1") + (:level 5 :id "id-bar-1.2.2.2" :title "bar-1.2.2.2") + (:level 6 :id "id-bar-1.2.2.2.1" :title "bar-1.2.2.2.1") + (:level 6 :id "id-bar-1.2.2.2.2" :title "bar-1.2.2.2.2") + (:level 6 :id "id-bar-1.2.2.2.3" :title "bar-1.2.2.2.3") + (:level 2 :id "id-bar-2" :title "bar-2") + (:level 3 :id "id-bar-2.1" :title "bar-2.1") + (:level 3 :id "id-bar-2.2" :title "bar-2.2") + (:level 2 :id "id-bar-3" :title "bar-3") + (:level 3 :id "id-bar-3.1" :title "bar-3.1") + (:level 2 :id "id-bar-4" :title "bar-4"))) + " + +")) + ) diff --git a/one.el/one.el b/one.el/one.el new file mode 100644 index 0000000..49223dd --- /dev/null +++ b/one.el/one.el @@ -0,0 +1,2021 @@ +;;; one.el --- Static Site Generator for org-mode users -*- lexical-binding: t; -*- +;; +;; Copyright (C) 2021-2022 Tony Aldon + +;; Author: Tony Aldon +;; Version: 1.1 +;; Package-Requires: ((emacs "28.1") (jack "1.0") (htmlize "1.57")) +;; Keywords: hypermedia, outlines +;; Homepage: https://github.com/tonyaldon/one.el + +;; 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 . + +;;; Commentary: +;; +;; You can find the documentation at https://one.tonyaldon.com. +;; +;; In `one.el', the following org document defines a website with 3 pages +;; that we build by calling `one-build' command while we are visiting it: +;; +;; --------------------------------- +;; * My website +;; :PROPERTIES: +;; :ONE: one-default-home +;; :CUSTOM_ID: / +;; :END: +;; +;; Welcome to my website! +;; +;; * Blog post 1 +;; :PROPERTIES: +;; :ONE: one-default +;; :CUSTOM_ID: /blog/page-1/ +;; :END: +;; +;; My first blog post! +;; +;; * Blog post 2 +;; :PROPERTIES: +;; :ONE: one-default +;; :CUSTOM_ID: /blog/page-2/ +;; :END: +;; +;; My second blog post! +;; --------------------------------- +;; +;; Note that if we want to use the default css style sheet we can add it +;; by calling `one-default-add-css-file' before building the website. +;; +;; The path `/' in the first `CUSTOM_ID' org property tells `one.el' that the +;; page "My website" is the home page. That page is rendered using +;; `one-default-home' render function, value of `ONE' org property of the +;; same headline. +;; +;; The path `/blog/page-1/' in the second `CUSTOM_ID' org property tells +;; `one.el' that we want to render "Blog post 1" page in such a way +;; that when we serve our website locally at `http://localhost:3000' for +;; instance, that page is served at `http://localhost:3000/blog/page-1/'. +;; How that page is rendered is determined by the value of `ONE' org +;; property of the same headline which is `one-default', a render +;; function. +;; +;; The same goes for the last page "Blog post 2". +;; +;; As you might have noticed, a `one.el' website is an org file where the +;; pages are the headlines of level 1 with the org properties `ONE' and +;; `CUSTOM_ID' set. Nothing more! +;; +;; `ONE' is the only org property added by `one.el'. Its value, an Emacs Lisp +;; function which returns an HTML string, for a given page determines how +;; `one.el' renders that page. +;; +;; Paths of pages are set using `CUSTOM_ID' org property. + + +;;; Code: + +(require 'jack) +(require 'ox) +(require 'htmlize) + +(defvar htmlize-buffer-places) + +;;; utils + +(defun one-escape (s) + "Return the string S with some caracters escaped. +`<', `>' and `&' are escaped." + (replace-regexp-in-string + "\\(<\\)\\|\\(>\\)\\|\\(&\\)\\|\\(\"\\)\\|\\('\\)" + (lambda (m) (pcase m + ("<" "<") + (">" ">") + ("&" "&") + ("\"" """) + ("'" "'"))) + s)) + +;;; one-ox +;;;; one backend + +(org-export-define-backend 'one-ox + '((headline . one-ox-headline) + (section . one-ox-section) + (paragraph . one-ox-paragraph) + + (plain-text . one-ox-plain-text) + + (bold . one-ox-bold) + (italic . one-ox-italic) + (strike-through . one-ox-strike-through) + (underline . one-ox-underline) + (code . one-ox-code) + (verbatim . one-ox-verbatim) + + (subscript . one-ox-no-subscript) + (superscript . one-ox-no-superscript) + + (plain-list . one-ox-plain-list) + (item . one-ox-item) + + (src-block . one-ox-src-block) + (example-block . one-ox-example-block) + (fixed-width . one-ox-fixed-width) + (quote-block . one-ox-quote-block) + + (table . one-ox-table) + (table-row . one-ox-table-row) + (table-cell . one-ox-table-cell) + + (link . one-ox-link))) + +;;;; headline, section, paragraph, etc. + +(defun one-ox-headline (headline contents _info) + "Transcode a HEADLINE element from Org to HTML. + +CONTENTS holds the contents of the headline. + +Note that markups and links are not exported if used in headlines, +only the raw value string. So don't use them in headlines." + + (let* ((level (org-element-property :level headline)) + (title (org-element-property :raw-value headline)) + ;; the property `:one-internal-id' is set by + ;; `one-parse-buffer' This allow to produce unified + ;; ids that can be use to build a TOC for each page. + (id (org-element-property :one-internal-id headline)) + (ct (if (null contents) "" contents))) + (format "
    %s%s
    " level id title level ct))) + +(defun one-ox-section (_section contents _info) + "Transcode a SECTION element from Org to HTML. + +CONTENTS holds the contents of the section." + (if (null contents) "" (format "
    %s
    " contents))) + +(defun one-ox-paragraph (_paragraph contents _info) + "Transcode a PARAGRAPH element from Org to HTML. + +CONTENTS is the contents of the paragraph, as a string." + (format "

    %s

    " contents)) + +(defun one-ox-plain-text (text _info) + "Transcode a TEXT string from Org to HTML. + +TEXT is the string to transcode." + (one-escape text)) + +(defun one-ox-bold (_bold contents _info) + "Transcode BOLD from Org to HTML. + +CONTENTS is the text with bold markup." + (format "%s" contents)) + +(defun one-ox-italic (_italic contents _info) + "Transcode ITALIC from Org to HTML. + +CONTENTS is the text with italic markup." + (format "%s" contents)) + +(defun one-ox-strike-through (_strike-through contents _info) + "Transcode STRIKE-THROUGH from Org to HTML. + +CONTENTS is the text with strike-through markup." + (format "%s" contents)) + +(defun one-ox-underline (_underline contents _info) + "Transcode UNDERLINE from Org to HTML. + +CONTENTS is the text with underline markup." + (format "%s" contents)) + +(defun one-ox-code (code _contents _info) + "Transcode CODE from Org to HTML." + (format "%s" + (one-escape (org-element-property :value code)))) + +(defun one-ox-verbatim (verbatim _contents _info) + "Transcode VERBATIM from Org to HTML." + (format "%s" + (one-escape (org-element-property :value verbatim)))) + +(defun one-ox-plain-list (plain-list contents _info) + "Transcode a PLAIN-LIST element from Org to HTML. + +CONTENTS is the contents of the list." + (let* ((type (pcase (org-element-property :type plain-list) + (`ordered "ol") + (`unordered "ul") + (other (error "`one-ox' doesn't support list type: %s" other))))) + (format "<%s>%s" type contents type))) + +(defun one-ox-item (_item contents _info) + "Transcode an ITEM element from Org to HTML. + +CONTENTS holds the contents of the item." + (format "
  • %s
  • " contents)) + +(defun one-ox-table (_table contents _info) + "Transcode a TABLE element from Org to HTML. + +CONTENTS holds the contents of the table." + (format "%s
    " contents)) + +(defun one-ox-table-row (table-row contents _info) + "Transcode a TABLE-ROW element from Org to HTML. + +CONTENTS holds the contents of the row." + (if (eq 'rule (org-element-property :type table-row)) + "" + (format "%s" contents))) + +(defun one-ox-table-cell (table-cell contents _info) + "Transcode a TABLE-CELL element from Org to HTML. + +CONTENTS holds the contents of the cell." + (let* ((row (org-export-get-parent table-cell)) + (table (org-export-get-parent-table table-cell)) + (has-header (org-export-table-has-header-p table _info)) + (row-number (org-export-table-row-number row _info)) + (tag (if (and has-header (= row-number 0)) "th" "td"))) + (format "<%s>%s" tag (or contents "") tag))) + +(defun one-ox-no-subscript (_subscript contents _info) + "Transcode a SUBSCRIPT object from Org to HTML. + +CONTENTS is the contents of the object." + (concat "_" contents)) + +(defun one-ox-no-superscript (_superscript contents _info) + "Transcode a SUPERSCRIPT object from Org to HTML. + +CONTENTS is the contents of the object." + (concat "^" contents)) + +;;;; blocks + +(defun one-ox-fontify-code (code lang) + "Color CODE with htmlize library. + +CODE is a string representing the source code to colorize. LANG +is the language used for CODE, as a string, or nil." + (when code + (let* ((lang (or (assoc-default lang org-src-lang-modes) lang)) + (lang-mode (and lang (intern (format "%s-mode" lang))))) + (if (functionp lang-mode) + (let* ((code + (let ((inhibit-read-only t)) + (with-temp-buffer + (funcall lang-mode) + (insert code) + (font-lock-ensure) + (org-src-mode) + (set-buffer-modified-p nil) + (let* ((htmlize-output-type 'css) + (htmlize-css-name-prefix "one-hl-") + (htmlbuf (htmlize-buffer))) + (unwind-protect + (with-current-buffer htmlbuf + (buffer-substring + (plist-get htmlize-buffer-places 'content-start) + (plist-get htmlize-buffer-places 'content-end))) + (kill-buffer htmlbuf))))))) + ;; Strip any enclosing
     tags.
    +            (if-let ((beg (and (string-match "\\`]*>\n?" code) (match-end 0)))
    +                     (end (string-match "\\'" code)))
    +                (substring code beg end)
    +              code))
    +        (one-escape code)))))
    +
    +(defun one-ox-htmlize (code lang &optional is-results-p)
    +  "Return CODE string htmlized using `htmlize.el' in language LANG.
    +
    +If IS-RESULTS-P is non-nil, CSS class of tag  in the returned
    +string is \"one-hl one-hl-results\".
    +If nil, the CSS class is `one-hl one-hl-block'."
    +  (let* ((class (if is-results-p
    +                    "one-hl one-hl-results"
    +                  "one-hl one-hl-block")))
    +    (format "
    %s
    " + class + (replace-regexp-in-string + "\\([^<]*\\)" + "\\1" + (if (null lang) + (one-escape (or code "")) ; example blocks + (one-ox-fontify-code code lang)))))) + +(defun one-ox-src-block (src-block _contents _info) + "Return SRC-BLOCK element htmlized using `htmlize.el'." + (let* ((code (car (org-export-unravel-code src-block))) + (lang (org-element-property :language src-block)) + (is-results-p (org-element-property :results src-block))) + (one-ox-htmlize code lang is-results-p))) + +(defun one-ox-example-block (example-block _contents _info) + "Return EXAMPLE-BLOCK element htmlized using `htmlize.el'." + (let* ((code (car (org-export-unravel-code example-block))) + (lang "text") + (is-results-p (org-element-property :results example-block))) + (one-ox-htmlize code lang is-results-p))) + +(defun one-ox-fixed-width (fixed-width _contents _info) + "Return FIXED-WIDTH element htmlized using `htmlize.el'." + (let* ((code (car (org-export-unravel-code fixed-width))) + (lang "text") + (is-results-p (org-element-property :results fixed-width))) + (one-ox-htmlize code lang is-results-p))) + +(defun one-ox-quote-block (_quote-block contents _info) + "Transcode a QUOTE-BLOCK element from Org to HTML. + +CONTENTS holds the contents of the block." + (format "
    %s
    " contents)) + +;;;; links + +(define-error 'one-link-broken "Unable to resolve link") + +(defvar one-ox-link-image-extensions + (format "\\.%s\\'" + (regexp-opt + '("webp" "avif" "png" "jpeg" "jpg" "gif" + "tiff" "tif" "xbm" "xpm" "pbm" "pgm" "ppm") + t)) + "Regexp matching image extensions. + +See `one-ox-link'.") + +(defun one-ox-link (link desc info) + "Transcode a LINK object from Org to HTML. +DESC is the description part of the link, or the empty string. +INFO is a plist holding contextual information." + (let* ((type (org-element-property :type link)) + (path (org-element-property :path link)) + (raw-link (org-element-property :raw-link link)) + (custom-type-link + (let ((export-func (org-link-get-parameter type :export))) + (and (functionp export-func) + (funcall export-func path desc 'one-ox info)))) + (href (cond + ((string= type "custom-id") path) + ((string= type "fuzzy") + (let ((beg (org-element-property :begin link))) + (signal 'one-link-broken + `(,raw-link + "fuzzy links not supported" + ,(format "goto-char: %s" beg))))) + ((string= type "file") + (or + ;; ./assets/images/image-1.png --> /images/image-1.png + ;; ./public/blog/page-1.md --> /blog/page-1.md + (and (string-match "\\`\\./\\(assets\\|public\\)" path) + (replace-match "" nil nil path)) + (let ((beg (org-element-property :begin link))) + (signal 'one-link-broken + `(,raw-link ,(format "goto-char: %s" beg)))))) + (t raw-link)))) + (or custom-type-link + (and + (string-match one-ox-link-image-extensions path) + (format "

    \"%s\"

    " + href (or (org-string-nw-p desc) href)) ) + (format "%s" + href (or (org-string-nw-p desc) href))))) + +;;; Commands to build `one.el' web sites + +(defvar one-add-to-global + '((:one-global-property :one-tree + :one-global-function (lambda (pages tree) tree))) + "List used to set the `global' argument passed to render functions. + +Elements in that list are plist with the following properties: + +- `:one-global-property': a keyword that is used as proprety + in the `global' argument passed to the render functions, +- `:one-global-function': a function that takes two arguments `pages' + (list of pages, see `one-list-pages') and `tree' + (see `one-parse-buffer'). That function is called once in + `one-render-pages' and its result is used as the value of + the property `:one-global-property' in the `global' argument + passed to the render functions. + +For instance, if `one-add-to-global' is set to + + ((:one-global-property :one-tree + :one-global-function (lambda (pages tree) tree))) + +then `global' local variable will be set to + + ((:one-tree tree)) + +where `tree' is the value returned by `one-parse-buffer' function.") + +(defvar one-hook nil + "List of functions called once in `one-render-pages'. + +Those functions take three arguments: + +- `pages': list of pages, see `one-list-pages', +- `tree': see `one-parse-buffer', +- `global': see `one-add-to-global'. + +As those functions take `global' argument they are called after +that argument has been let binded using `one-add-to-global'.") + +(defvar one-emacs-cmd-line-args-async nil + "List of command line arguments to pass to `emacs' subprocess. + +The function `one-render-pages-async' and `one-build-async' spawn an +`emacs' subprocess in order to build html pages asynchronously. The +arguments passed to `emacs' depends on `one-emacs-cmd-line-args-async' value. + +By default, when `one-emacs-cmd-line-args-async' is nil, we run `emacs' +in \"batch mode\", we load the user's initialization file and we evaluate +a specific sexp that builds html pages. Specifically, we pass +the following `command' (`emacs' file name followed by command line +arguments) to `make-process' function like this: + + (let* ((emacs (file-truename + (expand-file-name invocation-name invocation-directory))) + (command \\=`(,emacs \"--batch\" + \"-l\" ,user-init-file + \"--eval\" ,sexp)) + (sexp ...)) + (make-process + :name ... + :buffer ... + :command command)) + +If `one-emacs-cmd-line-args-async' is non-nil, we no longer load the user's +initialization file and replace '\"-l\" ,user-init-file' in `command' above +by the elements of `one-emacs-cmd-line-args-async'. For instance, if +`one-emacs-cmd-line-args-async' is equal to + + \\='(\"-l\" \"/path/to/some-elisp-file.el\") + +then `command' becomes + + (let* (... + (command \\=`(,emacs \"--batch\" + \"-l\" \"/path/to/some-elisp-file.el\" + \"--eval\" ,sexp)) + ...) + ...)") + +(define-error 'one-path "CUSTOM_ID not defined") + +(defun one-internal-id (headline) + "Return a string id for HEADLINE to be used as :one-internal-id property. + +The id is built from CUSTOM_ID property of HEADLINE if set +or generated a randomly." + (let* ((custom-id (org-element-property :CUSTOM_ID headline))) + (or (and custom-id + ;; we match "baz" in "/foo/bar/#baz" + (string-match "\\`\\(?:[^#]+\\S-*\\)#\\(.+\\)" custom-id) + (match-string-no-properties 1 custom-id)) + (format "one-%x" (random #x10000000000))))) + +(defun one-parse-buffer () + "Parse current org buffer and return structure. + +The only difference with `org-element-parse-buffer' is that +we add the property `:one-internal-id' to each headline." + (let ((tree (org-element-parse-buffer))) + ;; destructively add :one-internal-id property to headlines in `tree' + (org-element-map tree 'headline + (lambda (elt) + (org-element-put-property + elt :one-internal-id (one-internal-id elt)))) + tree)) + +(defun one-is-page (headline) + "Return nil if HEADLINE element is not a `one.el' page. + +If HEADLINE is a page, return a plist with the properties +`:one-title', `:one-path', `:one-render-page-function' and +`:one-page-tree' defined like this: + +- `:one-title': the raw value of the first headline of HEADLINE, +- `:one-path': the path of the page as a string, +- `:one-render-page-function': the function to render the page as + a symbol. This function is declared in the org buffer for + each page using the org property ONE. + + This function takes 3 arguments: + + - `page-tree' which correspond to the data in `:one-page-tree', + - `pages' list of pages, + - `global' a plist of global informations that are computed once + when `one.el' website is built (before rendering the pages), see + `one-render-pages' and `one-build'. This argument can be + modified by the user at build time. That means that if your + render function needs extra information you can tell `one.el' to + compute those informations and to add them to `global'. + + You can see how to implement render functions looking at the + default render functions: + + - `one-default-home', + - `one-default-home-list-pages' + - `one-default', + - `one-default-with-toc', + - `one-default-with-sidebar' and + - `one-default-doc'. + +- `:one-page-tree': the argument HEADLINE passed to `one-is-page'. + +See `one-list-pages'." + (when (= (org-element-property :level headline) 1) + (when-let ((path (org-element-property :CUSTOM_ID headline)) + (render-page-function (org-element-property :ONE headline))) + `(:one-title ,(org-element-property :raw-value headline) + :one-path ,path + :one-render-page-function ,(intern render-page-function) + :one-page-tree ,headline)))) + +(defun one-list-pages (tree) + "Return the list of the pages in TREE. + +TREE is a parsed org buffer as returned by `one-parse-buffer'. +The function `one-is-page' determines which headlines in TREE +are pages." + (org-element-map tree 'headline + (lambda (headline) (one-is-page headline)))) + +(defun one-render-page (page pages global) + "Render the webpage PAGE. + +See `one-is-page' for the meaning of PAGES and GLOBAL argument." + (let* ((path (concat "./public" (plist-get page :one-path))) + (file (concat path "index.html")) + (render-page-function (plist-get page :one-render-page-function)) + (page-tree (plist-get page :one-page-tree))) + (make-directory path t) + (with-temp-file file + (insert (funcall render-page-function page-tree pages global))))) + +;;;###autoload +(defun one-render-pages (&optional one-path) + "Render webpages of the current buffer under `./public/' dir. + +If ONE-PATH is non-nil, it must be the path of page in the current +buffer. That means it must match the `CUSTOM_ID' org property value +of a level 1 headline. + +If a file `onerc.el' exist in the current directory, it will be loaded +first. This way we can customize how the website is built by adding +some Elisp code in that file. + +Information in `one-add-to-global' are used to set the ‘global’ argument +passed to render functions. + +Once `onerc.el' file has been loaded and `global' argument passed to +render functions set the hook `one-hook' is run. Then the webpages +are rendered. + +The current buffer should look like this. + + ---------- Buffer ---------- + * Home + :PROPERTIES: + :ONE: render-function-0 + :CUSTOM_ID: / + :END: + + Content of the home page + + * Page 1 + :PROPERTIES: + :ONE: render-function-1 + :CUSTOM_ID: /blog/page-1/ + :END: + + Content of page 1 + + * Page 2 + :PROPERTIES: + :ONE: render-function-2 + :CUSTOM_ID: /blog/page-2/ + :END: + + Content of page 2 + + * I'm not a Page and I won't be rendered + ---------- Buffer ---------- + +Each level 1 headline with the org properties `ONE' and +`CUSTOM_ID' set is a page. See `one-is-page'. + +How those pages are rendered and where they are rendered +depends on the render functions specified in `ONE' org +property and the path specified in `CUSTOM_ID' org property. + +For instance, with the above buffer, assuming the render +functions `render-function-0' and `render-function-2' +are well defined and assuming the render function +`render-function-1' is defined like this + + (defun render-function-1 (page-tree pages global) + \"

    Hello world!

    \") + +calling the command `one-render-pages' produces +the following files + + . + └── public + ├── blog + │ ├── page-1 + │ │ └── index.html + │ └── page-2 + │ └── index.html + └── index.html + +and the content of the file `./public/blog/page-1/index.html' is + + ---------- File: ./public/blog/page-1/index.html ---------- +

    Hello world!

    + ---------- File: ./public/blog/page-1/index.html ---------- + +Therefore if you serve the website in `./public/' directory at +`http://localhost:3000' you can access the \"Hello word!\" page +at `http://localhost:3000/blog/page-1/'. + +You can see how to implement render functions looking at the +implementation of the default render functions `one-default-home', +`one-default-home-list-pages',`one-default', `one-default-with-toc', +`one-default-with-sidebar' and `one-default-doc'. + +Note that `one-render-pages' doesn't copy files from +`./assets/' directory to `./public/' directory. + +See `one-build'. + +If you want to start a new `one.el' project with the default style +see `one-default-new-project' command. + +Note: I use https://browsersync.io to serve the website in `./public/' +directory. Once you have it installed, to start a web server with +live reloading, you can run the following commands (in a terminal): + + $ cd public + $ browser-sync start -s -w --files \"*\"" + (interactive) + (let ((onerc (concat default-directory "onerc.el")) + (inhibit-message t)) + (when (file-exists-p onerc) (load onerc))) + (let* ((tree (org-with-wide-buffer (one-parse-buffer))) + (pages (one-list-pages tree)) + (global + (let (global) + (dolist (glob one-add-to-global) + (push (funcall (plist-get glob :one-global-function) pages tree) + global) + (push (plist-get glob :one-global-property) global)) + global))) + (dolist (hook one-hook) (funcall hook pages tree global)) + (if one-path + (if-let ((page (seq-some + (lambda (page) + (when (string= (plist-get page :one-path) one-path) + page)) + pages))) + (progn + (message "Build page `%s'..." one-path) + (one-render-page page pages global) + (message "Build page `%s'...done" one-path)) + (error "Page `%s' doesn't exist" one-path)) + (message "Build pages...") + (dolist (page pages) + (progn + (message "Build page `%s'" (plist-get page :one-path)) + (one-render-page page pages global))) + (message "Build pages...done")))) + +;;;###autoload +(defun one-render-pages-async (&optional one-path) + "Render webpages of the current buffer under `./public/' dir asynchronously. + +The function `one-render-pages-async' spawns an `emacs' subprocess +in order to build html pages asynchronously. The arguments passed to +`emacs' depends on `one-emacs-cmd-line-args-async' value. + +If ONE-PATH is non-nil, it must be the path of page in the current +buffer. That means it must match the `CUSTOM_ID'org property value +of a level 1 headline. + +See `one-render-pages'." + (interactive) + (let* ((org-content (org-with-wide-buffer + (buffer-substring (point-min) (point-max)))) + (org-content-file (make-temp-file "one-content-" nil ".org")) + (current-dir default-directory) + (sexp (with-output-to-string + (prin1 `(progn + (require 'one) + (find-file ,org-content-file) + (setq default-directory ,current-dir) + (one-render-pages ,one-path))))) + (emacs (file-truename + (expand-file-name invocation-name invocation-directory))) + (command (if one-emacs-cmd-line-args-async + `(,emacs "--batch" ,@one-emacs-cmd-line-args-async "--eval" ,sexp) + `(,emacs "--batch" "-l" ,user-init-file "--eval" ,sexp))) + (sentinel (lambda (process msg) + (internal-default-process-sentinel process msg) + (if (string-match-p "finished" msg) + (if one-path + (message "Build page `%s'...done" one-path) + (message "Build pages...done")) + (message "%s, check buffer `%s'" + (string-trim-right msg) + (buffer-name (process-buffer process))))))) + (with-temp-file org-content-file (insert org-content)) + (if one-path + (message "Build page `%s'..." one-path) + (message "Build pages...")) + (let ((process-connection-type nil) + (inhibit-message t)) + (make-process + :name "one" + :buffer (get-buffer-create "*one*") + :command command + :connection-type nil + :sentinel sentinel)))) + +(defun one-page-at-point () + "Return `one-path' of the page at point. + +Return nil if no page found. +Doesn't move point nor change the match data." + (save-match-data + (save-excursion + (org-with-wide-buffer + (goto-char (line-beginning-position)) + (when (not (looking-at "^\\* ")) + (search-backward-regexp "^\\* " nil t)) + (org-element-property :CUSTOM_ID (org-element-at-point)))))) + +;;;###autoload +(defun one-render-page-at-point () + "Build page at point. + +See `one-render-pages'." + (interactive) + (if-let ((one-path (one-page-at-point))) + (one-render-pages one-path) + (message "No page found at point"))) + +;;;###autoload +(defun one-render-page-at-point-async () + "Build page at point asynchronously. + +See `one-render-pages-async'." + (interactive) + (if-let ((one-path (one-page-at-point))) + (one-render-pages-async one-path) + (message "No page found at point"))) + +(defun one-copy-assets-to-public () + "Copy `./assets/' files into `./public/' subdirectory." + (interactive) + (when (file-exists-p "./assets/") + (copy-directory "./assets/" "./public/" nil t 'copy-contents))) + +;;;###autoload +(defun one-build-async () + "Build website of the current buffer under `./public/' dir asynchronously. + +The function `one-build-async' spawns an `emacs' subprocess in order to +render html pages, clean `./public/' directory and copy `./assets/' +directory asynchronously. The arguments passed to `emacs' depends +on `one-emacs-cmd-line-args-async' value. + +See `one-build'." + (interactive) + (let* ((org-content (org-with-wide-buffer + (buffer-substring (point-min) (point-max)))) + (org-content-file (make-temp-file "one-content-" nil ".org")) + (current-dir default-directory) + (sexp (with-output-to-string + (prin1 `(progn + (find-file ,org-content-file) + (setq default-directory ,current-dir) + (when (file-exists-p "./public/") + (dolist (file (cddr (directory-files "./public/" 'full))) + (if (file-directory-p file) + (delete-directory file t) + (delete-file file)))) + (require 'one) + (one-copy-assets-to-public) + (one-render-pages))))) + (emacs (file-truename + (expand-file-name invocation-name invocation-directory))) + (command (if one-emacs-cmd-line-args-async + `(,emacs "--batch" ,@one-emacs-cmd-line-args-async "--eval" ,sexp) + `(,emacs "--batch" "-l" ,user-init-file "--eval" ,sexp))) + (sentinel (lambda (process msg) + (internal-default-process-sentinel process msg) + (if (string-match-p "finished" msg) + (message "Build pages...done") + (message "%s, check buffer `%s'" + (string-trim-right msg) + (buffer-name (process-buffer process))))))) + (with-temp-file org-content-file (insert org-content)) + (message "Build pages...") + (let ((process-connection-type nil) + (inhibit-message t)) + (make-process + :name "one" + :buffer (get-buffer-create "*one*") + :command command + :connection-type nil + :sentinel sentinel)))) + +;;;###autoload +(defun one-build () + "Build website of the current buffer under `./public/' subdirectory. + +Specifically: + +1) clean `./public/' subdirectory (if it exists), +2) copy `./assets/' files into `./public/' subdirectory and +3) call `one-render-pages' once. + +See `one-render-pages'." + (interactive) + (when (file-exists-p "./public/") + (dolist (file (cddr (directory-files "./public/" 'full))) + (if (file-directory-p file) + (delete-directory file t) + (delete-file file)))) + (one-copy-assets-to-public) + (one-render-pages)) + +;;; A default web site + +(defvar one-default-css + "@import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400&family=Noto+Sans:wght@400;700&display=swap'); + +html, body, p, ol, ul, li, dl, dt, dd, +blockquote, figure, fieldset, legend, textarea, +pre, iframe, hr, h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; +} + +*, *::before, *::after { + box-sizing: border-box; +} + +p, blockquote, ul, ol, code, +dl, table, pre, details { + margin-bottom: 16px; + margin-top: 0; +} + +ul { + padding-left: 2em; + list-style: disc; +} + +ul ul { + margin-top: 0; + margin-bottom: 0; +} + +ol { + padding-left: 2em; + list-style: decimal; +} + +li p:first-of-type { + margin: 0; +} + +li p { + margin: 16px 0; +} + +li code { + margin: 16px 0; +} + +html { + scroll-padding-top: 4rem; /* because we use a sticky header */ +} + +body { + background: #151515; + color: #dedede; + font-family: \"Noto Sans\",sans-serif; + font-size: 106%; + line-height: 1.5; + word-wrap: break-word; +} + +h1 { + font-size: 2em; +} + +h2, h3, h4, h5, h6 { + padding-bottom: 0.3em; + margin-top: 24px; + margin-bottom: 16px; + font-weight: bold; + line-height: 1.25; +} + +h2, h3 { + border-bottom: 1px solid #1d272b; +} + +h2 {font-size: 2em;} +h3 {font-size: 1.5em;} +h4 {font-size: 1.25em;} +h5 {font-size: 1em;} +h6 {font-size: .875em;} + +a { + color: #ffd787; + cursor: pointer; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +a:visited { + color: #ffd787; +} + +img { + width: 100%; + height: auto; + border-radius: 6px; +} + +/* ------- '.one' classes used by 'one-ox' org backend ------- */ + +.one-hl { + font-family: 'Fira Mono', monospace; + font-size: 80%; + border-radius: 6px; +} + +.one-hl-inline { + background: #31424a; + padding: 0.2em 0.4em; + margin: 0; + white-space: break-spaces; +} + +.one-hl-block { + background: #161f22; + color: #c5c5c5; + display: block; + overflow: auto; + padding: 16px; + line-height: 1.45; +} + +.one-blockquote { + background: #202d31; + border-left: 0.3em solid #31424a; + margin: 0px auto 16px; + padding: 1em 1em; + width: 90%; +} + +.one-blockquote > p:last-child { + margin-bottom: 0; +} + +.one-hl-results { + background: #202d31 ; + border-left: 2px solid #c5c5c5; + display: block; + margin: auto; + padding: 0.5em 1em; + overflow: auto; + width: 98%; +} + +.one-hl-negation-char { color: #ff6c60} /* font-lock-negation-char-face */ +.one-hl-warning { color: #fd971f} /* font-lock-warning-face */ +.one-hl-variable-name { color: #fd971f} /* font-lock-variable-name-face */ +.one-hl-doc { color: #d3b2a1} /* font-lock-doc-face */ +.one-hl-doc-string { color: #d3b2a1} /* font-lock-doc-string-face */ +.one-hl-string { color: #d3b2a1} /* font-lock-string-face */ +.one-hl-function-name { color: #02d2da} /* font-lock-function-name-face */ +.one-hl-builtin { color: #b2a1d3} /* font-lock-builtin-face */ +.one-hl-type { color: #457f8b} /* font-lock-type-face */ +.one-hl-keyword { color: #f92672} /* font-lock-keyword-face */ +.one-hl-preprocessor { color: #f92672} /* font-lock-preprocessor-face */ +.one-hl-comment-delimiter { color: #8c8c8c} /* font-lock-comment-delimiter-face */ +.one-hl-comment { color: #8c8c8c} /* font-lock-comment-face */ +.one-hl-constant { color: #f5ebb6} /* font-lock-constant-face */ +.one-hl-reference { color: #f5ebb6} /* font-lock-reference-face */ +.one-hl-regexp-grouping-backslash { color: #966046} /* font-lock-regexp-grouping-backslash */ +.one-hl-regexp-grouping-construct { color: #aa86ee} /* font-lock-regexp-grouping-construct */ +.one-hl-number { color: #eedc82} /* font-lock-number-face */ + +.one-hl-sh-quoted-exec { color: #62bd9c} /* sh-quoted-exec */ + +/* -------- scrollbar -------- */ + +::-webkit-scrollbar { + width: 1em; + height: 1em; +} + +::-webkit-scrollbar-track { + background: #202d31; +} + +::-webkit-scrollbar-thumb { + background: #31424a; + border-radius: 0.5em; +} + +::-webkit-scrollbar-thumb:hover { + background: #31424a; +} + +/* -------- specific to the default render functions -------- */ + +.header { + color: #ffffff; + font-size: 2em; + font-weight: bold; + padding: 0 16px 0 16px; + background: #151515; + width: 100%; + height: 3.5rem; + position: fixed; + top: 0; + left: 0; + border-bottom: 1px solid #1d272b; + display: flex; + justify-content: center; + align-items: center; +} + +.header > a { + color: inherit; + cursor: pointer; + text-decoration: none; +} + +.header > a:visited { + color: inherit; +} + +.content { + margin: 3.5rem auto; + padding-top: 1.8rem; + max-width: 740px; + padding: 0 16px; +} + +.title { + text-align: center; + padding: 1.8rem 0; +} + +.title-empty { + padding-top: 1rem; +} + +/* -------- one-default-home -------- */ + +#home { + margin: 5rem 0 1.5rem 0; +} + +/* -------- one-default-home-list-pages -------- */ + +#home-list-pages { + margin: 5rem 0 1.5rem 0; +} + +#pages ul { + padding: 0; + list-style: none; +} + +#pages a { + display: block; + line-height: 1.2em; + font-size: 1.2em; + color: #dedede; + border-bottom: 1px solid #1d272b; + padding: 1em 0.3em; +} + +#pages a:hover { + text-decoration: none; + background: #31424a; + color: #ffffff; +} + +/* -------- one-default, one-default-with-toc, one-default-with-sidebar, one-default-doc -------- */ + +.nav { + border-top: 1px solid #c5c5c5; + margin-top: 3em; + padding: 2em 0; + display: flex; + justify-content: center; + gap: 0.5em; + font-weight: bold; +} + +.nav a { + display: block; + background: #dedede; + border-radius: 6px; + padding: 0.2em 0.8em; + color: #151515; + width: 20%; + text-align: center; +} + +@media (max-width:600px) { + .nav a { + width: auto; + } +} + +/* -------- one-default-with-toc, one-default-doc -------- */ + +.toc { + display: flex; + justify-content: center; + margin-bottom: 1.8rem; + color: #d1d1d1; +} + +.toc > div { + padding: 0 1em; +} + +.toc a { + color: #d1d1d1; +} + +.toc > div > div:first-child { + text-decoration: underline 1px; + text-align: center; + font-size: 1.2em; + margin-bottom: 16px; +} + +/* --------- one-default-with-sidebar, one-default-doc --------- */ + +#sidebar-header { + color: #ffffff; + font-size: 2em; + font-weight: bold; + padding: 0 16px 0 16px; + background: #151515; + width: 100%; + height: 3.5rem; + position: fixed; + top: 0; + left: 0; + border-bottom: 1px solid #1d272b; + display: flex; + justify-content: center; + align-items: center; +} + +#sidebar-header > a { + color: inherit; + cursor: pointer; + text-decoration: none; +} + +#sidebar-header > a:visited { + color: inherit; +} + +#hamburger { + cursor: pointer; + height: 1em; + fill: #dedede; + display: none; + font-weight: normal; + margin-right: 0.3em; +} + +#sidebar-content { + margin: 3.5rem auto; + display: flex; + margin-left: auto; + margin-right: auto; + max-width: 1140px; + width: 100%; + padding: 1em 16px; +} + +#sidebar { + border-right: 2px solid #31424a; + top: 4.5rem; + position: sticky; + padding-top: 2.2em; + padding-bottom: 6em; + width: 250px; + max-height: 100vh; + overflow-y: auto; +} + +#sidebar a { + display: block; + color: #dedede; +} + +#sidebar a:hover { + text-decoration: none; +} + +#sidebar ul { + list-style: none; + padding:0; +} + +#sidebar li { + padding: 0.5em 0.6em; +} + +#sidebar li:hover { + background: #31424a; +} + +article { + padding: 0 1.5em; + max-width: 640px; + width: 100%; +} + +#sidebar-left { + width: 0; + height: 100%; + position: fixed; + z-index: 3; + top: 0; + left: 0; + transition: 0.25s; + background: #2c444f; + overflow: hidden; /* to make the children disappear when width is 0 */ + overflow-y: auto; +} + +#sidebar-left > div:first-child { + height: 3.5rem; + font-size: 2em; + font-weight: bold; + border-bottom: 1px solid #b8b8b8; + padding-left: 16px; + margin-bottom: 16px; + display: flex; + align-items: center; +} + +#sidebar-left > ul { + padding: 0 16px 0 16px; +} + +#sidebar-left > ul ul { + padding-left: 0.8em; + margin-left: 3px; + border-left: 1px solid #b8b8b8; +} + +#sidebar-left a { + color: #dedede; + text-decoration: none; +} + +#sidebar-left li { + padding: 0.5em 0; + list-style-type: none; +} + +#sidebar-main { + display: none; + top: 0; + right: 0; + width: 100%; + height: 100%; + position: fixed; + background: #080808; + opacity: 0.80; + z-index: 2; +} + +@media (max-width: 840px) { + #hamburger { + display: block; + } + #sidebar { + display: none; + } + #sidebar-content { + justify-content: center; + } + #sidebar-header { + justify-content: left; + } + article { + padding: 0; + } +} +" + "Default CSS style sheet. + +This style sheet is meant to be used with the default render functions +`one-default-home', `one-default-home-list-pages', `one-default', +`one-default-with-toc', `one-default-with-sidebar' and `one-default-doc'. + +See `one-default-new-project' and `one-default-add-css-file'.") + +(defvar one-default-org-content + "* one.el +:PROPERTIES: +:ONE: one-default-home +:CUSTOM_ID: / +:END: + +This is a new ~one.el~ project. + +If you don't know how ~one.el~ works, you can check the documentation at +https://one.tonyaldon.com. + +If you want to list all the pages on your website on the home page, +check [[#/blog/default-home-list-pages/][List all website's pages on the home page]]. + +* List all website's pages on the home page +:PROPERTIES: +:ONE: one-default-home-list-pages +:CUSTOM_ID: /blog/default-home-list-pages/ +:END: + +This page is rendered with the default render function +~one-default-home-list-pages~ specified in ~ONE~ org property which lists +below all the pages on the website. You can use it instead of +~one-default-home~ for your home page. + +* The default page +:PROPERTIES: +:ONE: one-default +:CUSTOM_ID: /blog/default/ +:END: + +This page is rendered with the default render function ~one-default~ +specified in ~ONE~ org property. + +** Do you want a table of content? + +As you can see, ~one-default~ doesn't add a table of content (TOC). If +you want a default render function that adds the TOC to the page you can +use the render function ~one-default-with-toc~ presented in [[#/blog/one-default-with-toc/][The default +page with a TOC]]. + +** Headline foo +*** Headline bar + +Some content. + +*** Headline baz + +#+BEGIN_SRC bash :results verbatim +tree +#+END_SRC + +#+RESULTS: +#+begin_example +. +├── assets +│ └── one.css +├── one.org +└── public + ├── blog + │ ├── default + │ │ └── index.html + │ ├── default-home-list-pages + │ │ └── index.html + │ ├── one-default-doc + │ │ └── index.html + │ ├── one-default-with-sidebar + │ │ └── index.html + │ └── one-default-with-toc + │ └── index.html + ├── index.html + └── one.css + +8 directories, 9 files +#+end_example + +* The default page with a TOC +:PROPERTIES: +:ONE: one-default-with-toc +:CUSTOM_ID: /blog/one-default-with-toc/ +:END: + +This page is rendered with the render function ~one-default-with-toc~ +specified in the org property ~ONE~. + +** Do you want a sidebar? + +Perhaps you want a sidebar listing all the pages on your website, as +many modern documentation sites do. If so, you can use the default +render function ~one-default-with-sidebar~ presented in [[#/blog/one-default-with-sidebar/][The default page +with a sidebar]]. + +** Headline foo +*** Headline bar + +Some content. + +*** Headline baz + +#+BEGIN_SRC bash :results verbatim +tree +#+END_SRC + +#+RESULTS: +#+begin_example +. +├── assets +│ └── one.css +├── one.org +└── public + ├── blog + │ ├── default + │ │ └── index.html + │ ├── default-home-list-pages + │ │ └── index.html + │ ├── one-default-doc + │ │ └── index.html + │ ├── one-default-with-sidebar + │ │ └── index.html + │ └── one-default-with-toc + │ └── index.html + ├── index.html + └── one.css + +8 directories, 9 files +#+end_example + +* The default page with a sidebar +:PROPERTIES: +:ONE: one-default-with-sidebar +:CUSTOM_ID: /blog/one-default-with-sidebar/ +:END: + +This page is rendered with the render function ~one-default-with-sidebar~ +specified in the org property ~ONE~. + +** Do you want a sidebar and a TOC? + +Perhaps you want a sidebar listing all the pages on your website and a +table of content, as many modern documentation sites do. If so, you +can use the default render function ~one-default-doc~ presented in [[#/blog/one-default-doc/][The +default page with TOC and sidebar]]. + +** Headline foo +*** Headline bar + +Some content. + +*** Headline baz + +#+BEGIN_SRC bash :results verbatim +tree +#+END_SRC + +#+RESULTS: +#+begin_example +. +├── assets +│ └── one.css +├── one.org +└── public + ├── blog + │ ├── default + │ │ └── index.html + │ ├── default-home-list-pages + │ │ └── index.html + │ ├── one-default-doc + │ │ └── index.html + │ ├── one-default-with-sidebar + │ │ └── index.html + │ └── one-default-with-toc + │ └── index.html + ├── index.html + └── one.css + +8 directories, 9 files +#+end_example + +* The default page with TOC and sidebar +:PROPERTIES: +:ONE: one-default-doc +:CUSTOM_ID: /blog/one-default-doc/ +:END: + +This page is rendered with the function ~one-default-doc~ specified +in the org property ~ONE~. + +** Do you want to know more about one.el? + +Check the documentation at https://one.tonyaldon.com. + +** Headline foo +*** Headline bar + +Some content. + +*** Headline baz + +#+BEGIN_SRC bash :results verbatim +tree +#+END_SRC + +#+RESULTS: +#+begin_example +. +├── assets +│ └── one.css +├── one.org +└── public + ├── blog + │ ├── default + │ │ └── index.html + │ ├── default-home-list-pages + │ │ └── index.html + │ ├── one-default-doc + │ │ └── index.html + │ ├── one-default-with-sidebar + │ │ └── index.html + │ └── one-default-with-toc + │ └── index.html + ├── index.html + └── one.css + +8 directories, 9 files +#+end_example +" + "Default org file to start a new `one.el' project. + +See `one-default-new-project'.") + +(defun one-default-add-css-file () + "Add default css file `./assets/one.css' with the content `one-default-css'. + +See `one-default-new-project'. + +See `one-default-home', `one-default-home-list-pages',`one-default', +`one-default-with-toc', `one-default-with-sidebar' and `one-default-doc'." + (interactive) + (make-directory "assets" t) + (with-temp-file "./assets/one.css" (insert one-default-css))) + +;;;###autoload +(defun one-default-new-project () + "Initialize a new project in the current directory with the default style. + +It is structured like this: + + . + ├── assets + │ └── one.css + └── one.org + +The content of the file `./assets/one.css' is `one-default-css'. +The content of the file `./one.org' is `one-default-org-content'. + +Once you've initialized this new `one.el' project, you can build it +calling `one-build' command while visiting the file `./one.org'. +This results in producing the website under the subdirectory `./public/'. + +See `one-render-pages'." + (interactive) + (one-default-add-css-file) + (with-temp-file "one.org" (insert one-default-org-content)) + (find-file "one.org")) + +(defun one-default-home (page-tree pages _global) + "Default render function to use in the home page. + +See `one-is-page' for the meaning of PAGE-TREE and PAGES. + +Also see `one-render-pages' and `one-default-css'." + (let* ((title (org-element-property :raw-value page-tree)) + (content (org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox nil)) + (website-name (one-default-website-name pages))) + (jack-html + "" + `(:html + (:head + (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1")) + (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css")) + (:title ,title)) + (:body + (:div.header ,website-name) + (:div.content + (:div/home ,content))))))) + +(defun one-default-home-list-pages (page-tree pages _global) + "Default render function to use in the home page that lists pages. + +See `one-is-page' for the meaning of PAGE-TREE and PAGES. + +Also see `one-render-pages' and `one-default-css'." + (let* ((title (org-element-property :raw-value page-tree)) + (content (org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox nil)) + (website-name (one-default-website-name pages)) + ;; All pages but the home pages + (pages-list (one-default-pages pages "/.+"))) + (jack-html + "" + `(:html + (:head + (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1")) + (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css")) + (:title ,title)) + (:body + (:div.header (:a (@ :href "/") ,website-name)) + (:div.content + (:div/home-list-pages ,content) + (:div/pages (:ul ,(reverse pages-list))))))))) + +(defun one-default (page-tree pages _global) + "Default render function. + +See `one-is-page' for the meaning of PAGE-TREE and PAGES. + +Also see `one-render-pages' and `one-default-css'." + (let* ((title (org-element-property :raw-value page-tree)) + (path (org-element-property :CUSTOM_ID page-tree)) + (content (org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox nil)) + (website-name (one-default-website-name pages)) + (nav (one-default-nav path pages))) + (jack-html + "" + `(:html + (:head + (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1")) + (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css")) + (:title ,title)) + (:body + (:div.header (:a (@ :href "/") ,website-name)) + (:div.content + (:div.title + ,(if (not (string= path "/")) + `(:div.title (:h1 ,title)) + '(:div.title-empty))) + ,content + ,nav)))))) + +(defun one-default-toc-component (headlines) + "Return the table of content from HEADLINES as a `jack-html' component. + +Return nil if HEADLINES is nil. + +See `one-default-list-headlines' and `one-default-toc'." + (when headlines + `(:div.toc + (:div + (:div "Table of content") + (:div ,(one-default-toc headlines)))))) + +(defun one-default-with-toc (page-tree pages _global) + "Default render function with a table of content. + +See `one-is-page' for the meaning of PAGE-TREE and PAGES. + +Also see `one-render-pages' and `one-default-css'." + (let* ((title (org-element-property :raw-value page-tree)) + (path (org-element-property :CUSTOM_ID page-tree)) + (content (org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox nil)) + (website-name (one-default-website-name pages)) + (headlines (cdr (one-default-list-headlines page-tree))) + (toc (one-default-toc-component headlines)) + (nav (one-default-nav path pages))) + (jack-html + "" + `(:html + (:head + (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1")) + (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css")) + (:title ,title)) + (:body + (:div.header (:a (@ :href "/") ,website-name)) + (:div.content + (:div.title + ,(if (not (string= path "/")) + `(:div.title (:h1 ,title)) + '(:div.title-empty))) + ,toc + ,content + ,nav)))))) + +(defun one-default-sidebar (page-tree pages _global &optional with-toc) + "Return a HTML string with PAGES listed in a sidebar. + +The arguments PAGE-TREE, PAGES and _GLOBAL are the same as +render functions take (See `one-is-page'). + +When WITH-TOC is non-nil, add the table of content of PAGE-TREE +in the HTML string. + +This function is meant to be used by `one-default-with-sidebar' +and `one-default-doc' render functions. + +See `one-render-pages', `one-default-css' and `one-default-pages'." + (let* ((title (org-element-property :raw-value page-tree)) + (path (org-element-property :CUSTOM_ID page-tree)) + (content (org-export-data-with-backend + (org-element-contents page-tree) + 'one-ox nil)) + (website-name (one-default-website-name pages)) + (pages-list (one-default-pages pages)) + (headlines (cdr (one-default-list-headlines page-tree))) + (toc (one-default-toc-component headlines)) + (nav (one-default-nav path pages))) + (jack-html + "" + `(:html + (:head + (:meta (@ :name "viewport" :content "width=device-width,initial-scale=1")) + (:link (@ :rel "stylesheet" :type "text/css" :href "/one.css")) + (:title ,title)) + (:body + ;; sidebar-left and sidebar-main are for small devices + (:div/sidebar-left (@ :onclick "followSidebarLink()") + (:div (:div "Pages")) + ,pages-list) + (:div/sidebar-main) + (:div/sidebar-header + (:svg/hamburger (@ :viewBox "0 0 24 24" :onclick "sidebarShow()") + (:path (@ :d "M21,6H3V5h18V6z M21,11H3v1h18V11z M21,17H3v1h18V17z"))) + (:a (@ :href "/") ,website-name)) + (:div/sidebar-content + (:div/sidebar ,pages-list) + (:article + ,(if (not (string= path "/")) + `(:div.title (:h1 ,title)) + '(:div.title-empty)) + ,(when with-toc toc) + ,content + ,nav))) + (:script " +function sidebarShow() { + if (window.innerWidth < 481) + document.getElementById('sidebar-left').style.width = '75vw'; + else { + document.getElementById('sidebar-left').style.width = 'min(300px, 34vw)'; + } + document.getElementById('sidebar-main').setAttribute('onclick', 'sidebarHide()'); + document.getElementById('sidebar-main').style.display = 'block'; +} +function sidebarHide() { + document.getElementById('sidebar-left').style.width = '0'; + document.getElementById('sidebar-main').style.display = 'none'; +} +"))))) + +(defun one-default-with-sidebar (page-tree pages global) + "Default render function with a sidebar listing PAGES. + +See `one-is-page' for the meaning of PAGE-TREE and GLOBAL. + +Also see `one-default-sidebar', `one-render-pages' and `one-default-css'." + (one-default-sidebar page-tree pages global)) + +(defun one-default-doc (page-tree pages global) + "Default render function with a sidebar listing PAGES and the table of content. + +See `one-is-page' for the meaning of PAGE-TREE and GLOBAL. + +Also see `one-default-sidebar', `one-render-pages' and `one-default-css'." + (one-default-sidebar page-tree pages global 'with-toc)) + +(defun one-default-pages (pages &optional filter) + "Return `jack-html' list of PAGES component. + +If FILTER is non-nil, a page is listed only when its path (value +of `:one-path' property) matches FILTER regexp. + +Evaluating the following form + + (one-default-pages + \\='((:one-title \"HOME\" :one-path \"/\") + (:one-title \"FOO-1\" :one-path \"/foo-1/\") + (:one-title \"FOO-2\" :one-path \"/foo-2/\"))) + +returns: + + (:ul + (:li (:a (@ :href \"/\") \"HOME\")) + (:li (:a (@ :href \"/foo-1/\") \"FOO-1\")) + (:li (:a (@ :href \"/foo-2/\") \"FOO-2\"))) + +And evaluating the following form with the filter \"/.+\" + + (one-default-pages + \\='((:one-title \"HOME\" :one-path \"/\") + (:one-title \"FOO-1\" :one-path \"/foo-1/\") + (:one-title \"FOO-2\" :one-path \"/foo-2/\")) + \"/.+\") + +returns a list which doesn't include the home page: + + (:ul + (:li (:a (@ :href \"/foo-1/\") \"FOO-1\")) + (:li (:a (@ :href \"/foo-2/\") \"FOO-2\")))" + (when-let ((li-items + (delq nil + (mapcar + (lambda (page) + (let ((href (plist-get page :one-path)) + (title (plist-get page :one-title))) + (when (string-match-p (or filter ".*") href) + `(:li (:a (@ :href ,href) ,title))))) + pages)))) + `(:ul ,@li-items))) + +(defun one-default-website-name (pages) + "Return the website's name. + +This corresponds to the title (value of the property `:one-title') +of the page in PAGES whom path is \"/\" (the home page). +Return nil if the home page is not part of PAGES. + +See `one-default-home', `one-default-home-list-pages', `one-default', +`one-default-with-toc', `one-default-with-sidebar' and `one-default-doc'." + (seq-some + (lambda (page) + (when (string= (plist-get page :one-path) "/") + (plist-get page :one-title))) + pages)) + +(defun one-default-nav (path pages) + "Return `jack-html' navigation component. + +The component is composed of 3 links: + +- \"PREV\": link to the page before the page whose `:one-path' is + equal to PATH in PAGES, +- \"RANDOM\": link to a random page picked in PAGES whose `:one-path' + is not equal to PATH, +- \"NEXT\": link to the page after the page whose `:one-path' is + equal to PATH in PAGES. + +For instance, evaluating the following form + + (one-default-nav \"/foo-2/\" + \\='((:one-path \"/\") + (:one-path \"/foo-1/\") + (:one-path \"/foo-2/\") + (:one-path \"/foo-3/\") + (:one-path \"/foo-4/\"))) + +returns (the \"RANDOM\" link could have been \"/\", \"/foo-1/\" or \"/foo-3/\") + + (:div.nav + (:a (@ :href \"/foo-1/\") \"PREV\") + (:a (@ :href \"/foo-4/\") \"RANDOM\") + (:a (@ :href \"/foo-3/\") \"NEXT\")) + +See `one-default', `one-default-with-toc', `one-default-with-sidebar' +and `one-default-doc'." + + (let* ((pages-not-path + (seq-remove + (lambda (page) (string= (plist-get page :one-path) path)) + pages))) + (when (<= 2 (length pages)) + (let (prev + (tail pages) + (random (seq-random-elt pages-not-path))) + (while (not (string= (plist-get (car tail) :one-path) path)) + (setq prev (car tail)) + (setq tail (cdr tail))) + `(:div.nav + ,(when prev `(:a (@ :href ,(plist-get prev :one-path)) "PREV")) + ,(when (<= 3 (length pages)) + `(:a (@ :href ,(plist-get random :one-path)) "RANDOM")) + ,(when-let ((next (plist-get (cadr tail) :one-path))) + `(:a (@ :href ,next) "NEXT"))))))) + +(defun one-default-list-headlines (tree) + "Return the list in order of the headlines in TREE. + +TREE is meant to be the parsed tree of an org buffer of a website +we want to build. See `one-parse-buffer'. + +Each headline in the returned list is a plist with the following +properties `:id',`:level' and `:title'. + +We can use the list returned by `one-default-list-headlines' to build +a table of content of TREE using `one-default-toc'. + +See `one-default-with-toc' and `one-default-doc'." + (org-element-map tree 'headline + (lambda (elt) + `(:id ,(org-element-property :one-internal-id elt) + :level ,(org-element-property :level elt) + :title ,(org-element-property :raw-value elt))))) + +(defun one-default-toc (headlines) + "Return table of content (TOC) as an HTML string. + +The TOC returned is computed from the ordered flat list of +headlines in HEADLINES where the level of each +headline is given by the property `:level'. +See `one-default-list-headlines'. + +For instance, evaluating the following form + + (one-default-toc + \\='((:level 1 :title \"foo\" :id \"id-foo\") + (:level 1 :title \"bar-1\" :id \"id-bar-1\") + (:level 2 :title \"bar-2\" :id \"id-bar-2\") + (:level 3 :title \"bar-3\" :id \"id-bar-3\") + (:level 2 :title \"bar-22\" :id \"id-bar-22\") + (:level 1 :title \"baz\" :id \"id-baz\"))) + +returns + + \" + + \" + +See `one-default-with-toc' and `one-default-doc'." + + (let* ((prev-level (1- (plist-get (car headlines) :level))) + (start-level prev-level) + (concat-n-times + (lambda (n str) (apply #'concat (make-list n str))))) + (concat + (mapconcat + (lambda (headline) + (let ((title (plist-get headline :title)) + (href (concat "#" (plist-get headline :id))) + (level (plist-get headline :level))) + (concat + (let* ((delta (- level prev-level)) + (times (if (> delta 0) (1- delta) (- delta)))) + (setq prev-level level) + (concat + (funcall concat-n-times + times (cond ((> delta 0) "\n
      \n
    • ") + ((< delta 0) "
    • \n
    \n"))) + (if (> delta 0) "\n
      \n
    • " "
    • \n
    • "))) + (concat "" title "")))) + headlines "") + (funcall concat-n-times (- prev-level start-level) "
    • \n
    \n")))) + +;;; one provide + +(provide 'one) +;;; one.el ends here