From bcdc9cee8ff2f70a88032ee75e953b21a7e3faac Mon Sep 17 00:00:00 2001 From: Erik Winter Date: Thu, 30 Sep 2021 06:54:42 +0200 Subject: [PATCH] Revert "moved stuff" This reverts commit 95c9a0116c82f3b1de569ea3978055aa0a520fe5. --- LICENSE.txt | 661 ++++++++++++++++++ Makefile | 3 - README.md | 7 + cmd/notes/main.go | 71 ++ cmd/notes/note/note.go | 76 ++ cmd/notes/note/notes.go | 33 + main.go => cmd/ssg/main.go | 0 .../ssg/resources}/ewnl/font/Amiko-Bold.ttf | Bin .../resources}/ewnl/font/Amiko-Regular.ttf | Bin .../resources}/ewnl/font/Amiko-SemiBold.ttf | Bin .../ewnl/font/Merriweather-Regular.otf | Bin .../ewnl/font/merriweather.regular.ttf | Bin .../resources}/ewnl/template/archive.gohtml | 0 .../ssg/resources}/ewnl/template/head.gohtml | 0 .../ssg/resources}/ewnl/template/list.gohtml | 0 .../ssg/resources}/ewnl/template/menu.gohtml | 0 .../ssg/resources}/ewnl/template/post.gohtml | 0 .../ssg/resources}/ewnl/template/rss.goxml | 0 .../resources}/ewnl/template/static.gohtml | 0 .../ssg/resources}/vkvnl/template/post.gohtml | 0 .../ssg/resources}/vkvnl/template/rss.goxml | 0 {site => cmd/ssg/site}/config.go | 0 {site => cmd/ssg/site}/html.go | 0 {site => cmd/ssg/site}/html_test.go | 0 {site => cmd/ssg/site}/meta.go | 0 {site => cmd/ssg/site}/meta_test.go | 0 {site => cmd/ssg/site}/post.go | 0 {site => cmd/ssg/site}/post_test.go | 0 {site => cmd/ssg/site}/posts.go | 0 {site => cmd/ssg/site}/posts_test.go | 0 {site => cmd/ssg/site}/renderewnl.go | 0 {site => cmd/ssg/site}/rendervkvnl.go | 0 {site => cmd/ssg/site}/site.go | 0 {site => cmd/ssg/site}/sites.go | 0 {site => cmd/ssg/site}/xml.go | 0 ...-tiny-subset-of-asciidoc-for-blogging.adoc | 128 ++++ doc/code-walkthrough.adoc | 81 +++ ...t-my-own-shitty-static-site-generator.adoc | 85 +++ go.mod | 6 +- go.sum | 4 +- pkg/adoc/adoc.go | 75 ++ pkg/adoc/adoc_test.go | 110 +++ pkg/adoc/block.go | 55 ++ pkg/adoc/block_test.go | 134 ++++ pkg/adoc/inline.go | 40 ++ pkg/adoc/inline_test.go | 50 ++ pkg/adoc/parser.go | 238 +++++++ pkg/adoc/parser_test.go | 220 ++++++ 48 files changed, 2069 insertions(+), 8 deletions(-) create mode 100644 LICENSE.txt delete mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/notes/main.go create mode 100644 cmd/notes/note/note.go create mode 100644 cmd/notes/note/notes.go rename main.go => cmd/ssg/main.go (100%) rename {resources => cmd/ssg/resources}/ewnl/font/Amiko-Bold.ttf (100%) rename {resources => cmd/ssg/resources}/ewnl/font/Amiko-Regular.ttf (100%) rename {resources => cmd/ssg/resources}/ewnl/font/Amiko-SemiBold.ttf (100%) rename {resources => cmd/ssg/resources}/ewnl/font/Merriweather-Regular.otf (100%) rename {resources => cmd/ssg/resources}/ewnl/font/merriweather.regular.ttf (100%) rename {resources => cmd/ssg/resources}/ewnl/template/archive.gohtml (100%) rename {resources => cmd/ssg/resources}/ewnl/template/head.gohtml (100%) rename {resources => cmd/ssg/resources}/ewnl/template/list.gohtml (100%) rename {resources => cmd/ssg/resources}/ewnl/template/menu.gohtml (100%) rename {resources => cmd/ssg/resources}/ewnl/template/post.gohtml (100%) rename {resources => cmd/ssg/resources}/ewnl/template/rss.goxml (100%) rename {resources => cmd/ssg/resources}/ewnl/template/static.gohtml (100%) rename {resources => cmd/ssg/resources}/vkvnl/template/post.gohtml (100%) rename {resources => cmd/ssg/resources}/vkvnl/template/rss.goxml (100%) rename {site => cmd/ssg/site}/config.go (100%) rename {site => cmd/ssg/site}/html.go (100%) rename {site => cmd/ssg/site}/html_test.go (100%) rename {site => cmd/ssg/site}/meta.go (100%) rename {site => cmd/ssg/site}/meta_test.go (100%) rename {site => cmd/ssg/site}/post.go (100%) rename {site => cmd/ssg/site}/post_test.go (100%) rename {site => cmd/ssg/site}/posts.go (100%) rename {site => cmd/ssg/site}/posts_test.go (100%) rename {site => cmd/ssg/site}/renderewnl.go (100%) rename {site => cmd/ssg/site}/rendervkvnl.go (100%) rename {site => cmd/ssg/site}/site.go (100%) rename {site => cmd/ssg/site}/sites.go (100%) rename {site => cmd/ssg/site}/xml.go (100%) create mode 100644 doc/a-tiny-subset-of-asciidoc-for-blogging.adoc create mode 100644 doc/code-walkthrough.adoc create mode 100644 doc/why-i-built-my-own-shitty-static-site-generator.adoc create mode 100644 pkg/adoc/adoc.go create mode 100644 pkg/adoc/adoc_test.go create mode 100644 pkg/adoc/block.go create mode 100644 pkg/adoc/block_test.go create mode 100644 pkg/adoc/inline.go create mode 100644 pkg/adoc/inline_test.go create mode 100644 pkg/adoc/parser.go create mode 100644 pkg/adoc/parser_test.go diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/Makefile b/Makefile deleted file mode 100644 index e329429..0000000 --- a/Makefile +++ /dev/null @@ -1,3 +0,0 @@ - -test: - go test ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..73a7881 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# My Shitty Static Site Generator + +Although there are many SSG's out there that work perfectly fine, I still went through the effort of creating my own program to generate [my site](https://erikwinter.nl/). And after doing that I wrote a lot of text explaining why. As it turns out, there are some underlying motivations for creating your own tools that I think are interesting for non-programmers too. If you want to read more about it, check the [essay on my site](https://erikwinter.nl/articles/2020/why-i-built-my-own-shitty-static-site-generator/). There is also a [note on the markup used](https://erikwinter.nl/articles/2020/a-tiny-subset-of-asciidoc-for-blogging/). I will create some more documentation later. [Here](https://erikwinter.nl/articles/2021/shitty-ssg-code-walkthrough/) is a high level description of the code. + +But, one of the points to do it was to have the freedom to put my content anywhere I want. So I was able to bundle everything together and you can find de source files out of which the mentioned posts are generated right in this repository, in de `doc` folder. + +As noted in the project name, this software is shitty. Don't use it. I put it here for inspiration and for educational purposes. diff --git a/cmd/notes/main.go b/cmd/notes/main.go new file mode 100644 index 0000000..305b0cb --- /dev/null +++ b/cmd/notes/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "path/filepath" + "strconv" + + "git.sr.ht/~ewintr/shitty-ssg/cmd/notes/note" +) + +func main() { + notesPath := os.Getenv("NOTES_PATH") + if notesPath == "" { + log.Fatal("no notes directory to parse") + } + + if len(os.Args) != 2 { + log.Fatal("exactly one search term is required as parameter") + } + searchTerm := os.Args[1] + + var notes note.Notes + if err := filepath.Walk(notesPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && filepath.Ext(path) == ".adoc" { + if err := notes.AddFileNote(path); err != nil { + return nil + } + } + + return nil + }); err != nil { + log.Fatal(err) + } + + notes = notes.FilterByTerm(searchTerm) + if len(notes) == 0 { + fmt.Println("Found nothing.") + + return + } + + for i, n := range notes { + fmt.Printf("%d) %s\n", i, n.Title) + } + reader := bufio.NewReader(os.Stdin) + r, _, err := reader.ReadRune() + if err != nil { + log.Fatal(err) + } + c := string(r) + switch c { + case "q": + return + default: + i, err := strconv.Atoi(c) + if err != nil { + log.Fatal(err) + } + if i < 0 || i >= len(notes) { + fmt.Println("number out of range") + return + } + fmt.Printf("\n\n%s\n\n%s\n\n", notes[i].Title, notes[i].Content) + } +} diff --git a/cmd/notes/note/note.go b/cmd/notes/note/note.go new file mode 100644 index 0000000..19b7cdd --- /dev/null +++ b/cmd/notes/note/note.go @@ -0,0 +1,76 @@ +package note + +import ( + "strings" + + "git.sr.ht/~ewintr/shitty-ssg/pkg/adoc" +) + +type Kind string + +type Tag string + +const ( + KIND_NOTE = Kind("note") + KIND_PRIVATE_NOTE = Kind("private_note") + KIND_WORK_NOTE = Kind("work_note") + KIND_INVALID = Kind("") +) + +func mapKind(akind adoc.Kind) Kind { + nkind, ok := map[adoc.Kind]Kind{ + adoc.KIND_NOTE: KIND_NOTE, + adoc.KIND_PRIVATE_NOTE: KIND_PRIVATE_NOTE, + adoc.KIND_WORK_NOTE: KIND_WORK_NOTE, + }[akind] + if !ok { + return KIND_INVALID + } + + return nkind +} + +type Note struct { + doc *adoc.ADoc + Title string + Kind Kind + Tags []Tag + Content string +} + +func NewNote(doc *adoc.ADoc) *Note { + var paragraphs []string + for _, be := range doc.Content { + paragraphs = append(paragraphs, be.Text()) + } + content := strings.Join(paragraphs, "\n\n") + + var tags []Tag + for _, t := range doc.Tags { + tags = append(tags, Tag(t)) + } + + return &Note{ + doc: doc, + Kind: mapKind(doc.Kind), + Title: doc.Title, + Tags: tags, + Content: content, + } +} + +func (n *Note) Contains(term string) bool { + for _, t := range n.Tags { + if strings.ToLower(string(t)) == strings.ToLower(term) { + return true + } + } + + for _, w := range strings.Split(n.Content, " ") { + if strings.ToLower(w) == strings.ToLower(term) { + return true + } + } + + return false +} diff --git a/cmd/notes/note/notes.go b/cmd/notes/note/notes.go new file mode 100644 index 0000000..fc0d018 --- /dev/null +++ b/cmd/notes/note/notes.go @@ -0,0 +1,33 @@ +package note + +import ( + "io/ioutil" + + "git.sr.ht/~ewintr/shitty-ssg/pkg/adoc" +) + +type Notes []*Note + +func (n *Notes) AddFileNote(fPath string) error { + content, err := ioutil.ReadFile(fPath) + if err != nil { + return err + } + note := NewNote(adoc.New(string(content))) + if note.Kind != KIND_INVALID { + *n = append(*n, note) + } + + return nil +} + +func (n *Notes) FilterByTerm(term string) Notes { + found := Notes{} + for _, note := range *n { + if note.Contains(term) { + found = append(found, note) + } + } + + return found +} diff --git a/main.go b/cmd/ssg/main.go similarity index 100% rename from main.go rename to cmd/ssg/main.go diff --git a/resources/ewnl/font/Amiko-Bold.ttf b/cmd/ssg/resources/ewnl/font/Amiko-Bold.ttf similarity index 100% rename from resources/ewnl/font/Amiko-Bold.ttf rename to cmd/ssg/resources/ewnl/font/Amiko-Bold.ttf diff --git a/resources/ewnl/font/Amiko-Regular.ttf b/cmd/ssg/resources/ewnl/font/Amiko-Regular.ttf similarity index 100% rename from resources/ewnl/font/Amiko-Regular.ttf rename to cmd/ssg/resources/ewnl/font/Amiko-Regular.ttf diff --git a/resources/ewnl/font/Amiko-SemiBold.ttf b/cmd/ssg/resources/ewnl/font/Amiko-SemiBold.ttf similarity index 100% rename from resources/ewnl/font/Amiko-SemiBold.ttf rename to cmd/ssg/resources/ewnl/font/Amiko-SemiBold.ttf diff --git a/resources/ewnl/font/Merriweather-Regular.otf b/cmd/ssg/resources/ewnl/font/Merriweather-Regular.otf similarity index 100% rename from resources/ewnl/font/Merriweather-Regular.otf rename to cmd/ssg/resources/ewnl/font/Merriweather-Regular.otf diff --git a/resources/ewnl/font/merriweather.regular.ttf b/cmd/ssg/resources/ewnl/font/merriweather.regular.ttf similarity index 100% rename from resources/ewnl/font/merriweather.regular.ttf rename to cmd/ssg/resources/ewnl/font/merriweather.regular.ttf diff --git a/resources/ewnl/template/archive.gohtml b/cmd/ssg/resources/ewnl/template/archive.gohtml similarity index 100% rename from resources/ewnl/template/archive.gohtml rename to cmd/ssg/resources/ewnl/template/archive.gohtml diff --git a/resources/ewnl/template/head.gohtml b/cmd/ssg/resources/ewnl/template/head.gohtml similarity index 100% rename from resources/ewnl/template/head.gohtml rename to cmd/ssg/resources/ewnl/template/head.gohtml diff --git a/resources/ewnl/template/list.gohtml b/cmd/ssg/resources/ewnl/template/list.gohtml similarity index 100% rename from resources/ewnl/template/list.gohtml rename to cmd/ssg/resources/ewnl/template/list.gohtml diff --git a/resources/ewnl/template/menu.gohtml b/cmd/ssg/resources/ewnl/template/menu.gohtml similarity index 100% rename from resources/ewnl/template/menu.gohtml rename to cmd/ssg/resources/ewnl/template/menu.gohtml diff --git a/resources/ewnl/template/post.gohtml b/cmd/ssg/resources/ewnl/template/post.gohtml similarity index 100% rename from resources/ewnl/template/post.gohtml rename to cmd/ssg/resources/ewnl/template/post.gohtml diff --git a/resources/ewnl/template/rss.goxml b/cmd/ssg/resources/ewnl/template/rss.goxml similarity index 100% rename from resources/ewnl/template/rss.goxml rename to cmd/ssg/resources/ewnl/template/rss.goxml diff --git a/resources/ewnl/template/static.gohtml b/cmd/ssg/resources/ewnl/template/static.gohtml similarity index 100% rename from resources/ewnl/template/static.gohtml rename to cmd/ssg/resources/ewnl/template/static.gohtml diff --git a/resources/vkvnl/template/post.gohtml b/cmd/ssg/resources/vkvnl/template/post.gohtml similarity index 100% rename from resources/vkvnl/template/post.gohtml rename to cmd/ssg/resources/vkvnl/template/post.gohtml diff --git a/resources/vkvnl/template/rss.goxml b/cmd/ssg/resources/vkvnl/template/rss.goxml similarity index 100% rename from resources/vkvnl/template/rss.goxml rename to cmd/ssg/resources/vkvnl/template/rss.goxml diff --git a/site/config.go b/cmd/ssg/site/config.go similarity index 100% rename from site/config.go rename to cmd/ssg/site/config.go diff --git a/site/html.go b/cmd/ssg/site/html.go similarity index 100% rename from site/html.go rename to cmd/ssg/site/html.go diff --git a/site/html_test.go b/cmd/ssg/site/html_test.go similarity index 100% rename from site/html_test.go rename to cmd/ssg/site/html_test.go diff --git a/site/meta.go b/cmd/ssg/site/meta.go similarity index 100% rename from site/meta.go rename to cmd/ssg/site/meta.go diff --git a/site/meta_test.go b/cmd/ssg/site/meta_test.go similarity index 100% rename from site/meta_test.go rename to cmd/ssg/site/meta_test.go diff --git a/site/post.go b/cmd/ssg/site/post.go similarity index 100% rename from site/post.go rename to cmd/ssg/site/post.go diff --git a/site/post_test.go b/cmd/ssg/site/post_test.go similarity index 100% rename from site/post_test.go rename to cmd/ssg/site/post_test.go diff --git a/site/posts.go b/cmd/ssg/site/posts.go similarity index 100% rename from site/posts.go rename to cmd/ssg/site/posts.go diff --git a/site/posts_test.go b/cmd/ssg/site/posts_test.go similarity index 100% rename from site/posts_test.go rename to cmd/ssg/site/posts_test.go diff --git a/site/renderewnl.go b/cmd/ssg/site/renderewnl.go similarity index 100% rename from site/renderewnl.go rename to cmd/ssg/site/renderewnl.go diff --git a/site/rendervkvnl.go b/cmd/ssg/site/rendervkvnl.go similarity index 100% rename from site/rendervkvnl.go rename to cmd/ssg/site/rendervkvnl.go diff --git a/site/site.go b/cmd/ssg/site/site.go similarity index 100% rename from site/site.go rename to cmd/ssg/site/site.go diff --git a/site/sites.go b/cmd/ssg/site/sites.go similarity index 100% rename from site/sites.go rename to cmd/ssg/site/sites.go diff --git a/site/xml.go b/cmd/ssg/site/xml.go similarity index 100% rename from site/xml.go rename to cmd/ssg/site/xml.go diff --git a/doc/a-tiny-subset-of-asciidoc-for-blogging.adoc b/doc/a-tiny-subset-of-asciidoc-for-blogging.adoc new file mode 100644 index 0000000..0b41b57 --- /dev/null +++ b/doc/a-tiny-subset-of-asciidoc-for-blogging.adoc @@ -0,0 +1,128 @@ += A Tiny Subset of Asciidoc for Blogging +Erik Winter +2020-12-01 +:kind: essay +:public: yes +:tags: asciidoc +:project: shitty-ssg +:language: EN + +As promised in /articles/2020/why-i-built-my-own-shitty-static-site-generator/[my earlier post] on why I decided to build my own Shitty Static Site Generator, I’ll explain the markup format that I use for the posts on this site. The goal was to find a subset of Asciidoc that would be easy to parse myself, but would still result in valid documents, so that they can be processed by all other tools that understand Asciidoc without any problem. For instance, I wanted the syntax coloring in my editor to ‘just work’. + +In order to achieve that, I selected the following parts from the Asciidoc specification. They are categorized by the types of elements as they are used in a HTML page: + +* A header +* Block elements +* Inline elements + +Everything that is not described below will not work. I may add new stuff in the future, if I feel it is needed. If I do so, I will update this page. + +The https://erikwinter.nl/articles/2021/shitty-ssg-code-walkthrough/[code walkthrough] of the SSG contains a short description of how the parser works. + +== Header + +A header consists of the title of the post and various fields of metadata. It looks like this: + +---- += An Example Post +Erik Winter +2020-12-01 +:kind: note +:public: yes +:language: EN +:project: shitty-ssg +:tags: asciidoc, ssg, parser +​ +---- + +It starts with the title on the first line, which is prefixed with `=`, and it ends with an empty line. + +A the attributes of a post are defined as a key-value pair. Each pair gets its own line. The following attributes are supported: + +* `kind` The type of post. Either `note`, `story` or `article`. +* `public` Can be exported to sites/systems that other people see. Values are `true`/`yes`, `false`/`no`. Defaults to `false`. +* `language` A two letter country code. Only `NL` will have a visible effect, by showing the Dutch flag in various places. +* `project` Posts that belong to a personal project. +* `tags` A comma separated list of tags. + +Author and publishing date are the only two lines that don’t start with a semicolon. If the line has the format `YYYY-MM-DD` it is assumed to be the publishing date, the rest is considered the author. That last value is not used on this site, but useful for other tools. + +Lines that are not recognized are ignored. Later lines overwrite previous ones if they define the same thing. + +== Block Elements + +A post constist of multiple blocks that are separated by a blank line. Currently the following blocks are supported: + +* Subtitle and Subsubtitle +* List +* Code block +* Paragraph + +=== Subtitle and Subsubtitle + +---- +== A Subtitle +---- + +Start with a `==`, or a `===`, followed by a space. They translate to `

` and `

` elements. + +=== List + +---- +* List item one +* List item two +* List item three +---- + +Lists items start with an asterisk and are not separated by a blank line. The text of the item is parsed as a paragraph. + +=== Code Block + +---- +​---- +func (d *Dummy) DoSomething() string { + return “No. I don’t want to.” +} + +func (d *Dummy) SudoDoSomething() string { + return “Ok. If you insist, I’ll put my objections aside for a moment.” +} +​---- +---- + +Code blocks start and end width four dashes on an empty line. The text between is put in `` tags and result in monospaced font with whitespace preserved. Empty lines are allowed. + +=== Paragraph + +A paragraph is a line of text that gets parsed to find inline elements. + +== Inline Elements + +Currently the following types are recognized: + +* Strong and emphasis +* Link +* Inline code + +=== Strong and Emphasis + +---- +a text with some *strong* words, that I’d like to _emphasize_. +---- + +It is possible to combine the two for the same text. + +=== Link + +---- +Check out this https://erikwinter.nl/[awesome website] now! +---- + +Whatever is between the opening bracket and the first space before that is taken as URL, so both absolute and relative links without domain are possible. + +=== Inline Code + +---- +Some text with `code` in it. +---- + diff --git a/doc/code-walkthrough.adoc b/doc/code-walkthrough.adoc new file mode 100644 index 0000000..e2743a9 --- /dev/null +++ b/doc/code-walkthrough.adoc @@ -0,0 +1,81 @@ += Shitty SSG: Code Walkthrough +Erik Winter +2021-03-09 +:kind: article +:public: yes +:tags: golang, asciidoc +:project: shitty-ssg +:language: en + +One would expect a walk through the code of a self proclaimed “shitty” software project not to be a pleasant read. I promise that it is not that bad. Earlier I https://erikwinter.nl/articles/2020/why-i-built-my-own-shitty-static-site-generator/[described] why I built this static site generator and the https://erikwinter.nl/articles/2020/a-tiny-subset-of-asciidoc-for-blogging/[Asciidoc subset] that it uses as a markup language. Here I will describe the code that transforms these documents into a website. Later I will go more into detail on how the flexibility of this generator is used to create and publish to multiple sites from multiple sources in one go. + +Originally I made the SSG to create one site. But it turned out to be not that hard to generalize it and now it generates: + +* My personal site: https://erikwinter.nl[https://erikwinter.nl] +* A second site with just the Dutch short stories: https://vrijkorteverhalen.nl _edit: I’ve decomissioned that site._ +* A very basic note viewer for the terminal. As it felt weird to fire up a browser and go on the internet, just to view a note that was already sitting on my hard drive a few directories away. + +In the future I might add an export for a Gopher or a Gemini site and if the collection of notes grows I’ll probably improve the search somewhat. Now it only finds notes on tags or whole words. It displays them just in plain text. I will also probably dive more into shell escape codes and such to see if the apperance can be improved. + +The code itself can be found https://git.sr.ht/~ewintr/shitty-ssg[here]. + +== Directories + +So how does it work? On a very high level, the directory structure looks like this: + +---- +. +├── cmd +│   ├── notes +│   └── ssg +└── pkg +    └── adoc +---- + +I arrived there by following https://erikwinter.nl/notes/2021/my-default-golang-directory-structure/[the conventions for folders] I always follow when starting a new Go program. In this case there are two binaries. One is the notes program, the other the site generator. That one generator can generate both sites, depending on the configuration its fed. + +Everything is built on the `adoc` package, that parses Asciidoc files in the most clumsy way possible. You give it a string, it gives you back a pointer to an `ADoc` struct that holds all information found in the string. + +There is no `internal` directory. That might have been a mistake. A possible use for this directory is to store packages that contain domain logic that is specific to the programs in the repository. I thought there would not really be any domain specific thing in this project. We read a file, let `adoc` parse the contents and once we know that we can go straight to the formatting of HTML, I thought. But it turns out there are some rules that must be applied and that is a translation on the kind of document we are processing. + +There all kinds of documents. Notes, stories, tutorials, etc. These are indicated by the `:kind:` metadata property in the Asciidoc document. On the site, there are also different kinds of posts, but these don’t map one on one. Essays and tutorials get lumped together in the more generic article kind on my personal site, fo instance. Once I had the note viewer, I also started to store private notes into the system. But these private notes should only show up in the viewer, not on any site. + +In the end, I thought that this translation of “source kind” to “output kind” was specific to the thing we’re outputting to, so the translation should be done at the level of the programs, in `cmd/notes` and `cmd/ssg`. But it feels weird and somehow repetitive. Anyway. It works now. The public/private thing still irks me though. + +== Asciidoc parsing + +As said, the parsing of the documents is clumsy and the most amateurish part of this code. I was interested in creating a parser by hand already before this project and enjoyed, for instance, reading https://blog.gopheracademy.com/advent-2014/parsers-lexers/[Handwritten Parser and Lexers in Go] by Ben Johnson. Following his lead, I created a lexer and a parser and started writing a bunch of unit tests to implement the rules and then... I figured out two things. + +The first was that I am not a very smart person and that the recursion in the parser kept being confusing. My mistake here was probably that I did not do any background reading on the different types of parsers and how they work. Most documents online talk about grammar and generated parsers, which was not what I wanted. I just wanted to build something from the ground up, like in the article of Ben Johnson. But there are all kinds of possible strategies and structures to parse a text. The blog posts I used for research did mention that, but I kind of glossed over it. I thought I would just take “the easiest” version, without checking whether everyone had the same definition of “easiest”. It did not really help that the examples I found where in different programming languages that were all new to me. In short, I did not have a clear picture of how I wanted to do it. + +The second thing was reading the https://asciidoc.org/userguide.html#_text_formatting[Asciidoc specification]. Trying to read it. Because I quickly discovered that it was a lot bigger than I remembered and that it contains a lot of rules. At the start I thought I would be able to unit test myself out of it, but together with the first point I lost faith that I was going to accomplish that. + +So I stopped programming for while and started to think about that. This was coming way too big for a side project. Then it hit me. While the whole specification is huge, the portion that I need for my generator is small. I only needed the parts that map to the simple HTML I was going to generate. And looking at HTML, I could break it up even further. HTML consists of block level elements and they contain inline elements. The inline elements were already working in the failed version, the block elements are basically the things separated by an empty line in the document. I might not be smart enough to write a complete parser, I do know how to split a text on empty lines. + +In practice, this was a little more complicated. Code blocks can have empty lines, for instance. A List is a block element that contains list items, which are also block elements. But it was not hard to add those once the basics were there. It does not make for the nicest code though. + +The inline parsing is just a big state machine with a lot of variables and lots of fiddling to get te behavior I wanted. There are bugs, but it’s usable for me. + +== Generating a site + +The generator itself consists of two parts. There is a `Post` type and a `Posts` collection type. The latter supports filtering, sorting, etc. by method chaining. The second part is a type for render functions: + +---- +type TemplateConfig struct { + ... + Render func(targetPath string, tpl *template.Template, posts Posts, staticPages []*StaticPage) error +} +---- + +This takes a go text template, the big `Posts` collection and a target path. The collection is filtered, sorted, etc. in the render function and is then fed to the template. The template get rendered to the target path. + +So what happens is: + +* At startup, the content folders get scanned for parsable Asciidoc files. +* These parsed documents are put in one big collection of `Posts`. +* Each site has its own list of render functions, that are called one by one with the `Posts`. +* The render function renders a portion of the site and stores it at the target path. + +The `StaticPages` work similar. These are the content parts that are not a post, but still are specific to the site. For instance, the Other and About pages on https://erikwinter.nl[erikwinter.nl]. The HTML for that is just stored in a separate folder that is read at startup. + +And that is enough. I haven’t looked back since. diff --git a/doc/why-i-built-my-own-shitty-static-site-generator.adoc b/doc/why-i-built-my-own-shitty-static-site-generator.adoc new file mode 100644 index 0000000..9d8db5a --- /dev/null +++ b/doc/why-i-built-my-own-shitty-static-site-generator.adoc @@ -0,0 +1,85 @@ += Why I Built My Own Shitty Static Site Generator +Erik Winter +2020-11-09 +:kind: article +:public: yes +:tags: productivity, asciidoc, hugo +:language: EN +:project: shitty-ssg + +_NOTE: This post generated some interesting discussions elsewhere. See the comments on https://news.ycombinator.com/item?id=25227181[hackernews] and https://old.reddit.com/r/programming/comments/k05oy9/[reddit] to read more opinions on the subject._ + +On the internet, there is no shortage of good quality https://jamstack.org/generators/[static site generators] (SSG’s) that you can download for free. https://gohugo.io/[Hugo], https://jekyllrb.com/[Jekyll], and hundreds of others are readily available. And they work. You can build all kinds of sites with them. I know that, because I’ve used some of them. Hugo was the driving force behind this website until very recently. Despite that, when I tried to add a new section a while ago, I got rather frustrated with it and decided to build my own generator. It turned out to be a very pleasant experience and not just because I like to program things. + +While working on it, I discovered some of the deeper motivations that drove me to undertake this project. On the surface it would seem an odd thing to do, because it takes a lot of time and it appears to offer little benefit. I did not create any new or spectacular functionality. If you click around this site and think: Hey, but I could totally make this with , then you would probably be right. But that is not the point. There are certain advantages to making it all yourself and I suspect that these advantages trancend the subject of SSG’s and programming. My speculation is that exploring this direction might also be interesting for people who do not like to program, or maybe even for those who don’t like computers that much at all. + +So, why choose this project out so many others that all sound so much more interesting? It is easy to summarize, but without some context it may sound a bit abstract. The real reason for all this work is that I think that a personal site should be personal and to make it personal, one should solely be guided by one’s intuition and not by the mental models of available tools and the restrictions they impose on your thoughts. + +That probably sounded vague and perhaps a little far fetched. After all, as long as you can write the words you want to write, draw the lines you want to draw, you are not limited in your creativity, right? Maybe you are. To make the point more tangible, let me expand on my own situation for a bit. I wil focus on Hugo, since that is the SSG I know best. But the same principles hold for other generators. All other tools even, I believe. + +== Metadata and Organising Thoughts + +As said, the list of available static site generators is endless. But somehow they all seem to focus on https://en.wikipedia.org/wiki/Markdown[Markdown] as markup language to write your posts in. Markdown is very easy to learn and that is probably the reason why it is so popular. Unfortunately, it is a pretty bad markup language for this use case, as it is very incomplete. It is not really a language. Markdown is better seen as a bunch of shortcuts to simplify writing a few common HTML tags. Out of the box you can only sort of markup the body of a document with it. Titles, paragraphs, lists, etc. But not more than that. As we are dealing with a website, shortcuts for HTML tags can be useful, but we need more. For instance, one also needs metadata, like tags, publishing dates, etc. You do want the latest post to be at the top of the newsfeed, right? Then we must find a way to indicate the time when a post was published. + +In Hugo this is solved with the awkward concept of https://gohugo.io/content-management/front-matter/[frontmatter]. At the top of each Markdown file, one needs to add a block of text that is not Markdown, but another format. You can pick either YAML, TOML, or JSON. In that block you can specify the things I mentioned. Publishing date, author, category, etc. It is all very flexible, you can even define your own metadata. So you can definitely make a nice looking site out of this. + +But the downside is that any blog or article you write is now tied to the system that you use to publish it. Frontmatter is not part of Markdown. It is part of Hugo. So other Markdown tools will not be able to extract that info. Only Hugo can. So if you, like me, like to write all kinds of things, and you want to have some of your writings to end up on your site and others not, a decision that is perhaps made even only after you’ve done the writing, then you have just created a problem. Because now you have to make two piles of writing. One pile for texts that will be published on your site, and another for text that you want to export to, say, PDF. But wait, maybe you _also_ want to compile a selection of your stories and make it into an ebook. And maybe you are really proud of a piece and you want to publish it all three ways at once. It becomes a bit weird, and also very impractical, to store your work then. Are you going to change the document format each time you want to make an export? Are you going to make multiple copies? What happens if you find a spelling error later? This all quickly becomes a big mess. + +Ideally, I want to have one folder for storing all my writing. I want to organize that folder the way I think fits best with the content. Then I want to point the site generator to that folder and it should figure out the rest by itself. What needs to be published on my site? Where should it end up? The metadata should be all that is needed to figure it out. And I want the same thing to be true for all other publishers, generators, indexers, etc. that I use, or may want to use in the future. The only solution is then to store your texts in open, tool agnostic document format that can hold all the relevant info. Preferably a plain text format too. Because that part of Markdown I do like. Using simple text editors, Git version control, yes, give me that. + +Enter https://asciidoc.org/[Asciidoc]. A Markdown so structured and complete that you can write a whole book in it. Yet is has the same simple way of adding markup and it looks very similar. I wrote https://erikwinter.nl/articles/2020/a-tiny-subset-of-asciidoc-for-blogging/[another post] on how I used a subset of Asciidoc to make my generator. The point I want to make here is that a simple, in my opinion very reasonable requirement to not want to be forced to reorganise and duplicate my files in an illogical way, already rules out 90% of the available tools. And, conversely, that merely by adopting one of those existing tools, you have suddenly become a bit restricted in the way you can think about your creative work. + +Think about it. The moment you start anything, the moment where the ideas in your head are not more than undefined glimpses of images or feelings. The moment you have to concentrate really hard to not let the fleeting, still wordless impressions slip. Blink your eyes one time too many and they will be lost, floated away. On _that_ very moment, you get bothered by the question “Where should this end up, once it is finished? Is it a note for my diary, or a book?” That is totally backwards. That should not be the first question about your work, but the last. Not everyone is the same, but for me this upfront question is limiting. Thoughts that are not mature enough to be categorized, are forced to materialize in anyway, so you can put them in the right bucket. And then they slip away. + +== Blank Page Instead of Puzzles and Pieces + +By now, some pepole might be thinking: yes, that is all fine, but some generators, like Hugo _can_ use Asciidoc as input and you _can_ set an alternative content path. Surely you can work something out here and configure things the way it suits you? + +Well, yes, from the face of it, it might be possible to cobble something up. But that is not going to end up well. Going that route, you will get dragged down in an endless cycle of figuring out options and options of options and in the end, you will be happy to get anything on the screen at all. + +Let’s start simple. Some generators say they support Asciidoc, but they don’t do that nativly. At least the ones I’ve seen. That is, you have to install another piece of software to get the functionality. In this case https://asciidoctor.org/[Asciidoctor]. (And Asciidocter in turn requires Ruby, but I think it ends there.) Then the two pieces must be configured to work together and this must be done on overy device you want to use. This is what developers call a dependency and they are to be avoided wherever possible, as they require you to do work, just to keep things running as they are. At the time of writing you can read https://gohugo.io/content-management/formats/#additional-formats-through-external-helpers[this] in the Hugo documentation on how to configure Asciidoc: + + +_“AsciiDoc implementation EOLs in Jan 2020 and is no longer supported. AsciiDoc development is being continued under Asciidoctor. The format AsciiDoc remains of course. Please continue with the implementation Asciidoctor.”_ + +So what this says is that the people behind Hugo saw that a piece of software they relied on, AsciiDoc, was going to be outdated so they switched to another piece of external software, Asciidoctor, to prevent things from breaking. A sensible move. But for you, the user, things are now already broken, because now you have remove the first piece of software, install the second, and configure things again to make them work together. And again, this must be done on all your devices. You get to choose between options, but the options are not stable and require work by you, the user. Sometimes the options have some dependencies themselves, repeating the problem. Not a fun way to spend an afternoon. + +But enough about Asciidoc. Let’s talk about templates. + +It is wellknown that Hugo has a difficult templating language. This is because Hugo is built in Go and leverages the https://golang.org/pkg/html/template/[template functionality] of the Go standard library. That functionality is fast and elegant, but very much geared to programmers. I knew this beforehand, but since I am a Go developer, I figured this would be an advantage, not a disadvantage. I do this Go stuff all day. It has payed for the laptop I am writing on right now and the chair I am sitting in. Surely this will be easy? + +Not so much. Writing Hugo templates turned out to be a moderately frustrating experience. It was an endless game of guessing variables, types and trying to combine them into something useful by chaining functions. There is documentation, every variable and function is listed, but a crucial thing is missing: the big picture. + +The act of templating consists of two parts: data, and what to do with the data. Let’s say for instance we want to render a title. In the template we indicate that titles have a certain color, size and font. For each page we get a new title, the data, but we render them all the same way, like we specified. That is the job of a template. Telling how we want data to be rendered. + +However, a page contains a lot more data than just the title. In fact, in Hugo you get access to everything. Everything that is content. Including all metadata and, crucially, how it all relates together in the big abstract, imagined structure that makes up a site. This mental picture is not shown in the documentation. And worse, it is a _generalized_ mental picture of a site. Because Hugo is a universal SSG and you must have the option to build any type of site. Count the levels of unwinding you have to do before you can translate this to your actual site: generalized (one), abstract (two), mental (three) picture. + +Of course, making use of a template already imposes some abstractions. They can be very helpful, I ended up using a few too. But sometimes it is better for all parties involved (i.e. you and your tools) to just hardcode some things. + +These two issues, the templating and the external dependencies, have something in common. To solve them, your mind must switch to an analysing mode. There is a black box with buttons on it. If you figure out how it works inside, you can push the buttons in the right order and make it do the thing you want. If the box contains a lot of complex gears and levers, this can be a hard riddle to solve and you need to spend more effort. You will start to ask yourself questions along the lines of: What did the makers of the box think when they designed it? What was it designed for exactly? What problem does it solve and what would seem logical to them? They probably catered to the most common needs as they saw them. + +If you want solve this riddle, you have to leave your own framing of the the problem aside for a moment and adopt theirs. You have to step out of your own thinking and into theirs. + +At one point, if you are succesful, you’ve grasped it and then you want to get back to your own, original frame. See how you can connect the two. But more often than not this is hard, or even impossible. By making their way of doing things your own, you have overwritten your original perspective. At least in part. This is not always a bad thing, but it is important to realize that it happens. That you might not want that. Compare this with starting from scratch. No solutions to other peoples problems, just your own. This means creating a solution all by yourself, which is hard. But you can al least be sure that it fits _your_ problem. + +== The Spectrum of Software Tools + +So, should everyone and their mother start programming everything from scratch, even if they have no interest in making software whatsoever? That would be impractical. And probably bad for their motivation. Not to mention that for a lot of people, programming feels exactly like that magical black box with buttons and complicated machinery inside, so that would be counterproductive. Nevertheless, I think there are some general lessons to draw from this. + +All software tools make some kind of trade-off between flexibility and ease of use. Some make a better compromise than others, but a compromise it allways will be. The easiest tool to use is the one that has only one button. Push it and you get a complete result. But in order to do that, the tool, or actually the creators of the tool, have to make all kinds of decisions for you, both big and small. If you want more control over the outcome, that is possible, but by definition that means that you have to give more input. More buttons that need to be pushed, more dials to adjust. The level of control you have will match the level of input you have to give. If you extend this far enough, add every control imaginable, you end up with the very intricate and elaborate tool that we call a programming language. In a programming language, every little detail of the end result is yours to dictate. But on the flip side, it requires a lot of input and effort to get something moving. + +Site generators can be anywhere on this scale. One could argue that services like Facebook and Twitter are the ultimate “require only the push of one button” versions in this space. Thanks to them, anyone can publish without having to invest time and effort. Write your text, push the button and it is there for everyone to see. Design, structure, notifying readers, it is al magically there. + +But remember, if you don’t make the decisions, someone else has do it for you. It might be a good feeling to outsource all these difficult problems. Maybe you assume that it is for the better, because you think that other person knows more about the necessary mechanics. They probably do. But on the other hand, that other person does not know what is inside _your_ head. + +If Twitter is the only publishing platform you’ll ever use, then, without trying, you will naturally start to write texts that are 280 characters or less. That is just how most people work. But maybe this limitation irritates you often enough that you start to look for a way around it. You search online and you find apps like https://threadreaderapp.com/[Threadreader], that lets users string multiple tweets into one document as if they were a single text. This is a solution to the problem you had, but if you read your new posts carefully, you will notice that they don’t “feel” right. The limitation of 280 characters is still there, but it is hidden. One tweet becomes one paragraph, so you are bound very short paragraphs and as a result the flow of your text is still very.. _staccato_. Even though your texts can now be much longer, you still can’t write the way you want. Not to mention the clumsy process of composing the multiple tweets in the right order. + +In a situation like this, you would have been much better off with starting a https://wordpress.com/[Wordpress] blog. One step on the scale of tools, a little more work to do, but now you are able to write exactly the way you want. No programming required. If you want to have more control, you have to give more input. But there is a major difference between using one tool with two buttons, versus using two tools with one button. + +So, my advise is to be aware of the restrictions and the hidden models of the tools you use as much as possible. Maybe it is not necessary to become a programmer. But imagine for a moment that you are one. Let you mind wander and see what comes up. What would you build? How would it work? And if you’ve thought of something, take as many steps on the scale as you’re comfortable with and see if you can make it work. Trust me, it will feel liberating. + +== My Shitty SSG + +In the title, I mention that my generator is “shitty” and it is. It does not have many features. It is riddled with bugs and edge cases that it can’t handle. But that is not important. It works for my problem. If I don’t like something, I can fix it. If bug doesn’t bother me, I’ll let it be. Like all creative endevours, it is important to just start and get it out. You can always improve it later. + +I put the source online https://git.sr.ht/~ewintr/shitty-ssg[here]. See https://erikwinter.nl/articles/2021/shitty-ssg-code-walkthrough/[here] for a high level overview. Not for people to blindly copy and run (why would you?), but to give some inspiration for people who are still on the fence. To show them that shitty does not have to be hard and that it can be good enough, as long as it is the right kind of shitty. _Your_ kind of shitty. diff --git a/go.mod b/go.mod index 7a6ccd7..8283089 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ -module wittekastje/var/repo/ewintr.nl +module git.sr.ht/~ewintr/shitty-ssg -go 1.16 +go 1.14 require ( + git.sr.ht/~ewintr/erikwinternl/shitty-ssg v0.0.0-20201204092507-5528bf33815d // indirect git.sr.ht/~ewintr/go-kit v0.0.0-20200704112030-36275689b0ea - git.sr.ht/~ewintr/shitty-ssg v0.0.0-20210918124034-310a03219589 ) diff --git a/go.sum b/go.sum index 6b2ae8d..048b1a9 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +git.sr.ht/~ewintr/erikwinternl v0.0.0-20201204092507-5528bf33815d h1:teNzxpdrJw5sUpB/84ojdjVV2q2+3IVwYjztCze9YmU= +git.sr.ht/~ewintr/erikwinternl/shitty-ssg v0.0.0-20201204092507-5528bf33815d h1:vyJ0Pgdrvqh28jxBCLGCGt/80p86iSQtKJW7rAbYRk8= git.sr.ht/~ewintr/erikwinternl/shitty-ssg v0.0.0-20201204092507-5528bf33815d/go.mod h1:ka/PzT9A0BNvjcUQpIM0YE1BnOR1/qr6x7XeCUyYDlw= git.sr.ht/~ewintr/go-kit v0.0.0-20200704112030-36275689b0ea h1:YdHan+/QwjzF05Dlp40BHw5N47nWy2QZCY7aseXf2n0= git.sr.ht/~ewintr/go-kit v0.0.0-20200704112030-36275689b0ea/go.mod h1:zrpigRr1EHuVGw7GWwvYWwfLgdAwJ110zIRAGtaicvI= -git.sr.ht/~ewintr/shitty-ssg v0.0.0-20210918124034-310a03219589 h1:O37wlRQLijgtDCFAP3uuz9R+9Qk+xr91vks/f8zri+U= -git.sr.ht/~ewintr/shitty-ssg v0.0.0-20210918124034-310a03219589/go.mod h1:SlgFsg3RNRoRrHBVVvycRDgXf9sVzIH6iuxgDGYylrs= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= diff --git a/pkg/adoc/adoc.go b/pkg/adoc/adoc.go new file mode 100644 index 0000000..560c65d --- /dev/null +++ b/pkg/adoc/adoc.go @@ -0,0 +1,75 @@ +package adoc + +import ( + "strings" + "time" +) + +const ( + KIND_NOTE = Kind("note") + KIND_PRIVATE_NOTE = Kind("private_note") + KIND_WORK_NOTE = Kind("work_note") + KIND_VKV = Kind("vkv") + KIND_STORY = Kind("story") + KIND_SNIPPET = Kind("snippet") + KIND_ESSAY = Kind("essay") + KIND_ARTICLE = Kind("article") + KIND_TUTORIAL = Kind("tutorial") + KIND_UNKNOWN = Kind("unknown") +) + +type Kind string + +func NewKind(text string) Kind { + switch text { + case "verhaal": + text = "story" + case "los": + text = "snippet" + } + + for _, k := range []string{ + "note", "private_note", "work_note", "vkv", + "story", "snippet", + "essay", "tutorial", "work", "article", + } { + if k == text { + return Kind(k) + } + } + + return KIND_UNKNOWN +} + +const ( + LANGUAGE_EN = Language("en") + LANGUAGE_NL = Language("nl") + LANGUAGE_UNKNOWN = Language("unknown") +) + +type Language string + +func NewLanguage(ln string) Language { + switch strings.ToLower(ln) { + case "nl": + return LANGUAGE_NL + case "en": + return LANGUAGE_EN + default: + return LANGUAGE_UNKNOWN + } +} + +type Tag string + +type ADoc struct { + Title string + Author string + Kind Kind + Language Language + Public bool + Path string + Date time.Time + Tags []Tag + Content []BlockElement +} diff --git a/pkg/adoc/adoc_test.go b/pkg/adoc/adoc_test.go new file mode 100644 index 0000000..de4a260 --- /dev/null +++ b/pkg/adoc/adoc_test.go @@ -0,0 +1,110 @@ +package adoc_test + +import ( + "testing" + + "git.sr.ht/~ewintr/go-kit/test" + "git.sr.ht/~ewintr/shitty-ssg/pkg/adoc" +) + +func TestNewKind(t *testing.T) { + for _, tc := range []struct { + name string + input string + exp adoc.Kind + }{ + { + name: "empty", + exp: adoc.KIND_UNKNOWN, + }, + { + name: "unknown", + input: "something", + exp: adoc.KIND_UNKNOWN, + }, + { + name: "note", + input: "note", + exp: adoc.KIND_NOTE, + }, + { + name: "vkv", + input: "vkv", + exp: adoc.KIND_VKV, + }, + { + name: "story", + input: "verhaal", + exp: adoc.KIND_STORY, + }, + { + name: "snippet", + input: "los", + exp: adoc.KIND_SNIPPET, + }, + { + name: "essay", + input: "essay", + exp: adoc.KIND_ESSAY, + }, + { + name: "tutorial", + input: "tutorial", + exp: adoc.KIND_TUTORIAL, + }, + { + name: "article", + input: "article", + exp: adoc.KIND_ARTICLE, + }, + } { + t.Run(tc.name, func(t *testing.T) { + act := adoc.NewKind(tc.input) + test.Equals(t, tc.exp, act) + }) + } +} + +func TestNewLanguage(t *testing.T) { + for _, tc := range []struct { + name string + input string + exp adoc.Language + }{ + { + name: "empty", + exp: adoc.LANGUAGE_UNKNOWN, + }, + { + name: "dutch lower", + input: "nl", + exp: adoc.LANGUAGE_NL, + }, + { + name: "dutch upper", + input: "NL", + exp: adoc.LANGUAGE_NL, + }, + { + name: "english lower", + input: "en", + exp: adoc.LANGUAGE_EN, + }, + { + name: "english upper", + input: "EN", + exp: adoc.LANGUAGE_EN, + }, + { + name: "unknown", + input: "something", + exp: adoc.LANGUAGE_UNKNOWN, + }, + } { + t.Run(tc.name, func(t *testing.T) { + act := adoc.NewLanguage(tc.input) + test.Equals(t, tc.exp, act) + }) + + } +} diff --git a/pkg/adoc/block.go b/pkg/adoc/block.go new file mode 100644 index 0000000..5e64d63 --- /dev/null +++ b/pkg/adoc/block.go @@ -0,0 +1,55 @@ +package adoc + +import ( + "fmt" + "strings" +) + +type BlockElement interface { + Text() string +} + +type Paragraph []InlineElement + +func (p Paragraph) Text() string { + var text []string + for _, ie := range p { + text = append(text, ie.Text()) + } + + return strings.Join(text, " ") +} + +type SubTitle string + +func (st SubTitle) Text() string { return string(st) } + +type SubSubTitle string + +func (st SubSubTitle) Text() string { return string(st) } + +type CodeBlock string + +func (cb CodeBlock) Text() string { return string(cb) } + +type ListItem []InlineElement + +func (li ListItem) Text() string { + var text []string + for _, ie := range li { + text = append(text, ie.Text()) + } + + return fmt.Sprintf("%s%s", LISTITEM_PREFIX, strings.Join(text, "")) +} + +type List []ListItem + +func (l List) Text() string { + var items []string + for _, item := range l { + items = append(items, item.Text()) + } + + return strings.Join(items, "\n") +} diff --git a/pkg/adoc/block_test.go b/pkg/adoc/block_test.go new file mode 100644 index 0000000..e30169b --- /dev/null +++ b/pkg/adoc/block_test.go @@ -0,0 +1,134 @@ +package adoc_test + +import ( + "testing" + + "git.sr.ht/~ewintr/go-kit/test" + "git.sr.ht/~ewintr/shitty-ssg/pkg/adoc" +) + +func TestParagraph(t *testing.T) { + for _, tc := range []struct { + name string + elements []adoc.InlineElement + exp string + }{ + { + name: "empty", + elements: []adoc.InlineElement{}, + }, + { + name: "one", + elements: []adoc.InlineElement{ + adoc.PlainText("one"), + }, + exp: "one", + }, + { + name: "many", + elements: []adoc.InlineElement{ + adoc.PlainText("one"), + adoc.PlainText("two"), + adoc.PlainText("three"), + }, + exp: "one two three", + }, + } { + t.Run(tc.name, func(t *testing.T) { + p := adoc.Paragraph(tc.elements) + test.Equals(t, tc.exp, p.Text()) + }) + } +} + +func TestBlockSimple(t *testing.T) { + text := "text" + for _, tc := range []struct { + name string + element adoc.BlockElement + }{ + { + name: "subtitle", + element: adoc.SubTitle(text), + }, + { + name: "subsubtitle", + element: adoc.SubSubTitle(text), + }, + { + name: "code block", + element: adoc.CodeBlock(text), + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, text, tc.element.Text()) + }) + } +} + +func TestListItem(t *testing.T) { + for _, tc := range []struct { + name string + elements []adoc.InlineElement + exp string + }{ + { + name: "empty", + exp: "* ", + }, + { + name: "one", + elements: []adoc.InlineElement{ + adoc.PlainText("one"), + }, + exp: "* one", + }, + { + name: "many", + elements: []adoc.InlineElement{ + adoc.PlainText("one"), + adoc.PlainText("two"), + adoc.PlainText("three"), + }, + exp: "* onetwothree", + }, + } { + t.Run(tc.name, func(t *testing.T) { + li := adoc.ListItem(tc.elements) + test.Equals(t, tc.exp, li.Text()) + }) + } +} + +func TestList(t *testing.T) { + for _, tc := range []struct { + name string + elements []adoc.ListItem + exp string + }{ + { + name: "empty", + }, + { + name: "one", + elements: []adoc.ListItem{ + {adoc.PlainText("one")}, + }, + exp: "* one", + }, + { + name: "many", + elements: []adoc.ListItem{ + {adoc.PlainText("one")}, + {adoc.PlainText("two")}, + {adoc.PlainText("three")}, + }, + exp: "* one\n* two\n* three", + }, + } { + t.Run(tc.name, func(t *testing.T) { + l := adoc.List(tc.elements) + test.Equals(t, tc.exp, l.Text()) + }) + } +} diff --git a/pkg/adoc/inline.go b/pkg/adoc/inline.go new file mode 100644 index 0000000..c6b6b9d --- /dev/null +++ b/pkg/adoc/inline.go @@ -0,0 +1,40 @@ +package adoc + +type InlineElement interface { + Text() string +} + +type PlainText string + +func (pt PlainText) Text() string { return string(pt) } + +type StrongText string + +func (st StrongText) Text() string { return string(st) } + +type EmpText string + +func (et EmpText) Text() string { return string(et) } + +type StrongEmpText string + +func (set StrongEmpText) Text() string { return string(set) } + +type Link struct { + url string + title string +} + +func NewLink(url, title string) Link { + return Link{ + url: url, + title: title, + } +} + +func (l Link) URL() string { return l.url } +func (l Link) Text() string { return l.title } + +type CodeText string + +func (ct CodeText) Text() string { return string(ct) } diff --git a/pkg/adoc/inline_test.go b/pkg/adoc/inline_test.go new file mode 100644 index 0000000..9800b3e --- /dev/null +++ b/pkg/adoc/inline_test.go @@ -0,0 +1,50 @@ +package adoc_test + +import ( + "testing" + + "git.sr.ht/~ewintr/go-kit/test" + "git.sr.ht/~ewintr/shitty-ssg/pkg/adoc" +) + +func TestInlineSimple(t *testing.T) { + text := "text" + for _, tc := range []struct { + name string + element adoc.InlineElement + }{ + { + name: "plain text", + element: adoc.PlainText(text), + }, + { + name: "strong", + element: adoc.StrongText(text), + }, + { + name: "emphasis", + element: adoc.EmpText(text), + }, + { + name: "strong emphasis", + element: adoc.StrongEmpText(text), + }, + { + name: "code", + element: adoc.CodeText(text), + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, text, tc.element.Text()) + }) + } +} + +func TextLink(t *testing.T) { + url := "url" + title := "title" + l := adoc.NewLink(url, title) + + test.Equals(t, url, l.URL()) + test.Equals(t, title, l.Text()) +} diff --git a/pkg/adoc/parser.go b/pkg/adoc/parser.go new file mode 100644 index 0000000..a88812c --- /dev/null +++ b/pkg/adoc/parser.go @@ -0,0 +1,238 @@ +package adoc + +import ( + "strings" + "time" +) + +const ( + TITLE_PREFIX = "= " + SUBTITLE_PREFIX = "== " + SUBSUBTITLE_PREFIX = "=== " + PARAGRAPH_SEPARATOR = "\n\n" + LINE_SEPARATOR = "\n" + CODE_PREFIX = "----\n" + CODE_SUFFIX = "\n----" + LISTITEM_PREFIX = "* " +) + +func New(text string) *ADoc { + doc := &ADoc{ + Language: LANGUAGE_EN, + Tags: []Tag{}, + } + + // split up blocks + var pars []string + for _, s := range strings.Split(text, PARAGRAPH_SEPARATOR) { + if s == "" { + + continue + } + + pars = append(pars, s) + } + + // actually, code blocks are allowed to have empty lines + var blocks []string + var inCode bool + var currentBlock string + for _, par := range pars { + switch { + case strings.HasPrefix(par, CODE_PREFIX) && strings.HasSuffix(par, CODE_SUFFIX): + blocks = append(blocks, par) + case !inCode && strings.HasPrefix(par, CODE_PREFIX): + inCode = true + currentBlock = par + case inCode && !strings.HasSuffix(par, CODE_SUFFIX): + currentBlock += PARAGRAPH_SEPARATOR + par + case inCode && strings.HasSuffix(par, CODE_SUFFIX): + currentBlock += PARAGRAPH_SEPARATOR + par + blocks = append(blocks, currentBlock) + inCode = false + currentBlock = "" + default: + blocks = append(blocks, par) + } + } + + // interpret the blocks + for i, p := range blocks { + switch { + case i == 0 && strings.HasPrefix(p, TITLE_PREFIX): + ParseHeader(p, doc) + case strings.HasPrefix(p, SUBTITLE_PREFIX): + p = strings.TrimSpace(p) + s := strings.Split(p, SUBTITLE_PREFIX) + if len(s) == 1 || s[1] == "" { + + continue + } + doc.Content = append(doc.Content, SubTitle(s[1])) + case strings.HasPrefix(p, SUBSUBTITLE_PREFIX): + p = strings.TrimSpace(p) + s := strings.Split(p, SUBSUBTITLE_PREFIX) + if len(s) == 1 || s[1] == "" { + + continue + } + doc.Content = append(doc.Content, SubSubTitle(s[1])) + case isCodeBlock(p): + doc.Content = append(doc.Content, parseCodeBlock(p)) + case strings.HasPrefix(p, LISTITEM_PREFIX): + p = strings.TrimSpace(p) + var items []ListItem + for i, ti := range strings.Split(p, LISTITEM_PREFIX) { + if i > 0 { + inline := ParseInline(strings.TrimSpace(ti)) + items = append(items, ListItem(inline)) + } + } + doc.Content = append(doc.Content, List(items)) + + default: + p = strings.TrimSpace(p) + doc.Content = append(doc.Content, Paragraph(ParseInline(p))) + } + } + + return doc +} + +func isCodeBlock(par string) bool { + return strings.HasPrefix(par, CODE_PREFIX) && strings.HasSuffix(par, CODE_SUFFIX) +} + +func parseCodeBlock(par string) CodeBlock { + ss := strings.Split(par, "\n") + ss = ss[1 : len(ss)-1] + content := strings.Join(ss, "\n") + + return CodeBlock(content) +} + +func ParseHeader(text string, doc *ADoc) { + text = strings.TrimSpace(text) + lines := strings.Split(text, LINE_SEPARATOR) + for i, l := range lines { + switch { + case i == 0: + s := strings.Split(l, TITLE_PREFIX) + doc.Title = s[1] + case isDate(l): + date, _ := time.Parse("2006-01-02", l) + doc.Date = date + case strings.HasPrefix(l, ":kind:"): + s := strings.Split(l, ":") + doc.Kind = NewKind(strings.TrimSpace(s[2])) + case strings.HasPrefix(l, ":language:"): + s := strings.Split(l, ":") + doc.Language = NewLanguage(strings.TrimSpace(s[2])) + case strings.HasPrefix(l, ":public:"): + s := strings.Split(l, ":") + val := strings.TrimSpace(s[2]) + if val == "yes" || val == "true" { + doc.Public = true + } + case strings.HasPrefix(l, ":tags:"): + s := strings.Split(l, ":") + t := strings.Split(s[2], ",") + for _, tag := range t { + doc.Tags = append(doc.Tags, Tag(strings.TrimSpace(tag))) + } + default: + doc.Author = l + } + } +} + +func isDate(text string) bool { + if _, err := time.Parse("2006-01-02", text); err == nil { + return true + } + + return false +} + +func ParseInline(text string) []InlineElement { + var e []InlineElement + + ss := strings.Split(text, "") + var buffer, curWord, prevChar string + var strong, emp, code, linkTitle bool + wordStart := true + for _, s := range ss { + switch { + case (s == "_" && wordStart) || (s == "_" && emp): + e = addElement(e, buffer+curWord, strong, emp, code) + emp = !emp + buffer = "" + curWord = "" + case s == "*": + e = addElement(e, buffer+curWord, strong, emp, code) + buffer = "" + curWord = "" + strong = !strong + case s == "`": + e = addElement(e, buffer+curWord, strong, emp, code) + code = !code + buffer = "" + curWord = "" + case s == "[" && prevChar != "": + e = addElement(e, buffer, strong, emp, code) + linkTitle = true + curWord += s + case s == "]" && linkTitle: + e = addLink(e, curWord) + buffer = "" + curWord = "" + linkTitle = false + case s == " " && !linkTitle: + buffer += curWord + " " + curWord = "" + default: + curWord += s + } + prevChar = s + wordStart = false + if prevChar == " " { + wordStart = true + } + } + if len(buffer+curWord) > 0 { + e = addElement(e, buffer+curWord, false, false, false) + } + + return e +} + +func addLink(ies []InlineElement, linkText string) []InlineElement { + ss := strings.Split(linkText, "[") + if len(ss) < 2 { + ss = append(ss, "ERROR") + } + + return append(ies, Link{url: ss[0], title: ss[1]}) +} + +func addElement(ies []InlineElement, text string, strong, emp, code bool) []InlineElement { + if len(text) == 0 { + return ies + } + + var ne InlineElement + switch { + case code: + ne = CodeText(text) + case strong && emp: + ne = StrongEmpText(text) + case strong && !emp: + ne = StrongText(text) + case !strong && emp: + ne = EmpText(text) + default: + ne = PlainText(text) + } + + return append(ies, ne) +} diff --git a/pkg/adoc/parser_test.go b/pkg/adoc/parser_test.go new file mode 100644 index 0000000..72e37cc --- /dev/null +++ b/pkg/adoc/parser_test.go @@ -0,0 +1,220 @@ +package adoc_test + +import ( + "fmt" + "testing" + "time" + + "git.sr.ht/~ewintr/go-kit/test" + "git.sr.ht/~ewintr/shitty-ssg/pkg/adoc" +) + +func TestNew(t *testing.T) { + one := "one" + two := "two" + three := "three" + ptOne := adoc.PlainText(one) + ptTwo := adoc.PlainText(two) + ptThree := adoc.PlainText(three) + for _, tc := range []struct { + name string + input string + exp *adoc.ADoc + }{ + { + name: "empty", + exp: &adoc.ADoc{ + Tags: []adoc.Tag{}, + Language: adoc.LANGUAGE_EN, + }, + }, + { + name: "title", + input: "= Title", + exp: &adoc.ADoc{ + Title: "Title", + Tags: []adoc.Tag{}, + Language: adoc.LANGUAGE_EN, + }, + }, + { + name: "header", + input: "= Title\nT. Test\n2020-10-27\n:tags:\ttag1, tag2\n:kind:\tnote\n:language:\tnl\n:public: yes", + exp: &adoc.ADoc{ + Title: "Title", + Author: "T. Test", + Kind: adoc.KIND_NOTE, + Public: true, + Language: adoc.LANGUAGE_NL, + Tags: []adoc.Tag{ + adoc.Tag("tag1"), + adoc.Tag("tag2"), + }, + Date: time.Date(2020, time.October, 27, 0, 0, 0, 0, time.UTC), + }, + }, + { + name: "paragraphs", + input: fmt.Sprintf("%s\n\n%s\n\n%s", one, two, three), + exp: &adoc.ADoc{ + Tags: []adoc.Tag{}, + Language: adoc.LANGUAGE_EN, + Content: []adoc.BlockElement{ + adoc.Paragraph([]adoc.InlineElement{ptOne}), + adoc.Paragraph([]adoc.InlineElement{ptTwo}), + adoc.Paragraph([]adoc.InlineElement{ptThree}), + }, + }, + }, + { + name: "subtitle", + input: "== Subtitle", + exp: &adoc.ADoc{ + Tags: []adoc.Tag{}, + Language: adoc.LANGUAGE_EN, + Content: []adoc.BlockElement{ + adoc.SubTitle("Subtitle"), + }, + }, + }, + { + name: "code block", + input: "----\nsome code\nmore code\n----", + exp: &adoc.ADoc{ + Tags: []adoc.Tag{}, + Language: adoc.LANGUAGE_EN, + Content: []adoc.BlockElement{ + adoc.CodeBlock("some code\nmore code"), + }, + }, + }, + { + name: "code block with empty lines", + input: "----\nsome code\n\nmore code\n----", + exp: &adoc.ADoc{ + Tags: []adoc.Tag{}, + Language: adoc.LANGUAGE_EN, + Content: []adoc.BlockElement{ + adoc.CodeBlock("some code\n\nmore code"), + }, + }, + }, + { + name: "list", + input: "* item 1\n* item 2\n* *item 3*\n", + exp: &adoc.ADoc{ + Tags: []adoc.Tag{}, + Language: adoc.LANGUAGE_EN, + Content: []adoc.BlockElement{ + adoc.List{ + adoc.ListItem([]adoc.InlineElement{adoc.PlainText("item 1")}), + adoc.ListItem([]adoc.InlineElement{adoc.PlainText("item 2")}), + adoc.ListItem([]adoc.InlineElement{adoc.StrongText("item 3")}), + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + act := adoc.New(tc.input) + + test.Equals(t, tc.exp, act) + }) + } +} + +func TestParseInline(t *testing.T) { + for _, tc := range []struct { + name string + input string + exp []adoc.InlineElement + }{{ + name: "empty", + }, + { + name: "plain", + input: "some test text", + exp: []adoc.InlineElement{ + adoc.PlainText("some test text")}, + }, + { + name: "strong", + input: "*some strong text*", + exp: []adoc.InlineElement{ + adoc.StrongText("some strong text"), + }, + }, + { + name: "strong in plain", + input: "some *strong* text", + exp: []adoc.InlineElement{ + adoc.PlainText("some "), + adoc.StrongText("strong"), + adoc.PlainText(" text"), + }, + }, + { + name: "emphasis", + input: "_some emphasized text_", + exp: []adoc.InlineElement{ + adoc.EmpText("some emphasized text"), + }, + }, + { + name: "emphasis in plain", + input: "some _emphasized_ text", + exp: []adoc.InlineElement{ + adoc.PlainText("some "), + adoc.EmpText("emphasized"), + adoc.PlainText(" text"), + }, + }, + { + name: "emp and strong in plain", + input: "some _*special*_ text", + exp: []adoc.InlineElement{ + adoc.PlainText("some "), + adoc.StrongEmpText("special"), + adoc.PlainText(" text"), + }, + }, + { + name: "link", + input: "a link[title] somewhere", + exp: []adoc.InlineElement{ + adoc.PlainText("a "), + adoc.NewLink("link", "title"), + adoc.PlainText(" somewhere"), + }, + }, + { + name: "code", + input: "`command`", + exp: []adoc.InlineElement{ + adoc.CodeText("command"), + }, + }, + { + name: "code in plain", + input: "some `code` in text", + exp: []adoc.InlineElement{ + adoc.PlainText("some "), + adoc.CodeText("code"), + adoc.PlainText(" in text"), + }, + }, + { + name: "link with underscore", + input: "https://example.com/some_url[some url]", + exp: []adoc.InlineElement{ + adoc.NewLink("https://example.com/some_url", "some url"), + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + act := adoc.ParseInline(tc.input) + + test.Equals(t, tc.exp, act) + }) + } +}