emanote (empty) → 0.8.0.0
raw patch · 102 files changed
+7133/−0 lines, 102 filesdep +aesondep +aeson-extradep +aeson-opticsbinary-added
Dependencies added: aeson, aeson-extra, aeson-optics, async, base, blaze-html, bytestring, commonmark, commonmark-extensions, commonmark-pandoc, commonmark-simple, containers, data-default, dependent-sum, directory, ema, filepath, filepattern, fsnotify, hedgehog, heist-emanote, heist-extra, hspec, hspec-hedgehog, ixset-typed, lvar, map-syntax, megaparsec, monad-logger, monad-logger-extras, mtl, neat-interpolation, optics-core, optics-th, optparse-applicative, pandoc, pandoc-link-context, pandoc-types, parsec, path-tree, process-extras, profunctors, relude, shower, some, stm, tagged, tagtree, tailwind, text, time, tomland, unionmount, unliftio, unordered-containers, uri-encode, url-slug, uuid, which, with-utf8, xmlhtml, yaml
Files
- LICENSE +661/−0
- README.md +38/−0
- default/_emanote-bin/compile-css +4/−0
- default/_emanote-live-server/tailwind/2.2.2/tailwind.min.css too large to diff
- default/_emanote-static/emanote-logo.svg +92/−0
- default/_emanote-static/fonts/Work_Sans/WorkSans-Italic-VariableFont_wght.ttf binary
- default/_emanote-static/fonts/Work_Sans/WorkSans-VariableFont_wght.ttf binary
- default/_emanote-static/inverted-tree.css +118/−0
- default/_emanote-static/stork/flat.css +191/−0
- default/_emanote-static/stork/stork.js +2/−0
- default/_emanote-static/stork/stork.wasm binary
- default/favicon.svg +92/−0
- default/index.md +7/−0
- default/index.yaml +128/−0
- default/templates/base.tpl +51/−0
- default/templates/components/backlinks.tpl +18/−0
- default/templates/components/breadcrumbs.tpl +47/−0
- default/templates/components/checkbox-checked.tpl +4/−0
- default/templates/components/checkbox-unchecked.tpl +4/−0
- default/templates/components/context.tpl +24/−0
- default/templates/components/footer.tpl +46/−0
- default/templates/components/metadata.tpl +16/−0
- default/templates/components/note-body.tpl +5/−0
- default/templates/components/note-title.tpl +6/−0
- default/templates/components/note-uptree-recurse.tpl +16/−0
- default/templates/components/note-uptree.tpl +17/−0
- default/templates/components/pandoc.tpl +108/−0
- default/templates/components/sidebar-tree.tpl +80/−0
- default/templates/components/stork/stork-icon.tpl +5/−0
- default/templates/components/stork/stork-search-head.tpl +64/−0
- default/templates/components/stork/stork-search.tpl +12/−0
- default/templates/components/timeline.tpl +19/−0
- default/templates/error.tpl +39/−0
- default/templates/experimental.tpl +5/−0
- default/templates/filters/embed-image.tpl +1/−0
- default/templates/filters/embed-note.tpl +11/−0
- default/templates/filters/embed-pdf.tpl +4/−0
- default/templates/filters/embed-video.tpl +6/−0
- default/templates/filters/query-default.tpl +23/−0
- default/templates/filters/query-timeline.tpl +25/−0
- default/templates/hooks/after-note.tpl +1/−0
- default/templates/hooks/before-note.tpl +1/−0
- default/templates/hooks/more-head.tpl +49/−0
- default/templates/hooks/note-end.tpl +1/−0
- default/templates/layouts/book.tpl +82/−0
- default/templates/layouts/note.tpl +41/−0
- default/templates/special/base.tpl +18/−0
- default/templates/special/index.tpl +9/−0
- default/templates/special/tagindex.tpl +75/−0
- default/templates/special/tasks.tpl +51/−0
- emanote.cabal +214/−0
- src/Emanote.hs +160/−0
- src/Emanote/CLI.hs +71/−0
- src/Emanote/Model.hs +3/−0
- src/Emanote/Model/Calendar.hs +46/−0
- src/Emanote/Model/Graph.hs +115/−0
- src/Emanote/Model/Link/Rel.hs +144/−0
- src/Emanote/Model/Link/RelSpec.hs +32/−0
- src/Emanote/Model/Link/Resolve.hs +45/−0
- src/Emanote/Model/Meta.hs +43/−0
- src/Emanote/Model/Note.hs +345/−0
- src/Emanote/Model/Note/Filter.hs +50/−0
- src/Emanote/Model/Query.hs +92/−0
- src/Emanote/Model/QuerySpec.hs +19/−0
- src/Emanote/Model/SData.hs +74/−0
- src/Emanote/Model/StaticFile.hs +39/−0
- src/Emanote/Model/Stork.hs +31/−0
- src/Emanote/Model/Stork/Index.hs +104/−0
- src/Emanote/Model/Task.hs +51/−0
- src/Emanote/Model/Title.hs +88/−0
- src/Emanote/Model/Type.hs +271/−0
- src/Emanote/Pandoc/BuiltinFilters.hs +53/−0
- src/Emanote/Pandoc/Link.hs +37/−0
- src/Emanote/Pandoc/Markdown/Parser.hs +51/−0
- src/Emanote/Pandoc/Markdown/Syntax/HashTag.hs +82/−0
- src/Emanote/Pandoc/Markdown/Syntax/Highlight.hs +28/−0
- src/Emanote/Pandoc/Markdown/Syntax/WikiLink.hs +275/−0
- src/Emanote/Pandoc/Renderer.hs +77/−0
- src/Emanote/Pandoc/Renderer/Embed.hs +110/−0
- src/Emanote/Pandoc/Renderer/Query.hs +53/−0
- src/Emanote/Pandoc/Renderer/Url.hs +166/−0
- src/Emanote/Prelude.hs +40/−0
- src/Emanote/Route.hs +5/−0
- src/Emanote/Route/Ext.hs +92/−0
- src/Emanote/Route/ModelRoute.hs +95/−0
- src/Emanote/Route/R.hs +109/−0
- src/Emanote/Route/SiteRoute.hs +15/−0
- src/Emanote/Route/SiteRoute/Class.hs +202/−0
- src/Emanote/Route/SiteRoute/Type.hs +114/−0
- src/Emanote/Source/Dynamic.hs +96/−0
- src/Emanote/Source/Loc.hs +62/−0
- src/Emanote/Source/Patch.hs +184/−0
- src/Emanote/Source/Pattern.hs +42/−0
- src/Emanote/View.hs +3/−0
- src/Emanote/View/Common.hs +226/−0
- src/Emanote/View/Export.hs +96/−0
- src/Emanote/View/LiveServerFiles.hs +27/−0
- src/Emanote/View/TagIndex.hs +111/−0
- src/Emanote/View/TaskIndex.hs +55/−0
- src/Emanote/View/Template.hs +233/−0
- src/Main.hs +28/−0
- src/Spec.hs +12/−0
@@ -0,0 +1,661 @@+ GNU AFFERO GENERAL PUBLIC LICENSE+ Version 3, 19 November 2007++ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>+ 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.++ <one line to give the program's name and a brief idea of what it does.>+ Copyright (C) <year> <name of author>++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 <https://www.gnu.org/licenses/>.++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+<https://www.gnu.org/licenses/>.
@@ -0,0 +1,38 @@+# emanote++[](https://en.wikipedia.org/wiki/Affero_General_Public_License)+[](https://builtwithnix.org)+[](https://app.element.io/#/room/#ema:matrix.org "Chat on Matrix")++Spiritual successor to [neuron](https://neuron.zettel.page), based on [Ema](https://ema.srid.ca).++Create beautiful websites -- such as personal webpage, blog, wiki, Zettelkasten, notebook, knowledge-base, documentation, etc. from future-proof plain-text notes and arbitrary data -- with live preview that updates in real-time.++## Installing and using++https://emanote.srid.ca/start/install++### Examples++https://emanote.srid.ca/examples++## Developing++Emanote is written in Haskell. Thanks to Nix, this repository is pre-configured to provide a delightful development experience with full IDE support in Visual Studio Code. Follow these steps:++- [Install Nix](https://nixos.org/download.html) & [enable Flakes](https://nixos.wiki/wiki/Flakes#Installing_flakes)+- Run `nix develop -c haskell-language-server` to sanity check your environment (Expect it to download and build a bunch of things the first time)+- Open the repository [as single-folder workspace](https://code.visualstudio.com/docs/editor/workspaces#_singlefolder-workspaces) in Visual Studio Code+ - Install the [workspace recommended](https://code.visualstudio.com/docs/editor/extension-marketplace#_workspace-recommended-extensions) extensions+ - <kbd>Ctrl+Shift+P</kbd> to run command "Nix-Env: Select Environment" and select `shell.nix`. The extension will ask you to reload VSCode at the end.+- Press <kbd>Ctrl+Shift+B</kbd> in VSCode, or run `bin/run` in terminal, to launch the dev version Emanote on `./docs`, then navigate to http://localhost:9010/+ - Changing the Haskell sources will recompile and reload this instance automatically.++All but the final step need to be done only once. See [architecture.md](docs/architecture.md) for a high-level overview of the codebase.++## Discussion++To discuss the emanote project, [join Matrix][matrix] or post in [GitHub Discussions][ghdiscuss].++[matrix]: https://matrix.to/#/#ema:matrix.org+[ghdiscuss]: https://github.com/srid/emanote/discussions
@@ -0,0 +1,4 @@+#!/usr/bin/env bash+set -xe++echo "Deprecation Warning: this script should not be used anymore"
file too large to diff
@@ -0,0 +1,92 @@+<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 504.124 504.124" style="enable-background:new 0 0 504.124 504.124;" xml:space="preserve"> +<path style="fill:#EA0C5C;" d="M482.951,396.006c0,8.531-6.908,15.447-15.447,15.447H95.461c-8.515,0-15.439-6.916-15.439-15.447 + v-64.37c0-8.531,6.924-15.447,15.439-15.447h372.043c8.539,0,15.447,6.916,15.447,15.447L482.951,396.006L482.951,396.006z"/> +<path style="fill:#BC0852;" d="M467.512,316.197c8.539,0,15.447,6.916,15.447,15.447v64.37c0,8.531-6.908,15.447-15.447,15.447 + H95.461"/> +<path style="fill:#FF7171;" d="M80.022,331.643c0-8.531,6.924-15.447,15.439-15.447h372.043c8.539,0,15.447,6.916,15.447,15.447"/> +<g> + <path style="fill:#F2DB06;" d="M399.991,396.699c0,1.552-1.268,2.812-2.812,2.812l0,0c-1.552,0-2.82-1.26-2.82-2.812V330.95 + c0-1.56,1.268-2.82,2.82-2.82l0,0c1.544,0,2.812,1.26,2.812,2.82V396.699z"/> + <path style="fill:#F2DB06;" d="M417.84,396.699c0,1.552-1.26,2.812-2.812,2.812l0,0c-1.56,0-2.82-1.26-2.82-2.812V330.95 + c0-1.56,1.26-2.82,2.82-2.82l0,0c1.552,0,2.812,1.26,2.812,2.82V396.699z"/> + <path style="fill:#F2DB06;" d="M435.689,396.699c0,1.552-1.252,2.812-2.82,2.812l0,0c-1.544,0-2.804-1.26-2.804-2.812V330.95 + c0-1.56,1.26-2.82,2.804-2.82l0,0c1.568,0,2.82,1.26,2.82,2.82V396.699z"/> +</g> +<path style="fill:#038462;" d="M424.102,302.089c0,8.523-6.916,15.447-15.447,15.447H36.613c-8.531,0-15.447-6.924-15.447-15.447 + v-64.37c0-8.539,6.916-15.447,15.447-15.447h372.043c8.531,0,15.447,6.908,15.447,15.447V302.089z"/> +<path style="fill:#047769;" d="M408.655,222.272c8.531,0,15.447,6.908,15.447,15.447v64.37c0,8.523-6.916,15.447-15.447,15.447 + H36.613"/> +<path style="fill:#2EAF8D;" d="M21.174,237.719c0-8.539,6.916-15.447,15.447-15.447h372.035c8.531,0,15.447,6.908,15.447,15.447"/> +<g> + <path style="fill:#F2DB06;" d="M341.134,302.775c0,1.552-1.26,2.812-2.82,2.812l0,0c-1.544,0-2.796-1.26-2.796-2.812v-65.757 + c0-1.552,1.252-2.812,2.796-2.812l0,0c1.56,0,2.82,1.26,2.82,2.812V302.775z"/> + <path style="fill:#F2DB06;" d="M358.984,302.775c0,1.552-1.26,2.812-2.828,2.812l0,0c-1.544,0-2.804-1.26-2.804-2.812v-65.757 + c0-1.552,1.26-2.812,2.804-2.812l0,0c1.568,0,2.828,1.26,2.828,2.812V302.775z"/> + <path style="fill:#F2DB06;" d="M376.833,302.775c0,1.552-1.268,2.812-2.812,2.812l0,0c-1.56,0-2.82-1.26-2.82-2.812v-65.757 + c0-1.552,1.26-2.812,2.82-2.812l0,0c1.544,0,2.812,1.26,2.812,2.812V302.775z"/> +</g> +<path style="fill:#115989;" d="M424.102,488.678c0,8.523-6.916,15.447-15.447,15.447H36.613c-8.531,0-15.447-6.924-15.447-15.447 + v-64.37c0-8.531,6.916-15.447,15.447-15.447h372.043c8.531,0,15.447,6.916,15.447,15.447V488.678z"/> +<path style="fill:#003B5B;" d="M408.655,408.861c8.531,0,15.447,6.916,15.447,15.447v64.37c0,8.523-6.916,15.447-15.447,15.447 + H36.613"/> +<path style="fill:#158ACC;" d="M21.174,424.308c0-8.531,6.916-15.447,15.447-15.447h372.035c8.531,0,15.447,6.916,15.447,15.447"/> +<g> + <path style="fill:#F2DB06;" d="M341.134,489.371c0,1.552-1.26,2.812-2.82,2.812l0,0c-1.544,0-2.796-1.26-2.796-2.812v-65.757 + c0-1.536,1.252-2.812,2.796-2.812l0,0c1.56,0,2.82,1.276,2.82,2.812V489.371z"/> + <path style="fill:#F2DB06;" d="M358.984,489.371c0,1.552-1.26,2.812-2.828,2.812l0,0c-1.544,0-2.804-1.26-2.804-2.812v-65.757 + c0-1.536,1.26-2.812,2.804-2.812l0,0c1.568,0,2.828,1.276,2.828,2.812V489.371z"/> + <path style="fill:#F2DB06;" d="M376.833,489.371c0,1.552-1.268,2.812-2.812,2.812l0,0c-1.56,0-2.82-1.26-2.82-2.812v-65.757 + c0-1.536,1.26-2.812,2.82-2.812l0,0c1.544,0,2.812,1.276,2.812,2.812V489.371z"/> +</g> +<circle style="fill:#FFAA00;" cx="252.062" cy="131.853" r="92.987"/> +<path style="fill:#FF8500;" d="M252.062,38.882c51.358,0,92.987,41.622,92.987,92.979s-41.63,92.979-92.987,92.979"/> +<path style="fill:#54391E;" d="M252.566,57.038c-4.151,0-7.507-3.363-7.507-7.507c0-17.258-14.052-31.319-31.327-31.319 + c-4.151,0-7.507-3.356-7.507-7.515c0-4.151,3.356-7.507,7.507-7.507c25.553,0,46.34,20.787,46.34,46.34 + C260.081,53.675,256.725,57.038,252.566,57.038z"/> +<path style="fill:#382413;" d="M213.741,17.275c-0.819,0-1.575,0.221-2.332,0.473c0.756,0.244,1.512,0.473,2.332,0.473 + c17.266,0,31.327,14.052,31.327,31.319c0,4.143,3.356,7.507,7.507,7.507c2.725,0,5.01-1.52,6.333-3.694 + C254.197,32.722,235.757,17.275,213.741,17.275z"/> +<path style="fill:#7EC441;" d="M305.799,3.9c-12.768-8.271-34.068-3.119-50.105,12.902c-16.03,16.037-13.383,29.562-3.718,40.921 + L305.799,3.9z"/> +<path style="fill:#559E1B;" d="M305.696,3.79c8.263,12.761,3.119,34.068-12.91,50.105c-16.03,16.03-29.554,13.375-40.921,3.718 + L305.696,3.79z"/> +<g> + <path style="fill:#F4C951;" d="M236.285,55.187c7.593,7.593-9.051,17.912-25.687,34.54s-26.947,33.264-34.54,25.679 + c-7.57-7.578-0.236-27.199,16.384-43.827C209.078,54.943,228.691,47.609,236.285,55.187z"/> + <circle style="fill:#F4C951;" cx="177.129" cy="129.971" r="4.072"/> + <circle style="fill:#F4C951;" cx="176.814" cy="147.828" r="1.252"/> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +</svg>
binary file changed (absent → 335056 bytes)
binary file changed (absent → 359628 bytes)
@@ -0,0 +1,118 @@+/* Adapted from https://neuron.zettel.page/uplink-tree+ Originally from https://codepen.io/philippkuehn/pen/QbrOaN+*/++body .tree.flipped {+ -webkit-transform: rotate(180deg);+ -moz-transform: rotate(180deg);+ -ms-transform: rotate(180deg);+ -o-transform: rotate(180deg);+ transform: rotate(180deg);+}+body .tree {+ overflow: auto;+ /* See more-head.tpl */+ font-family: 'WorkSans', sans-serif;+ font-variation-settings: 'wght' 475;++ font-size: 0.8em;+}+body .tree ul.root {+ padding-top: 0px;+ margin-top: 0px;+}+body .tree ul {+ position: relative;+ padding: 1em 0px 0px 0px;+ white-space: nowrap;+ margin: 0px auto 0px auto;+ text-align: center;+}+body .tree ul::after {+ content: "";+ display: table;+ clear: both;+}+body .tree ul:last-child {+ padding-bottom: 0.1em;+}+body .tree li {+ display: inline-block;+ vertical-align: top;+ text-align: center;+ list-style-type: none;+ position: relative;+ padding: 1em 0.5em 0em 0.5em;+}+body .tree li::before {+ content: "";+ position: absolute;+ top: 0px;+ right: 50%;+ border-top: solid 2px #cccccc;+ width: 60%;+ height: 1.19999em;+}+body .tree li::after {+ content: "";+ position: absolute;+ top: 0px;+ right: 50%;+ border-top: solid 2px #cccccc;+ width: 50%;+ height: 1.19999em;+}+body .tree li::after {+ right: auto;+ left: 50%;+ border-left: solid 2px #cccccc;+}+body .tree li:only-child {+ padding-top: 0em;+}+body .tree li:only-child::after {+ display: none;+}+body .tree li:only-child::before {+ display: none;+}+body .tree li:first-child::before {+ border-style: none;+ border-width: 0px;+}+body .tree li:first-child::after {+ border-radius: 5px 0px 0px 0px;+}+body .tree li:last-child::after {+ border-style: none;+ border-width: 0px;+}+body .tree li:last-child::before {+ border-right: solid 2px #cccccc;+ border-radius: 0px 5px 0px 0px;+}+body .tree ul ul::before {+ content: "";+ position: absolute;+ top: 0px;+ left: 50%;+ border-left: solid 2px #cccccc;+ width: 0px;+ height: 1.19999em;+}+body .tree li div.forest-link {+ border: solid 2px #cccccc;+ padding: 0.2em 0.29999em 0.2em 0.29999em;+ text-decoration: none;+ display: inline-block;+ border-radius: 5px 5px 5px 5px;+ position: relative;+ top: 2px;+}+body .tree.flipped li div.forest-link {+ -webkit-transform: rotate(180deg);+ -moz-transform: rotate(180deg);+ -ms-transform: rotate(180deg);+ -o-transform: rotate(180deg);+ transform: rotate(180deg);+}
@@ -0,0 +1,191 @@+.stork-wrapper-flat {+ position: relative;+ font-family: inherit;+ box-sizing: border-box;+ font-size: 1em;++ --stork-blue-2: #a5d8ff;+ --stork-blue-3: #74c0fc;+ --stork-blue-4: #4dabf7;+ --stork-blue-5: #339af0;+ --stork-blue-7: #1c7ed6;+ --stork-gray-8: #343a40;+ --stork-gray-9: #212529;+ --stork-yellow-2: #ffec99;++ --stork-border-color: hsl(0, 0%, 80%);+ --stork-background-color: hsla(0, 0%, 97%);+ --stork-text-color: var(--stork-gray-9);++ --stork-input-height: 2.4em;+}++.stork-wrapper-flat *,+.stork-wrapper-flat *:before,+.stork-wrapper-flat *:after {+ box-sizing: border-box;+}++.stork-wrapper-flat .stork-input {+ width: 100%;+ height: var(--stork-input-height);+ font-size: 1em;+ padding: 0.4em 0.8em;+ position: relative;+ border: 2px solid var(--stork-border-color);+ border-radius: calc(var(--stork-input-height) / 2);+ background-color: var(--stork-background-color);+ color: var(--stork-text-color);+ font-family: inherit;+}++.stork-wrapper-flat .stork-input:focus {+ outline: none;+}++.stork-wrapper-flat .stork-progress {+ position: absolute;+ display: block;+ content: "";+ bottom: 1px;+ background-color: var(--stork-blue-5);+ box-shadow: 0 0 8px var(--stork-blue-4);+ height: 1px;+ transition: width 0.25s ease, opacity 0.4s ease 0.4s;+ margin-left: calc(var(--stork-input-height) / 2);+ max-width: calc(100% - var(--stork-input-height));+}++.stork-wrapper-flat .stork-output {+ position: absolute;+ width: 100%;+ margin-top: 0.5em;+ border-radius: 6px;+ display: flex;+ flex-direction: column;+ z-index: 100;+ color: var(--stork-text-color);+ font-weight: 400;+ font-family: inherit;+}++.stork-wrapper-flat .stork-attribution a:link,+.stork-wrapper-flat .stork-attribution a:visited {+ color: var(--stork-blue-7);+}++.stork-wrapper-flat .stork-output-visible {+ border: 2px solid var(--stork-border-color);+ background: var(--stork-background-color);+}++.stork-wrapper-flat .stork-message {+ width: 100%;+ padding: 0.5em 1em;+ color: var(--stork-text-color);+}++.stork-wrapper-flat .stork-attribution {+ width: 100%;+ padding: 0.5em 1em;+ font-size: 0.8em;+ color: var(--stork-text-color);+}++.stork-wrapper-flat .stork-results {+ margin: 0;+ padding: 0;+ width: 100%;+ list-style-type: none;+ max-height: 25em;+ overflow-y: scroll;+ border-top: 1px solid var(--stork-border-color);+ border-bottom: 1px solid var(--stork-border-color);+}++.stork-wrapper-flat .stork-result:not(:last-child) {+ border-bottom: 1px solid var(--stork-border-color);+}++.stork-wrapper-flat .stork-result.selected {+ background: var(--stork-blue-2);+}++.stork-wrapper-flat .stork-result a:link {+ padding: 1em;+ display: block;+ color: currentcolor;+ text-decoration: none;+}++.stork-wrapper-flat .stork-result p {+ margin: 0;+}++.stork-wrapper-flat .stork-title {+ font-weight: bold;+ font-size: 0.95em;+ margin: 0;+ color: var(--stork-text-color);++ /* Flexbox container for the title and the score, when debugging */+ display: flex;+ justify-content: space-between;+}++.stork-wrapper-flat .stork-excerpt-container {+ margin-top: 0.75em;+}++.stork-wrapper-flat .stork-excerpt {+ font-size: 0.8em;+ line-height: 1;+ margin: 0;+ color: var(--stork-gray-8);++ /* Flexbox container for the title and the score, when debugging */+ display: flex;+ justify-content: space-between;+}++.stork-wrapper-flat .stork-excerpt:not(:last-of-type) {+ margin-bottom: 0.6em;+}++.stork-wrapper-flat .stork-highlight {+ background-color: var(--stork-yellow-2);+ padding: 0 0.1em;+}++.stork-wrapper-flat .stork-error {+ outline: 2px solid #c92a2a;+}++.stork-wrapper-flat .stork-close-button {+ position: absolute;+ right: 0;+ margin: 0.5em 0.5em;+ height: 1.4em;+ width: 1.4em;+ padding: 0px;+ background: hsl(0, 0%, 85%);+ border: 1px solid hsla(0, 0%, 70%);+ font-size: 1em;+ color: hsl(0, 0%, 50%);+ border-radius: 50%;+ line-height: 1;+}++.stork-wrapper-flat .stork-close-button svg {+ width: 11px;+ height: 11px;+}++.stork-wrapper-flat .stork-close-button:hover {+ background: hsla(0, 0%, 78%);+ cursor: pointer;+}++.stork-wrapper-flat .stork-close-button:active {+ background: hsla(0, 0%, 65%);+}
@@ -0,0 +1,2 @@+var stork;(()=>{"use strict";var e={214:(e,t,n)=>{let r;n.r(t),n.d(t,{default:()=>_,wasm_register_index:()=>v,wasm_search:()=>y,wasm_stork_version:()=>w});const o=new Array(32).fill(void 0);function s(e){return o[e]}o.push(void 0,null,!0,!1);let i=o.length;let a=0,l=null;function u(){return null!==l&&l.buffer===r.memory.buffer||(l=new Uint8Array(r.memory.buffer)),l}let c=new TextEncoder("utf-8");const d="function"==typeof c.encodeInto?function(e,t){return c.encodeInto(e,t)}:function(e,t){const n=c.encode(e);return t.set(n),{read:e.length,written:n.length}};function h(e,t,n){if(void 0===n){const n=c.encode(e),r=t(n.length);return u().subarray(r,r+n.length).set(n),a=n.length,r}let r=e.length,o=t(r);const s=u();let i=0;for(;i<r;i++){const t=e.charCodeAt(i);if(t>127)break;s[o+i]=t}if(i!==r){0!==i&&(e=e.slice(i)),o=n(o,r,r=i+3*e.length);const t=u().subarray(o+i,o+r);i+=d(e,t).written}return a=i,o}let f=null;function g(){return null!==f&&f.buffer===r.memory.buffer||(f=new Int32Array(r.memory.buffer)),f}let p=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});function m(e,t){return p.decode(u().subarray(e,e+t))}function v(e,t){try{const d=r.__wbindgen_add_to_stack_pointer(-16);var n=h(e,r.__wbindgen_malloc,r.__wbindgen_realloc),o=a,s=function(e,t){const n=t(1*e.length);return u().set(e,n/1),a=e.length,n}(t,r.__wbindgen_malloc),i=a;r.wasm_register_index(d,n,o,s,i);var l=g()[d/4+0],c=g()[d/4+1];return m(l,c)}finally{r.__wbindgen_add_to_stack_pointer(16),r.__wbindgen_free(l,c)}}function y(e,t){try{const c=r.__wbindgen_add_to_stack_pointer(-16);var n=h(e,r.__wbindgen_malloc,r.__wbindgen_realloc),o=a,s=h(t,r.__wbindgen_malloc,r.__wbindgen_realloc),i=a;r.wasm_search(c,n,o,s,i);var l=g()[c/4+0],u=g()[c/4+1];return m(l,u)}finally{r.__wbindgen_add_to_stack_pointer(16),r.__wbindgen_free(l,u)}}function w(){try{const n=r.__wbindgen_add_to_stack_pointer(-16);r.wasm_stork_version(n);var e=g()[n/4+0],t=g()[n/4+1];return m(e,t)}finally{r.__wbindgen_add_to_stack_pointer(16),r.__wbindgen_free(e,t)}}p.decode();const _=async function e(t){void 0===t&&(t=new URL("stork_bg.wasm",function(e){const t=n.p;let r="";return(!t||t.indexOf("://")<0)&&(r+=window.location.protocol+"//"+window.location.host),r+=t||"/",r+e}("node_modules/stork-search/stork.js")));const l={wbg:{}};l.wbg.__wbg_new_693216e109162396=function(){return function(e){i===o.length&&o.push(o.length+1);const t=i;return i=o[t],o[t]=e,t}(new Error)},l.wbg.__wbg_stack_0ddaca5d1abfb52f=function(e,t){var n=h(s(t).stack,r.__wbindgen_malloc,r.__wbindgen_realloc),o=a;g()[e/4+1]=o,g()[e/4+0]=n},l.wbg.__wbg_error_09919627ac0992f5=function(e,t){try{console.error(m(e,t))}finally{r.__wbindgen_free(e,t)}},l.wbg.__wbindgen_object_drop_ref=function(e){!function(e){const t=s(e);(function(e){e<36||(o[e]=i,i=e)})(e)}(e)},("string"==typeof t||"function"==typeof Request&&t instanceof Request||"function"==typeof URL&&t instanceof URL)&&(t=fetch(t));const{instance:u,module:c}=await async function(e,t){if("function"==typeof Response&&e instanceof Response){if("function"==typeof WebAssembly.instantiateStreaming)try{return await WebAssembly.instantiateStreaming(e,t)}catch(t){if("application/wasm"==e.headers.get("Content-Type"))throw t;console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",t)}const n=await e.arrayBuffer();return await WebAssembly.instantiate(n,t)}{const n=await WebAssembly.instantiate(e,t);return n instanceof WebAssembly.Instance?{instance:n,module:e}:n}}(await t,l);return r=u.exports,e.__wbindgen_wasm_module=c,r}},914:(e,t,n)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.calculateOverriddenConfig=t.defaultConfig=void 0;var r=n(445),o=n(466);t.defaultConfig={showProgress:!0,printIndexInfo:!1,showScores:!1,showCloseButton:!0,minimumQueryLength:3,forceOverwrite:!1,resultNoun:{singular:"file",plural:"files"},onQueryUpdate:void 0,onResultSelected:void 0,onResultsHidden:void 0,onInputCleared:void 0,transformResultUrl:function(e){return e}},t.calculateOverriddenConfig=function(e){var n=(0,o.difference)(Object.keys(e),Object.keys(t.defaultConfig));if(n.length>0){var s=(0,o.plural)(n.length,"key","keys"),i=JSON.stringify(n);return new r.default("Invalid ".concat(s," in config object: ").concat(i))}for(var a=Object.assign({},t.defaultConfig),l=0,u=Object.keys(t.defaultConfig);l<u.length;l++){var c=u[l],d=e[c];void 0!==d&&(a[c]=d)}return a}},227:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.existsBeyondContainerBounds=t.setText=t.clear=t.add=t.create=void 0,t.create=function(e,t){var n=document.createElement(e);return t.classNames&&n.setAttribute("class",t.classNames.join(" ")),n},t.add=function(e,t,n){n.insertAdjacentElement(t,e)},t.clear=function(e){for(;e&&e.firstChild;)e.removeChild(e.firstChild)},t.setText=function(e,t){var n=document.createTextNode(t);e&&e.firstChild?e.replaceChild(n,e.firstChild):e&&e.appendChild(n)},t.existsBeyondContainerBounds=function(e,t){var n=e.getBoundingClientRect(),r=t.getBoundingClientRect();return n.bottom>r.bottom||n.top<r.top}},934:(e,t,n)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.Entity=void 0;var r=n(139),o=n(771),s=n(214),i=n(445),a=function(){function e(e,t,n){var r=this;this._state="initialized",this.downloadProgress=0,this.results=[],this.totalResultCount=0,this.eventListenerFunctions={},this.highlightedResult=0,this.resultsVisible=!1,this.hoverSelectEnabled=!0,this.setDownloadProgress=function(e){r.state="loading",r.downloadProgress=e,r.config.showProgress&&r.render()},this.name=e,this.url=t,this.config=n}return Object.defineProperty(e.prototype,"state",{get:function(){return this._state},set:function(e){this._state=e,this.render()},enumerable:!1,configurable:!0}),e.prototype.getCurrentMessage=function(){if(!this.domManager)return null;var e=this.domManager.getQuery();return"error"===this.state?"Error! Check the browser console.":"ready"!=this.state?"Loading...":(null==e?void 0:e.length)<this.config.minimumQueryLength?"Filtering...":this.results?0===this.totalResultCount?"No ".concat(this.config.resultNoun.plural," found."):1===this.totalResultCount?"1 ".concat(this.config.resultNoun.singular," found."):"".concat(this.totalResultCount," ").concat(this.config.resultNoun.plural," found."):null},e.prototype.generateRenderConfig=function(){return{results:this.results,resultsVisible:!0,showScores:this.config.showScores,message:this.getCurrentMessage(),showProgress:this.config.showProgress,progress:this.downloadProgress,state:this.state}},e.prototype.render=function(){this.domManager&&this.domManager.render(this.generateRenderConfig())},e.prototype.registerIndex=function(e){var t=this;return new Promise((function(n,r){var o=JSON.parse((0,s.wasm_register_index)(t.name,e));o.error?r(new i.default(o.error)):(t.config.printIndexInfo&&console.log(o),t.state="ready",n(o))}))},e.prototype.attachToDom=function(){this.domManager=new o.EntityDom(this.name,this),this.render()},e.prototype.injestSearchData=function(e){var t=this;this.results=e.results,this.totalResultCount=e.total_hit_count,this.highlightedResult=0;var n=e.url_prefix||"";this.results.map((function(e){var r="",o=e.excerpts.map((function(e){return e.internal_annotations})).filter((function(e){return!!e}))[0];if(o&&o[0]){var s=o[0];"string"==typeof s.a&&(r+=s.a)}e.excerpts&&e.excerpts[0]&&e.excerpts[0].internal_annotations&&e.excerpts[0].internal_annotations[0]&&e.excerpts[0].internal_annotations[0].a&&"string"==typeof e.excerpts[0].internal_annotations[0].a&&(r=e.excerpts[0].internal_annotations[0].a),e.entry.url=t.config.transformResultUrl("".concat(n).concat(e.entry.url).concat(r))})),this.render()},e.prototype.getSanitizedResults=function(){var e=this.results;return e.map((function(e){delete e.title_highlight_ranges,e.excerpts.map((function(e){delete e.highlight_ranges,delete e.internal_annotations}))})),e},e.prototype.setDownloadError=function(){this.state="error"},e.prototype.performSearch=function(e){if("ready"===this.state){if(e.length<this.config.minimumQueryLength)return this.results=[],void this.render();try{var t=(0,r.resolveSearch)(this.name,e);if(!t)return;this.injestSearchData(t),this.config.onQueryUpdate&&this.config.onQueryUpdate(e,this.getSanitizedResults())}catch(e){console.error(e)}}else this.render()},e}();t.Entity=a},771:(e,t,n)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.EntityDom=void 0;var r=n(227),o=n(9),s={results:[],resultsVisible:!1,showScores:!1,message:null,showProgress:!1,progress:1,state:"ready"},i=function(){function e(e,t){var n,o,i=this;this.scrollAnchorPoint="end",this.entity=t;var a=[{selector:'input[data-stork="'.concat(e,'"]'),elementName:"input"},{selector:'div[data-stork="'.concat(e,'-output"]'),elementName:"output"}].map((function(t){var n=document.querySelector(t.selector);if(!n)throw new Error('Could not register search box "'.concat(e,'": ').concat(t.elementName," element not found. Make sure an element matches the query selector `").concat(t.selector,"`"));return n})),l=a[0],u=a[1];this.elements={input:l,output:u,list:(0,r.create)("ul",{classNames:["stork-results"]}),attribution:(0,r.create)("div",{classNames:["stork-attribution"]}),progress:(0,r.create)("div",{classNames:["stork-progress"]}),message:(0,r.create)("div",{classNames:["stork-message"]}),closeButton:(0,r.create)("button",{classNames:["stork-close-button"]})},this.elements.input.removeEventListener("input",this.entity.eventListenerFunctions.inputInputEvent),this.elements.input.removeEventListener("keydown",this.entity.eventListenerFunctions.inputKeydownEvent),this.entity.eventListenerFunctions={inputInputEvent:function(e){i.handleInputEvent(e)},inputKeydownEvent:function(e){i.handleKeyDownEvent(e)}},this.elements.input.addEventListener("input",this.entity.eventListenerFunctions.inputInputEvent),this.elements.input.addEventListener("keydown",this.entity.eventListenerFunctions.inputKeydownEvent),null===(n=this.elements.list)||void 0===n||n.addEventListener("mousemove",(function(){i.hoverSelectEnabled=!0})),this.elements.attribution.innerHTML='Powered by <a href="https://stork-search.net">Stork</a>',this.elements.closeButton.innerHTML='\n<svg height="0.8em" viewBox="0 0 23 24" xmlns="http://www.w3.org/2000/svg">\n<g fill="none" fill-rule="evenodd" stroke-linecap="round">\n<g transform="translate(-700 -149)" stroke="currentcolor" stroke-width="4">\n<line id="a" x1="702.5" x2="720" y1="152.5" y2="170"/>\n<line transform="translate(711 161) rotate(-90) translate(-711 -161)" x1="702.5" x2="720" y1="152.5" y2="170"/>\n</g>\n</g>\n</svg>',this.entity.config.showProgress&&(0,r.add)(this.elements.progress,"afterend",this.elements.input),null===(o=this.elements.closeButton)||void 0===o||o.addEventListener("click",(function(){i.elements.input.value="",i.elements.input.focus(),i.render(s);var e=[i.entity.config.onInputCleared,i.entity.config.onResultsHidden],t=e[0],n=e[1];t&&t(),n&&n()}))}return e.prototype.clearDom=function(){var e;(0,r.clear)(this.elements.output),(0,r.clear)(this.elements.list),null===(e=this.elements.closeButton)||void 0===e||e.remove(),this.elements.output.classList.remove("stork-output-visible")},e.prototype.render=function(e){var t,n=this,s=this.elements.input.value;if(this.clearDom(),this.lastRenderState=e,e.showProgress){var i=function(){switch(e.state){case"ready":case"error":return 1;case"initialized":case"loading":return.9*e.progress+.05}}();i<1?(this.elements.progress.style.width="".concat(100*i,"%"),this.elements.progress.style.opacity="1"):(this.elements.progress.style.width="100%",this.elements.progress.style.opacity="0")}if("error"===e.state&&this.elements.input.classList.add("stork-error"),this.getQuery().length>0&&e.resultsVisible&&(this.elements.output.classList.add("stork-output-visible"),(0,r.add)(this.elements.message,"beforeend",this.elements.output)),e.message&&(0,r.setText)(this.elements.message,e.message),(null===(t=e.results)||void 0===t?void 0:t.length)>0&&e.resultsVisible){(0,r.add)(this.elements.list,"beforeend",this.elements.output);for(var a=function(t){var s=e.results[t],i={selected:t===l.highlightedResult,showScores:e.showScores},a=(0,o.resultToListItem)(s,i);(0,r.add)(a,"beforeend",l.elements.list),a.addEventListener("mousemove",(function(){n.hoverSelectEnabled&&t!==n.highlightedResult&&n.changeHighlightedResult({to:t,shouldScrollTo:!1})})),a.addEventListener("mouseleave",(function(){n.hoverSelectEnabled&&t===n.highlightedResult&&n.changeHighlightedResult({to:-1,shouldScrollTo:!1})})),a.addEventListener("click",(function(e){e.preventDefault(),n.selectResult()}))},l=this,u=0;u<e.results.length;u++)a(u);(0,r.add)(this.elements.attribution,"beforeend",this.elements.output)}((null==s?void 0:s.length)||0)>0&&this.entity.config.showCloseButton&&(0,r.add)(this.elements.closeButton,"afterend",this.elements.input)},e.prototype.selectResult=function(){if(null!=this.highlightedResult){var e=this.entity.results[this.highlightedResult];this.entity.config.onResultSelected?Promise.resolve(this.entity.config.onResultSelected(this.getQuery(),e)).finally((function(){window.location.assign(e.entry.url)})):window.location.assign(e.entry.url)}},e.prototype.changeHighlightedResult=function(e){var t,n=this.highlightedResult,o=Math.max(-1,Math.min(this.entity.results.length-1,e.to));this.highlightedResult=o,this.scrollAnchorPoint=(n||0)<o?"end":"start";for(var s=null,i=0;i<this.entity.results.length;i++){var a=null===(t=this.elements.list)||void 0===t?void 0:t.children[i];if(a){var l="selected";i==o?(a.classList.add(l),s=a):a.classList.remove(l)}}return e.shouldScrollTo&&(this.hoverSelectEnabled=!1,s&&(0,r.existsBeyondContainerBounds)(s,this.elements.list)&&s.scrollIntoView({behavior:"smooth",block:this.scrollAnchorPoint,inline:"nearest"})),o},e.prototype.handleKeyDownEvent=function(e){switch(e.keyCode){case 40:if(null==this.highlightedResult)this.changeHighlightedResult({to:0,shouldScrollTo:!0});else{var t=Math.min(this.highlightedResult+1,this.entity.results.length-1);this.changeHighlightedResult({to:t,shouldScrollTo:!0})}break;case 38:null!=this.highlightedResult&&(t=Math.max(0,this.highlightedResult-1),this.changeHighlightedResult({to:t,shouldScrollTo:!0}));break;case 13:this.selectResult();break;case 27:if(this.lastRenderState.resultsVisible)this.render(s),(n=this.entity.config.onResultsHidden)&&n();else if(this.elements.input.value.length>0){var n;this.elements.input.value="",this.render(s),(n=this.entity.config.onInputCleared)&&n()}break;default:return}},e.prototype.handleInputEvent=function(e){this.entity.performSearch(e.target.value)},e.prototype.getQuery=function(){return this.elements.input.value},e}();t.EntityDom=i},129:function(e,t,n){var r=this&&this.__assign||function(){return r=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e},r.apply(this,arguments)};Object.defineProperty(t,"__esModule",{value:!0}),t.debug=t.entityIsReady=t.attachToDom=t.register=void 0;var o=n(934),s=n(914),i=n(794),a=n(258),l=n(445),u={};t.register=function(e,t,n){return new Promise((function(r,c){var d=(0,s.calculateOverriddenConfig)(n);if(d instanceof l.default)c(d);else{u[e]&&!d.forceOverwrite&&c(new l.default("You're registering an index named `".concat(e,"`, but that already exists. If this is expected, set forceOverwrite to true in your Javascript config to allow overwriting indexes.")));var h=new o.Entity(e,t,d);u[e]=h,(0,i.loadIndexFromUrl)(t,{progress:function(e){h.setDownloadProgress(e)},load:function(e){(0,a.runAfterWasmLoaded)((function(){h.registerIndex(new Uint8Array(e)).then(r).catch(c)}),(function(){h.state="error"}))},error:function(){h.setDownloadError(),c()}})}}))},t.attachToDom=function(e){if(!u[e])throw new Error("Index ".concat(e," has not been registered!"));u[e].attachToDom()},t.entityIsReady=function(e){var t;return"ready"===(null===(t=u[e])||void 0===t?void 0:t.state)},t.debug=function(){return{entities:r({},u),entitiesCount:u.length}}},794:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.loadIndexFromUrl=void 0,t.loadIndexFromUrl=function(e,t){var n=new XMLHttpRequest;n.addEventListener("load",(function(e){var n=e.target,r=n.status,o=n.response;0!==r?r<200||r>299?t.error():t.load(o):t.progress(e.loaded/e.total)})),n.addEventListener("error",(function(){t.error()})),n.addEventListener("progress",(function(e){t.progress(e.loaded/e.total)})),n.responseType="arraybuffer",n.open("GET",e),n.send()}},480:function(e,t,n){var r=this&&this.__assign||function(){return r=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e},r.apply(this,arguments)};Object.defineProperty(t,"__esModule",{value:!0}),t.debug=t.register=t.search=t.attach=t.downloadIndex=t.initialize=void 0;var o=n(129),s=n(258),i=n(139),a=n(445),l=n(34),u=n(214);function c(e){return void 0===e&&(e=null),(0,s.loadWasm)(e).then((function(){}))}function d(e,t,n){return void 0===n&&(n={}),new Promise((function(r,s){var i=(0,l.validateIndexParams)(e,t);i?s(i):(0,o.register)(e,t,n).then(r).catch(s)}))}function h(e){try{(0,o.attachToDom)(e)}catch(e){throw new a.default(e.message)}}t.initialize=c,t.downloadIndex=d,t.attach=h,t.register=function(e,t,n){void 0===n&&(n={});var r=c(),o=d(e,t,n);return h(e),Promise.all([r,o]).then()},t.search=function(e,t){if(!e||!t)throw new a.default("Make sure to call stork.search() with two arguments: the index name and the search query.");if(!(0,o.entityIsReady)(e))throw new a.default("Couldn't find index. Make sure the stork.downloadIndex() promise has resolved before calling stork.search().");return(0,i.resolveSearch)(e,t)},t.debug=function(){return r(r(r({},(0,s.debug)()),(0,o.debug)()),{jsStorkVersion:"1.5.0",wasmStorkVersion:u.wasm_stork_version})}},112:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.highlight=void 0,t.highlight=function(e,t){function n(e,t,n){return e.substr(0,t)+n+e.substr(t)}for(var r=0,o=0,s=t;o<s.length;o++){var i=s[o],a='<mark class="stork-highlight">',l="</mark>";e=n(e,i.beginning+r,a),r+=a.length,e=n(e,i.end+r,l),r+=l.length}return e}},9:(e,t,n)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.resultToListItem=void 0;var r=n(112);t.resultToListItem=function(e,t){var n=document.createElement("template");return n.innerHTML='\n<li class="stork-result'.concat(t.selected?" selected":"",'">\n <a href="').concat(e.entry.url,'">\n <div class="stork-title">\n <p>').concat((0,r.highlight)(e.entry.title,e.title_highlight_ranges||[]),"</p>\n ").concat(t.showScores?"<code><b>".concat(e.score,"</b></code>"):"","\n </div>\n ").concat(e.excerpts.length>0?'<div class="stork-excerpt-container" />':"","\n ").concat(e.excerpts.map((function(e){return'<div class="stork-excerpt"><p>\n ...'.concat((0,r.highlight)(e.text,e.highlight_ranges||[]),"...\n </p>\n ").concat(t.showScores?"<code>".concat(e.score,"</code>"):"","\n </div>")})).join(""),"\n ").concat(e.excerpts.length>0?"</div>":"","\n </a>\n</li>"),n.content.firstElementChild}},139:(e,t,n)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.resolveSearch=void 0;var r=n(214),o=n(445);t.resolveSearch=function(e,t){var n=null,s=null;try{n=(0,r.wasm_search)(e,t),s=JSON.parse(n)}catch(e){throw new o.default("Could not parse data from wasm_search. If you see this, please file a bug: https://jil.im/storkbug "+n)}if(!s)throw new o.default("Data was an empty object");if(s.error)throw new o.default("Could not perform search: the WASM binary failed to return search results.\n You might not be serving your search index properly.\n If you think this is an error, please file a bug: https://jil.im/storkbug\n \n The WASM binary came back with:\n ".concat(s.error));return s}},445:function(e,t){var n,r=this&&this.__extends||(n=function(e,t){return n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},n(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0});var o=function(e){function t(t){var n=e.call(this,t)||this;return n.name="StorkError",n}return r(t,e),t}(Error);t.default=o},466:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.plural=t.difference=t.htmlToElement=void 0,t.htmlToElement=function(e){var t=document.createElement("template");return e=e.trim(),t.innerHTML=e,t.content.firstChild},t.difference=function(e,t){var n=new Set(e),r=new Set(t),o=new Set(Array.from(n).filter((function(e){return!r.has(e)})));return Array.from(o)},t.plural=function(e,t,n){return 1==e?t:n}},34:(e,t,n)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.validateIndexParams=void 0;var r=n(445);t.validateIndexParams=function(e,t){return"string"!=typeof e?new r.default("Index registration name must be a string."):"string"!=typeof t?new r.default("URL must be a string."):null}},258:(e,t,n)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.debug=t.loadWasm=t.runAfterWasmLoaded=void 0;var r=n(214),o=n(445),s="https://files.stork-search.net/releases/v".concat("1.5.0","/stork.wasm"),i=null,a=null,l=[],u=[];t.loadWasm=function(e){if(void 0===e&&(e=null),a)return a;var t=e||s;i=t;var n=(0,r.default)(t).then((function(){return c(),t})).catch((function(){throw d(),new o.default("Error while loading WASM at ".concat(t))}));return a=n,n},t.runAfterWasmLoaded=function(e,t){return a?(a.then((function(){return e()})).catch((function(){return t()})),a):(l.push(e),u.push(t),null)};var c=function(){l.forEach((function(e){e()})),l=[]},d=function(){u.forEach((function(e){e()})),u=[]};t.debug=function(){return{wasmSourceUrl:i,wasmLoadPromise:a,queueLength:l.length}}}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var s=t[r]={exports:{}};return e[r].call(s.exports,s,s.exports,n),s.exports}n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;n.g.importScripts&&(e=n.g.location+"");var t=n.g.document;if(!e&&t&&(t.currentScript&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");r.length&&(e=r[r.length-1].src)}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),n.p=e})();var r=n(480);stork=r})();+//# sourceMappingURL=stork.js.map
binary file changed (absent → 351489 bytes)
@@ -0,0 +1,92 @@+<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 504.124 504.124" style="enable-background:new 0 0 504.124 504.124;" xml:space="preserve"> +<path style="fill:#EA0C5C;" d="M482.951,396.006c0,8.531-6.908,15.447-15.447,15.447H95.461c-8.515,0-15.439-6.916-15.439-15.447 + v-64.37c0-8.531,6.924-15.447,15.439-15.447h372.043c8.539,0,15.447,6.916,15.447,15.447L482.951,396.006L482.951,396.006z"/> +<path style="fill:#BC0852;" d="M467.512,316.197c8.539,0,15.447,6.916,15.447,15.447v64.37c0,8.531-6.908,15.447-15.447,15.447 + H95.461"/> +<path style="fill:#FF7171;" d="M80.022,331.643c0-8.531,6.924-15.447,15.439-15.447h372.043c8.539,0,15.447,6.916,15.447,15.447"/> +<g> + <path style="fill:#F2DB06;" d="M399.991,396.699c0,1.552-1.268,2.812-2.812,2.812l0,0c-1.552,0-2.82-1.26-2.82-2.812V330.95 + c0-1.56,1.268-2.82,2.82-2.82l0,0c1.544,0,2.812,1.26,2.812,2.82V396.699z"/> + <path style="fill:#F2DB06;" d="M417.84,396.699c0,1.552-1.26,2.812-2.812,2.812l0,0c-1.56,0-2.82-1.26-2.82-2.812V330.95 + c0-1.56,1.26-2.82,2.82-2.82l0,0c1.552,0,2.812,1.26,2.812,2.82V396.699z"/> + <path style="fill:#F2DB06;" d="M435.689,396.699c0,1.552-1.252,2.812-2.82,2.812l0,0c-1.544,0-2.804-1.26-2.804-2.812V330.95 + c0-1.56,1.26-2.82,2.804-2.82l0,0c1.568,0,2.82,1.26,2.82,2.82V396.699z"/> +</g> +<path style="fill:#038462;" d="M424.102,302.089c0,8.523-6.916,15.447-15.447,15.447H36.613c-8.531,0-15.447-6.924-15.447-15.447 + v-64.37c0-8.539,6.916-15.447,15.447-15.447h372.043c8.531,0,15.447,6.908,15.447,15.447V302.089z"/> +<path style="fill:#047769;" d="M408.655,222.272c8.531,0,15.447,6.908,15.447,15.447v64.37c0,8.523-6.916,15.447-15.447,15.447 + H36.613"/> +<path style="fill:#2EAF8D;" d="M21.174,237.719c0-8.539,6.916-15.447,15.447-15.447h372.035c8.531,0,15.447,6.908,15.447,15.447"/> +<g> + <path style="fill:#F2DB06;" d="M341.134,302.775c0,1.552-1.26,2.812-2.82,2.812l0,0c-1.544,0-2.796-1.26-2.796-2.812v-65.757 + c0-1.552,1.252-2.812,2.796-2.812l0,0c1.56,0,2.82,1.26,2.82,2.812V302.775z"/> + <path style="fill:#F2DB06;" d="M358.984,302.775c0,1.552-1.26,2.812-2.828,2.812l0,0c-1.544,0-2.804-1.26-2.804-2.812v-65.757 + c0-1.552,1.26-2.812,2.804-2.812l0,0c1.568,0,2.828,1.26,2.828,2.812V302.775z"/> + <path style="fill:#F2DB06;" d="M376.833,302.775c0,1.552-1.268,2.812-2.812,2.812l0,0c-1.56,0-2.82-1.26-2.82-2.812v-65.757 + c0-1.552,1.26-2.812,2.82-2.812l0,0c1.544,0,2.812,1.26,2.812,2.812V302.775z"/> +</g> +<path style="fill:#115989;" d="M424.102,488.678c0,8.523-6.916,15.447-15.447,15.447H36.613c-8.531,0-15.447-6.924-15.447-15.447 + v-64.37c0-8.531,6.916-15.447,15.447-15.447h372.043c8.531,0,15.447,6.916,15.447,15.447V488.678z"/> +<path style="fill:#003B5B;" d="M408.655,408.861c8.531,0,15.447,6.916,15.447,15.447v64.37c0,8.523-6.916,15.447-15.447,15.447 + H36.613"/> +<path style="fill:#158ACC;" d="M21.174,424.308c0-8.531,6.916-15.447,15.447-15.447h372.035c8.531,0,15.447,6.916,15.447,15.447"/> +<g> + <path style="fill:#F2DB06;" d="M341.134,489.371c0,1.552-1.26,2.812-2.82,2.812l0,0c-1.544,0-2.796-1.26-2.796-2.812v-65.757 + c0-1.536,1.252-2.812,2.796-2.812l0,0c1.56,0,2.82,1.276,2.82,2.812V489.371z"/> + <path style="fill:#F2DB06;" d="M358.984,489.371c0,1.552-1.26,2.812-2.828,2.812l0,0c-1.544,0-2.804-1.26-2.804-2.812v-65.757 + c0-1.536,1.26-2.812,2.804-2.812l0,0c1.568,0,2.828,1.276,2.828,2.812V489.371z"/> + <path style="fill:#F2DB06;" d="M376.833,489.371c0,1.552-1.268,2.812-2.812,2.812l0,0c-1.56,0-2.82-1.26-2.82-2.812v-65.757 + c0-1.536,1.26-2.812,2.82-2.812l0,0c1.544,0,2.812,1.276,2.812,2.812V489.371z"/> +</g> +<circle style="fill:#FFAA00;" cx="252.062" cy="131.853" r="92.987"/> +<path style="fill:#FF8500;" d="M252.062,38.882c51.358,0,92.987,41.622,92.987,92.979s-41.63,92.979-92.987,92.979"/> +<path style="fill:#54391E;" d="M252.566,57.038c-4.151,0-7.507-3.363-7.507-7.507c0-17.258-14.052-31.319-31.327-31.319 + c-4.151,0-7.507-3.356-7.507-7.515c0-4.151,3.356-7.507,7.507-7.507c25.553,0,46.34,20.787,46.34,46.34 + C260.081,53.675,256.725,57.038,252.566,57.038z"/> +<path style="fill:#382413;" d="M213.741,17.275c-0.819,0-1.575,0.221-2.332,0.473c0.756,0.244,1.512,0.473,2.332,0.473 + c17.266,0,31.327,14.052,31.327,31.319c0,4.143,3.356,7.507,7.507,7.507c2.725,0,5.01-1.52,6.333-3.694 + C254.197,32.722,235.757,17.275,213.741,17.275z"/> +<path style="fill:#7EC441;" d="M305.799,3.9c-12.768-8.271-34.068-3.119-50.105,12.902c-16.03,16.037-13.383,29.562-3.718,40.921 + L305.799,3.9z"/> +<path style="fill:#559E1B;" d="M305.696,3.79c8.263,12.761,3.119,34.068-12.91,50.105c-16.03,16.03-29.554,13.375-40.921,3.718 + L305.696,3.79z"/> +<g> + <path style="fill:#F4C951;" d="M236.285,55.187c7.593,7.593-9.051,17.912-25.687,34.54s-26.947,33.264-34.54,25.679 + c-7.57-7.578-0.236-27.199,16.384-43.827C209.078,54.943,228.691,47.609,236.285,55.187z"/> + <circle style="fill:#F4C951;" cx="177.129" cy="129.971" r="4.072"/> + <circle style="fill:#F4C951;" cx="176.814" cy="147.828" r="1.252"/> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +<g> +</g> +</svg>
@@ -0,0 +1,7 @@+# My Emanote Site++Welcome to [Emanote](https://emanote.srid.ca/).++Your notes should be visible in the sidebar (to the left), or in the [index](-/all) page (see sidebar & footer). To get rid of this message, add a file named `index.md` (or `index.org`).++#emanote/default-layer
@@ -0,0 +1,128 @@+# These variables are made globally available in the HTML templates.+# Make sure that they do not conflict with HTML tag names.+# You can override them on a per-note or per-directory basis by creating the+# associated .yaml file.++# Order is used by Emanote to determine the sidebar order of notes.+order: 0++tags: []++# TODO: How to design this when generating RSS feeds?+template:+ # Which template to use to render notes. The filepath without the .tpl extension.+ name: /templates/layouts/book+ # Layout specific settings+ layout:+ # For base.tpl+ base:+ # The class to apply for <body> element when using base.tpl (used by note and book layouts)+ bodyClass: bg-gray-400 overflow-y-scroll+ note:+ containerClass: container mx-auto max-w-prose+ error:+ containerClass: container mx-auto max-w-prose+ uptree:+ # Class to apply for uptree's nodes+ nodeClass: text-gray-900+ # List of available colors: https://v2.tailwindcss.com/docs/customizing-colors#default-color-palette+ theme: blue+ # Value of the <base> tag.+ baseUrl: /+ # Change this to 'pretty' if you want URLs without the '.html' suffix.+ urlStrategy: direct+ iconUrl: ${ema:homeUrl}favicon.svg+ sidebar:+ # Whether this node in the sidebar tree should remain collapsed by default+ # (unless a route in it is active)+ collapsed: true++pandoc:+ # Rewrite the class specified in Pandoc's Div and Span nodes. You can specify the class using+ # https://github.com/jgm/commonmark-hs/blob/master/commonmark-extensions/test/attributes.md+ #+ # This feature is useful when managing a "library" of CSS styling to use on+ # Markdown text, especially when using Tailwind.+ rewriteClass:+ # How to style inline tags. Tag-specific styles can be applied as well.+ emanote:inline-tag: font-bold bg-gray-100 py-0.5 px-2 rounded-lg+ # This style is used by `#a/red/tag` in addition to the generic style above.+ # As this class comes *after* the generic class, it can override the styles+ # in the generic class.+ emanote:inline-tag:a/red/tag: bg-red-100+ emanote:placeholder-message: text-gray-400 border-t-2 inline-block pt-0.5+ emanote:error: text-l bg-red-100 p-2 border-2 border-black m-2 font-mono+ emanote:error:aside: font-mono align-top text-xs mr-1 tracking-tighter opacity-50 hover:opacity-100+ # You can also add your own class -> style mappings. We provide a sample below.+ sticky-note: px-3 py-1 rounded shadow bg-yellow-100 mx-2 transform -skew-y-1 scale-95 hover:scale-100 hover:border-yellow-400 hover:shadow-lg border-t-8 border-yellow-200 mb-8 mt-8+ note: p-2 mb-3 rounded shadow bg-gray-100 w-full float-none md:float-right md:w-1/2 md:clear-both+ highlight-block: px-3 py-1 mb-3 rounded bg-${theme}-100 hover:border-${theme}-400 hover:shadow border-t-8 border-${theme}-200+ highlight-inline: bg-yellow-200 px-2 py-0.5 rounded-xl+ center: flex justify-center items-center mx-auto++# Put page-specific metadata here. Override them in Markdown frontmatter or+# per-folder YAML as necessary.+page:+ siteTitle: My Emanote Site+ # Desription is expected to be set on a per-page basis.+ # By default, the first paragraph is used for description.+ # Used for https://ogp.me/+ description: ""++ # Image URL for previews.+ # By default, the first image URL is used. Note: wikilink are not yet+ # supported.+ # Used for https://ogp.me/+ image: ""++ # Put anything that should go in <head> of these routes here:+ # You can reference other metadata keys using <snippet var=".." />+ # Or use the JS behaviour library below.+ headHtml: |+ <meta name="generator" content="Emanote" />++# Builtin JS behaviour library. Use them in `page.headHtml` of your .yaml or .md+# frontmatter.+js:+ # Syntax highlighting using prism.js+ prism: |+ <!-- Prism.js (doesn't work great in live server) -->+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.28.0/themes/prism-tomorrow.min.css" rel="stylesheet" />+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.28.0/prism.min.js"></script>+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.28.0/plugins/autoloader/prism-autoloader.min.js"></script>+ # Syntax highlighting using highlight.js+ highlightjs: |+ <!-- highlight.js -->+ <with var="js">+ <link rel="stylesheet"+ href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${value:highlightjs-ver}/styles/hybrid.min.css">+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${value:highlightjs-ver}/highlight.min.js"></script>+ <!-- Include languages that Emanote itself uses -->+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${value:highlightjs-ver}/languages/haskell.min.js"></script>+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/${value:highlightjs-ver}/languages/nix.min.js"></script>+ <script>hljs.highlightAll();</script>+ </with>+ highlightjs-ver: 11.6.0 # Ref: https://cdnjs.com/libraries/highlight.js+ # Diagrams using mermaid.js+ mermaid: |+ <!-- mermaid.js -->+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>+ <script>+ mermaid.initialize({startOnLoad:false});+ mermaid.init(undefined,document.querySelectorAll(".mermaid"));+ </script>+ mathjax: |+ <script>+ window.MathJax = {+ startup: {+ ready: () => {+ MathJax.startup.defaultReady();+ }+ }+ };+ </script>+ <script async="" id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>++emanote:+ # Whether to automatically treat folder notes as a folgezettel parent of its contents+ folder-folgezettel: true
@@ -0,0 +1,51 @@+<!DOCTYPE HTML>+<html lang="en">++<head>+ <meta charset="UTF-8">+ <meta name="viewport" content="width=device-width, initial-scale=1">+ <title>+ <ema:titleFull />+ </title>+ <ema:metadata>+ <with var="page">+ <meta property="og:description" content="${value:description}" />+ <meta property="og:site_name" content="${value:siteTitle}" />+ <meta property="og:image" content="${value:image}" />+ <meta property="og:type" content="website" />+ <meta property="og:title" content="${ema:title}" />+ </with>+ <with var="template">+ <base href="${value:baseUrl}" />+ <link href="${value:iconUrl}" rel="icon" />+ </with>+ <snippet var="page.headHtml" />+ </ema:metadata>+ <tailwindCssShim />++ <!-- Heist error element -->+ <style>+ strong.error {+ color: lightcoral;+ font-size: 90%;+ font-family: monospace;+ }+ </style>+ <apply template="/templates/hooks/more-head" />++ <head-main />+ <apply template="components/stork/stork-search-head" />+</head>++<!-- DoNotFormat -->+<bind tag="theme"><ema:metadata><value var="template.theme" /></ema:metadata></bind>+<bind tag="iconSize">w-4 h-4 flex-shrink-0</bind>+<bind tag="bodyClass"><ema:metadata><value var="template.layout.base.bodyClass" /></ema:metadata></bind>+<!-- DoNotFormat -->++<body class="${bodyClass}">+ <body-main />+ <apply template="components/stork/stork-search" />+</body>++</html>
@@ -0,0 +1,18 @@+<ema:note:backlinks:nodaily>+ <div class="flex-1 p-4 mt-8 bg-gray-100 rounded">+ <header class="mb-2 text-xl font-semibold text-gray-500">Links to this page</header>+ <ul class="space-y-1">+ <backlink>+ <li>+ <a class="text-${theme}-600 mavenLinkBold hover:bg-${theme}-50"+ href="${backlink:note:url}">+ <backlink:note:title />+ </a>+ <backlink:note:contexts>+ <apply template="context" />+ </backlink:note:contexts>+ </li>+ </backlink>+ </ul>+ </div>+</ema:note:backlinks:nodaily>
@@ -0,0 +1,47 @@+<nav id="breadcrumbs" class="w-full text-gray-700 md:hidden">+ <div class="flex justify-left">+ <div class="w-full px-2 py-2 bg-gray-50">+ <ul class="flex flex-wrap text-lg">+ <li class="inline-flex items-center">+ <ema:metadata>+ <with var="template">+ <img style="width: 1rem;" src="${value:iconUrl}" />+ </with>+ </ema:metadata>+ </li>+ <ema:breadcrumbs>+ <each-crumb>+ <li class="inline-flex items-center">+ <a class="px-1 font-bold" href="${crumb:url}">+ <crumb:title />+ </a>+ <svg fill="currentColor" viewBox="0 0 20 20" class="w-auto h-5 text-gray-400">+ <path fill-rule="evenodd"+ d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"+ clip-rule="evenodd"></path>+ </svg>+ </li>+ </each-crumb>+ </ema:breadcrumbs>+ </ul>+ </div>+ <button class="inline px-2 py-1 bg-gray-50 outline-none cursor-pointer focus:outline-none"+ title="Search (Ctrl+K)" type="button" onclick="window.emanote.stork.toggleSearch()">+ <apply template="stork/stork-icon" />+ </button>+ <button+ class="inline px-2 py-1 text-white bg-${theme}-600 outline-none cursor-pointer focus:outline-none"+ title="Toggle sidebar" type="button" onclick="toggleHidden('sidebar')">+ <svg xmlns="http://www.w3.org/2000/svg" class="w-4" fill="none" viewBox="0 0 24 24"+ stroke="currentColor">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M4 6h16M4 12h16M4 18h16" />+ </svg>+ </button>+ <script>+ function toggleHidden(elemId) {+ document.getElementById(elemId).classList.toggle("hidden");+ }+ </script>+ </div>+</nav>
@@ -0,0 +1,4 @@+<svg xmlns='http://www.w3.org/2000/svg' class='inline h-4 w-4 mb-1 text-gray-600 stroke-current fill-current' viewBox='0 0 28 28'><desc>☑</desc><rect stroke-width='3' x='2' y='2' width='24' height='24' rx='3'/><path stroke='white' fill='none' stroke-linejoin='round' stroke-width='3.5' d='M7 14 l6 5L22 8'/></svg>+<span>+ <apply-content />+</span>
@@ -0,0 +1,4 @@+<svg xmlns='http://www.w3.org/2000/svg' class='inline h-4 w-4 mb-1 text-gray-600 stroke-current' fill='none' viewBox='0 0 28 28'><desc>☐</desc><rect stroke-width='3' x='2' y='2' width='24' height='24' rx='3'/></svg>+<span>+ <apply-content />+</span>
@@ -0,0 +1,24 @@+<div class="mb-4 overflow-auto text-sm text-gray-500">+ <context>+ <div class="pl-2 mt-2 border-l-2 border-${theme}-200 hover:border-${theme}-500">+ <context:body>+ <PandocLink class="text-gray-600">+ <Internal class="font-bold hover:bg-gray-50" />+ <External class="hover:underline" target="_blank" rel="noopener" />+ </PandocLink>+ <OrderedList class="ml-4 space-y-1 list-decimal list-inside" />+ <BulletList class="ml-4 space-y-1 list-decimal list-inside" />+ <Task:Checked>+ <apply template="/templates/components/checkbox-checked">+ <inlines />+ </apply>+ </Task:Checked>+ <Task:Unchecked>+ <apply template="/templates/components/checkbox-unchecked">+ <inlines />+ </apply>+ </Task:Unchecked>+ </context:body>+ </div>+ </context>+</div>
@@ -0,0 +1,46 @@+<footer class="flex items-center justify-center mt-2 mb-8 space-x-4 text-center text-gray-800">+ <bind tag="iconClass">w-6 h-6 hover:text-${theme}-700</bind>+ <div>+ <a href="${ema:homeUrl}" title="Go to Home page">+ <svg xmlns="http://www.w3.org/2000/svg" class="${iconClass}" fill="none" viewBox="0 0 24 24"+ stroke="currentColor">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />+ </svg>+ </a>+ </div>+ <div>+ <a href="${ema:indexUrl}" title="View Index">+ <svg class="${iconClass}" fill="none" stroke="currentColor" viewBox="0 0 24 24"+ xmlns="http://www.w3.org/2000/svg">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4">+ </path>+ </svg>+ </a>+ </div>+ <div>+ <a href="https://emanote.srid.ca" target="_blank" title="Generated by Emanote ${ema:version}">+ <img class="${iconClass}" src="${ema:emanoteStaticLayerUrl}/emanote-logo.svg" />+ </a>+ </div>+ <div>+ <a href="${ema:tagIndexUrl}" title="View tags">+ <svg class="${iconClass}" fill="none" stroke="currentColor" viewBox="0 0 24 24"+ xmlns="http://www.w3.org/2000/svg">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z">+ </path>+ </svg>+ </a>+ </div>+ <div>+ <a href="${ema:taskIndexUrl}" title="View tasks">+ <svg xmlns="http://www.w3.org/2000/svg" class="${iconClass}" fill="none" viewBox="0 0 24 24"+ stroke="currentColor">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />+ </svg>+ </a>+ </div>+</footer>
@@ -0,0 +1,16 @@+<ema:metadata>+ <section+ class="flex flex-wrap items-end justify-center my-4 space-x-2 space-y-2 font-mono text-sm">+ <with var="tags">+ <!-- FIXME: The use of -/tags is wrong, because we should use routeUrl using Ema's encoder + Perhaps Emanote should inject tagMetas with urls.+ -->+ <a title="Tag" class="px-1 bg-gray-100 rounded hover:bg-gray-50 hover:text-${theme}-500"+ href="-/tags/${value}${ema:urlStrategySuffix}">+ <!-- DoNotFormat -->+ #<value />+ <!-- DoNotFormat -->+ </a>+ </with>+ </section>+</ema:metadata>
@@ -0,0 +1,5 @@+<article class="overflow-auto">+ <apply template="/templates/hooks/before-note" />+ <apply template="/templates/components/pandoc" />+ <apply template="/templates/hooks/after-note" />+</article>
@@ -0,0 +1,6 @@+<h1+ class="flex items-end justify-center mb-4 p-3 bg-${theme}-100 text-5xl font-extrabold text-black rounded">+ <a class="z-40 tracking-tighter ">+ <ema:note:title />+ </a>+</h1>
@@ -0,0 +1,16 @@+<li>++ <div class="${nodeClass} forest-link">+ <a href="${node:url}">+ <node:text />+ </a>+ </div>++ <tree:open>+ <ul>+ <children>+ <apply template="note-uptree-recurse" />+ </children>+ </ul>+ </tree:open>+</li>
@@ -0,0 +1,17 @@+<!-- DoNotFormat -->+<bind tag="nodeClass"><ema:metadata><value var="template.uptree.nodeClass" /></ema:metadata></bind>+<!-- DoNotFormat -->++<nav id="uptree" class="flipped tree" style="transform-origin: 50%;">+ <ul class="root">+ <li>+ <ema:note:uptree:nonempty>+ <ul>+ <ema:note:uptree>+ <apply template="note-uptree-recurse" />+ </ema:note:uptree>+ </ul>+ </ema:note:uptree:nonempty>+ </li>+ </ul>+</nav>
@@ -0,0 +1,108 @@+<ema:note:pandoc>+ <Para>+ <p class="mb-3">+ <inlines />+ </p>+ </Para>+ <Task:Checked>+ <!-- FIXME: Fix list styling to use flexbox, so task lists don't botch them up -->+ <apply template="/templates/components/checkbox-checked">+ <inlines />+ </apply>+ </Task:Checked>+ <Task:Unchecked>+ <apply template="/templates/components/checkbox-unchecked">+ <inlines />+ </apply>+ </Task:Unchecked>+ <Cite>+ <cite>+ <inlines />+ </cite>+ </Cite>+ <BlockQuote>+ <blockquote+ class="py-0.5 px-4 mb-3 italic border-l-4 bg-gray-50 text-gray-600 border-gray-400 quote">+ <blocks />+ </blockquote>+ </BlockQuote>+ <DefinitionList>+ <dl class="flex flex-col mb-3">+ <DefinitionList:Items>+ <div class="my-1">+ <dt class="font-bold text-l">+ <DefinitionList:Item:Term />+ </dt>+ <DefinitionList:Item:DescList>+ <div class="flex flex-col pl-1">+ <dd class="pl-2 my-1 text-gray-700 border-l-2">+ <DefinitionList:Item:Desc />+ </dd>+ </div>+ </DefinitionList:Item:DescList>+ </div>+ </DefinitionList:Items>+ </dl>+ </DefinitionList>+ <Note:Ref>+ <sup class="px-0.5">+ <a class="text-${theme}-600 hover:underline" href="${ema:note:url}#fn${footnote:idx}">+ <footnote:idx />+ </a>+ </sup>+ </Note:Ref>+ <Note:List>+ <div title="Footnotes"+ class="pt-2 mt-8 space-y-1 text-gray-500 transform scale-x-90 border-t-2">+ <header class="font-semibold">Footnotes</header>+ <footnote>+ <div id="fn${footnote:idx}">+ <header class="italic">+ <footnote:idx />.+ </header>+ <div class="inline-block mb-2 ml-4">+ <footnote:content />+ </div>+ </div>+ </footnote>+ </div>+ </Note:List>++ <BulletList>+ <ul class="my-3 ml-6 space-y-1 list-disc">+ <BulletList:Items>+ <li>+ <BulletList:Item />+ </li>+ </BulletList:Items>+ </ul>+ </BulletList>+ <OrderedList>+ <ul class="my-3 ml-6 space-y-1 list-decimal list-inside">+ <OrderedList:Items>+ <li>+ <OrderedList:Item />+ </li>+ </OrderedList:Items>+ </ul>+ </OrderedList>+ <HorizontalRule>+ <hr class="mb-3" />+ </HorizontalRule>+ <!-- TODO: Expand the above kind of overriding (full DOM control) to other AST nodes (below) -->+ <PandocLink class="text-${theme}-600">+ <Internal class="mavenLinkBold hover:underline" />+ <External class="hover:underline" target="_blank" rel="noopener" />+ </PandocLink>+ <CodeBlock class="py-0.5 mb-3 text-sm" />+ <Code class="py-0.5 px-0.5 bg-gray-100" />+ <Header>+ <h1 class="pb-2 mb-2 text-5xl font-bold text-center" />+ <h2 class="inline-block mt-6 mb-4 text-4xl font-bold text-gray-700 border-b-2" />+ <h3 class="mt-6 mb-2 text-3xl font-bold text-gray-700" />+ <h4 class="mt-6 mb-2 text-2xl font-bold text-gray-700" />+ <h5 class="mt-6 mb-2 text-xl font-bold text-gray-700" />+ <h6 class="mt-6 mb-2 text-xl font-bold text-gray-700" />+ </Header>++</ema:note:pandoc>
@@ -0,0 +1,80 @@+<!-- Variable bindings for this tree-->+<node:active>+ <bind tag="link-class">font-bold text-${theme}-600 hover:underline</bind>+ <else />+ <tree:open>+ <has-children>+ <bind tag="link-class">font-bold hover:underline</bind>+ <else />+ <bind tag="link-class">hover:underline</bind>+ </has-children>+ <else />+ <bind tag="link-class">hover:underline</bind>+ </tree:open>+</node:active>++<has-children>+ <bind tag="icon">+ <tree:open>+ <svg xmlns="http://www.w3.org/2000/svg" class="${iconSize} inline text-gray-700"+ viewBox="0 0 20 20" fill="currentColor">+ <path fill-rule="evenodd"+ d="M2 6a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1H8a3 3 0 00-3 3v1.5a1.5 1.5 0 01-3 0V6z"+ clip-rule="evenodd" />+ <path d="M6 12a2 2 0 012-2h8a2 2 0 012 2v2a2 2 0 01-2 2H2h2a2 2 0 002-2v-2z" />+ </svg>+ <else />+ <svg xmlns="http://www.w3.org/2000/svg" class="${iconSize} inline text-gray-500"+ viewBox="0 0 20 20" fill="currentColor">+ <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" />+ </svg>+ </tree:open>+ </bind>+ <else />+ <bind tag="icon">+ <node:active>+ <svg class="${iconSize} inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"+ xmlns="http://www.w3.org/2000/svg">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">+ </path>+ </svg>+ <else />+ <svg class="${iconSize} inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"+ xmlns="http://www.w3.org/2000/svg">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z">+ </path>+ </svg>+ </node:active>+ </bind>+</has-children>++<!-- Rendering of this tree -->+<div class="pl-2">+ <!-- Node's rootLabel-->+ <div class="flex items-center my-2 space-x-2 justify-left">+ <icon />+ <a class="${link-class} truncate" title="${node:text}" href="${node:url}">+ <node:text />+ </a>+ <tree:open>+ <else />+ <node:terminal>+ <else />+ <span class="text-gray-300" title="${tree:childrenCount} children inside">+ <tree:childrenCount />+ </span>+ </node:terminal>+ </tree:open>+ </div>++ <!-- Node's children forest, displayed only on active trees+ TODO: Use <details> to toggle visibility?+ -->+ <tree:open>+ <children>+ <apply template="sidebar-tree" />+ </children>+ </tree:open>+</div>
@@ -0,0 +1,5 @@+<svg xmlns="http://www.w3.org/2000/svg" style="width: 1rem;" class="hover:text-${theme}-700" f+ fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">+ <path stroke-linecap="round" stroke-linejoin="round"+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />+</svg>
@@ -0,0 +1,64 @@+<link rel="stylesheet" href="${ema:emanoteStaticLayerUrl}/stork/flat.css" />+<!-- Custom Stork-search styling for Emanote -->+<style>+ #stork-search-container {+ z-index: 1000;+ background-color: rgb(15 23 42/.8);+ }++ .stork-overflow-hidden-important {+ overflow: hidden !important;+ }+</style>+++<script src="${ema:emanoteStaticLayerUrl}/stork/stork.js"></script>+<ema:metadata>+ <with var="template">+ <script data-emanote-base-url="${value:baseUrl}">+ window.emanote = {};+ window.emanote.stork = {+ searchShown: false,+ toggleSearch: function () {+ document.getElementById('stork-search-container').classList.toggle('hidden');+ window.emanote.stork.searchShown = document.body.classList.toggle('stork-overflow-hidden-important');+ if (window.emanote.stork.searchShown) {+ document.getElementById('stork-search-input').focus();+ }+ },+ clearSearch: function () {+ document.getElementById('stork-search-container').classList.add('hidden');+ document.body.classList.remove('stork-overflow-hidden-important');+ window.emanote.stork.searchShown = false;+ },++ init: function () {+ const indexName = 'emanote-search'; // used to match input[data-stork] attribute value+ const baseUrl = document.currentScript.getAttribute('data-emanote-base-url') || '/';+ const indexUrl = baseUrl + '-/stork.st';+ if (document.readyState !== 'complete') {+ window.addEventListener('load', function () {+ stork.initialize(baseUrl + '_emanote-static/stork/stork.wasm');+ stork.register(indexName, indexUrl);+ });++ document.addEventListener('keydown', event => {+ if (window.emanote.stork.searchShown && event.key === 'Escape') {+ window.emanote.stork.clearSearch();+ event.preventDefault();+ } else if ((event.key == 'k' || event.key == 'K') && (event.ctrlKey || event.metaKey)) {+ window.emanote.stork.toggleSearch();+ event.preventDefault();+ }+ });+ } else {+ // Override existing on Ema's hot-reload+ stork.register(indexName, indexUrl, { forceOverwrite: true });+ }+ }+ };++ window.emanote.stork.init();+ </script>+ </with>+</ema:metadata>
@@ -0,0 +1,12 @@+<div id="stork-search-container"+ class="hidden fixed w-screen h-screen inset-0 backdrop-filter backdrop-blur-sm">+ <div class="fixed w-screen h-screen inset-0" onclick="window.emanote.stork.toggleSearch()"></div>++ <div class="container mx-auto p-10 mt-10">+ <div class="stork-wrapper-flat container mx-auto">+ <input id="stork-search-input" data-stork="emanote-search" class="stork-input"+ placeholder="Search (Ctrl+K) ..." />+ <div data-stork="emanote-search-output" class="stork-output"></div>+ </div>+ </div>+</div>
@@ -0,0 +1,19 @@+<ema:note:backlinks:daily>+ <div class="p-4 mt-8 bg-${theme}-50 rounded flex-1">+ <header class="mb-2 text-xl font-semibold text-gray-500"+ title="Links to this page from daily notes">🗓️ Timeline</header>+ <ul class="space-y-1">+ <backlink>+ <li>+ <a class="header-font font-light text-gray-700 hover:text-${theme}-500"+ href="${backlink:note:url}">+ <backlink:note:title />+ </a>+ <backlink:note:contexts>+ <apply template="context" />+ </backlink:note:contexts>+ </li>+ </backlink>+ </ul>+ </div>+</ema:note:backlinks:daily>
@@ -0,0 +1,39 @@+<!-- DoNotFormat -->+<bind tag="containerClass"><ema:metadata><value var="template.error.containerClass" /></ema:metadata></bind>+<!-- DoNotFormat -->++<apply template="base">+ <bind tag="body-main">+ <div class="${containerClass}">+ <div class="mt-2 md:mt-4">+ <div class="md:shadow-2xl md:mb-8">+ <div class="flex-1 w-full overflow-x-auto bg-white">+ <main class="px-4 py-4 bg-red-100">+ <h1+ class="flex items-end justify-center mb-4 p-3 bg-red-500 text-5xl font-extrabold text-gray-100 rounded">+ <a class="z-40 tracking-tighter ">+ <ema:note:title />+ </a>+ </h1>+ <div class="flex items-center justify-center font-bold text-lg">+ Your notebook has an issue.+ </div>+ <div class="bg-gray-50 p-2 my-4">+ <apply template="/templates/components/pandoc" />+ </div>+ <div class="flex items-center justify-center text-xl">+ <div>Fix it (this page will reload), or <a class="font-bold underline" href="/">go+ to /</a>.+ </div>+ </div>+ <div class="transform scale-95 opacity-50 hover:opacity-100">+ <apply template="components/timeline" />+ <apply template="components/backlinks" />+ </div>+ </main>+ </div>+ </div>+ </div>+ </div>+ </bind>+</apply>
@@ -0,0 +1,5 @@+<div class="flex justify-center items-center mb-2">+ <a target=_blank class="italic underline"+ href="https://github.com/srid/emanote/discussions/${dnum}">Experimental+ feature</a> !+</div>
@@ -0,0 +1,1 @@+<img class="inline mb-3" src="${ema:url}" alt="${ema:alt}" style="width: inherit;" />
@@ -0,0 +1,11 @@+<section title="Embedded note" class="p-4 mx-2 mb-2 bg-white border-2 rounded-lg shadow-inner">+ <header+ class="flex items-center justify-center text-2xl italic bg-${theme}-50 rounded py-1 px-2 mb-3">+ <a href="${ema:note:url}">+ <ema:note:title />+ </a>+ </header>+ <div>+ <apply template="/templates/components/pandoc" />+ </div>+</section>
@@ -0,0 +1,4 @@+<object class="mb-3 embedded-pdf w-full" type="application/pdf" data="${ema:url}" + style="height: 800px"> + <a href="${ema:url}">Open pdf</a> +</object>
@@ -0,0 +1,6 @@+<video class="mb-3" autoplay="" loop="" muted="">+ <source src="${ema:url}">+ Your browser doesn't support HTML5 video. Here is a <a class="text-${theme}-600 hover:underline"+ href="${ema:url}">link to the+ video</a> instead.+</video>
@@ -0,0 +1,23 @@+<nav>+ <div class="mb-8">+ <header data-nosnippet class="pb-2 mb-2 font-semibold text-gray-600">+ <query />+ </header>+ <ul>+ <result>+ <li>+ <div class="flex flex-wrap my-2">+ <ema:note:metadata>+ <span data-nosnippet class="mr-2 text-right text-gray-600">+ </span>+ </ema:note:metadata>+ <a class="flex-1 text-${theme}-600 mavenLinkBold border-l-2 pl-2 hover:underline"+ href="${ema:note:url}">+ <ema:note:title />+ </a>+ </div>+ </li>+ </result>+ </ul>+ </div>+</nav>
@@ -0,0 +1,25 @@+<!-- TODO: DRY (same as query-default.tpl, except for date column)-->+<nav>+ <div class="mb-8">+ <header data-nosnippet class="pb-2 mb-2 font-semibold text-gray-600">+ <query />+ </header>+ <ul>+ <result>+ <li>+ <div class="flex flex-wrap my-2">+ <ema:note:metadata>+ <span data-nosnippet class="mr-2 text-right text-gray-600">+ <value var="date" />+ </span>+ </ema:note:metadata>+ <a class="flex-1 text-${theme}-600 mavenLinkBold border-l-2 pl-2 hover:underline"+ href="${ema:note:url}">+ <ema:note:title />+ </a>+ </div>+ </li>+ </result>+ </ul>+ </div>+</nav>
@@ -0,0 +1,1 @@+<!-- What goes in this file will appear below the note body-->
@@ -0,0 +1,1 @@+<!-- What goes in this file will appear on top of note body-->
@@ -0,0 +1,49 @@+<!-- What goes in this file will appear on near the end of <head>-->+<link rel="preload"+ href="${ema:emanoteStaticLayerUrl}/fonts/Work_Sans/WorkSans-VariableFont_wght.ttf" as="font"+ type="font/ttf" crossorigin>++<style>+ @font-face {+ font-family: 'WorkSans';+ /* FIXME: This ought to be: ${ema:emanoteStaticLayerUrl}/fonts/Work_Sans/WorkSans-VariableFont_wght.ttf */+ src: url(_emanote-static/fonts/Work_Sans/WorkSans-VariableFont_wght.ttf) format("truetype");+ font-display: swap;+ }++ body {+ font-family: 'WorkSans', sans-serif;+ font-variation-settings: 'wght' 350;+ }++ a.mavenLinkBold {+ font-variation-settings: 'wght' 400;+ }++ strong {+ font-variation-settings: 'wght' 500;+ }++ h1,+ h2,+ h3,+ h4,+ h5,+ h6,+ header,+ .header-font {+ font-family: 'WorkSans', sans-serif;+ }++ h1 {+ font-variation-settings: 'wght' 500;+ }++ h2 {+ font-variation-settings: 'wght' 400;+ }++ h3 {+ font-variation-settings: 'wght' 300;+ }+</style>
@@ -0,0 +1,1 @@+<!-- What goes in this file will at the very end of the main div -->
@@ -0,0 +1,82 @@+<apply template="base">+ <bind tag="head-main"></bind>+ <bind tag="body-main">+ <div class="container mx-auto">++ <apply template="components/breadcrumbs" />++ <div id="container"+ class="flex flex-nowrap flex-col md:flex-row bg-gray-50 md:mt-8 md:shadow-2xl md:mb-8">+ <!-- Sidebar column -->+ <nav id="sidebar"+ class="flex-shrink hidden leading-relaxed md:block md:sticky md:top-0 md:h-full md:w-48 xl:w-64">+ <div class="px-2 py-2 text-gray-800">+ <div id="indexing-links" class="flex flex-row float-right p-2 space-x-2 text-gray-500">+ <a href="${ema:tagIndexUrl}" title="View tags">+ <svg style="width: 1rem;" class="hover:text-${theme}-700" fill="none"+ stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z">+ </path>+ </svg>+ </a>+ <a href="${ema:indexUrl}" title="Expand full tree">+ <svg style="width: 1rem;" class="hover:text-${theme}-700" fill="none"+ stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4">+ </path>+ </svg>+ </a>+ <a title="Search (Ctrl+K)" class="cursor-pointer"+ onclick="window.emanote.stork.toggleSearch()">+ <apply template="components/stork/stork-icon" />+ </a>+ </div>++ <div id="site-logo" class="pl-2">+ <div class="flex items-center my-2 space-x-2 justify-left">+ <a href="${ema:homeUrl}" title="Go to Home">+ <ema:metadata>+ <with var="template">+ <!-- The style width attribute here is to prevent huge+ icon from displaying at those rare occasions when Tailwind+ hasn't kicked in immediately on page load + -->+ <img style="width: 1rem;"+ class="transition transform hover:scale-110 hover:opacity-80"+ src="${value:iconUrl}" />+ </with>+ </ema:metadata>+ </a>+ <a class="font-bold truncate" title="Go to Home" href="${ema:homeUrl}">+ Home+ </a>+ </div>+ </div>++ <ema:route-tree>+ <apply template="components/sidebar-tree" />+ </ema:route-tree>++ </div>+ </nav>++ <!-- Main body column -->+ <div class="flex-1 w-full overflow-x-auto bg-white">+ <main class="px-4 py-4">+ <apply template="components/note-title" />+ <apply template="components/note-body" />+ <div class="flex flex-col lg:flex-row lg:space-x-2">+ <apply template="components/timeline" />+ <apply template="components/backlinks" />+ </div>+ <apply template="components/metadata" />+ <apply template="/templates/hooks/note-end" />+ </main>+ </div>+ </div>+ <apply template="components/footer" />+ </div>+ </bind>+</apply>
@@ -0,0 +1,41 @@+<!-- TODO: Main navigation links (Home, index, tags, ...) -->++<!-- DoNotFormat -->+<bind tag="containerClass"><ema:metadata><value var="template.layout.note.containerClass" /></ema:metadata></bind>+<!-- DoNotFormat -->++<apply template="base">+ <bind tag="head-main">+ <link rel="stylesheet" href="${ema:emanoteStaticLayerUrl}/inverted-tree.css" />+ </bind>+ <bind tag="body-main">+ <div class="${containerClass}">+ <div class="mt-2 md:mt-4">+ <apply template="components/note-uptree" />++ <div class="relative md:shadow-2xl md:mb-8">++ <div class="absolute -top-6 right-1 md:right-0 flex flex-row items-center justify-center">+ <a title="Search (Ctrl+K)" class="cursor-pointer"+ onclick="window.emanote.stork.toggleSearch()">+ <apply template="components/stork/stork-icon" />+ </a>+ </div>+ <div class="flex-1 w-full overflow-x-auto bg-white">+ <main class="px-4 py-4">+ <apply template="components/note-title" />+ <apply template="components/note-body" />+ <div class="flex flex-col lg:flex-row lg:space-x-2">+ <apply template="components/timeline" />+ <apply template="components/backlinks" />+ </div>+ <apply template="components/metadata" />+ <apply template="/templates/hooks/note-end" />+ </main>+ </div>+ </div>+ <apply template="components/footer" />+ </div>+ </div>+ </bind>+</apply>
@@ -0,0 +1,18 @@+<apply template="/templates/base">+ <bind tag="body-main">+ <div class="container mx-auto ">+ <main class="flex-col items-center justify-center mx-2">+ <h1 class="pb-2 mt-2 mb-2 text-6xl text-center">+ <ema:title />+ </h1>+ <apply template="experimental">+ <bind tag="dnum">50</bind>+ </apply>+ <div class="pb-2 mx-auto my-4 lg:max-w-screen-md ">+ <special-main />+ </div>+ </main>+ <apply template="components/footer" />+ </div>+ </bind>+</apply>
@@ -0,0 +1,9 @@+<apply template="/templates/special/base">+ <bind tag="special-main">+ <div class="pt-1 pb-2 pl-4 bg-gray-200">+ <ema:route-tree>+ <apply template="components/sidebar-tree" />+ </ema:route-tree>+ </div>+ </bind>+</apply>
@@ -0,0 +1,75 @@+<apply template="/templates/special/base">+ <bind tag="special-main">+ <div class="bg-gray-200 pb-2">+ <nav id="tagcrumbs" class="w-full pl-2 bg-gray-100">+ <div class="flex items-center justify-left">+ <div class="w-full px-2 py-2 ">+ <ul class="flex flex-wrap text-lg">+ <ema:tagcrumbs>+ <ema:each-crumb>+ <li class="inline-flex items-center">+ <a class="px-1 font-mono font-semibold text-${theme}-600 "+ href="${ema:tagcrumb:url}">+ <ema:tagcrumb:title />+ </a>+ <svg fill="currentColor" viewBox="0 0 20 20" class="w-auto h-5 ">+ <path fill-rule="evenodd"+ d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"+ clip-rule="evenodd"></path>+ </svg>+ </li>+ </ema:each-crumb>+ </ema:tagcrumbs>+ <li class="inline-flex items-center text-black">+ <a class="px-1 font-mono">+ <ema:tag:title />+ </a>+ </li>+ </ul>+ </div>+ </div>+ </nav>+ <div class="flex flex-col pl-2 my-4 ml-2 space-y-2">+ <ema:childTags>+ <ema:each-childTag>+ <div>+ <svg class="${iconSize} inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"+ xmlns="http://www.w3.org/2000/svg">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z">+ </path>+ </svg>+ <span class="">+ <a href="${ema:childTag:url}" class="font-mono font-bold hover:text-${theme}-700">+ <ema:childTag:title />+ </a>+ <span class="font-mono text-sm text-gray-400">+ <!-- DoNotFormat -->+ (<ema:childTag:count-tag />+<ema:childTag:count-note />)+ <!-- DoNotFormat -->+ </span>+ </span>+ </div>+ </ema:each-childTag>+ </ema:childTags>+ <ema:notes>+ <ema:each-note>+ <div>+ <svg class="${iconSize} inline" fill="none" stroke="currentColor" viewBox="0 0 24 24"+ xmlns="http://www.w3.org/2000/svg">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"+ d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z">+ </path>+ </svg>++ <!-- TODO: DRY -->+ <a class="text-${theme}-600 hover:underline truncate" href="${ema:note:url}">+ <ema:note:title />+ </a>+ </div>+ </ema:each-note>+ </ema:notes>+ </div>+ </div>+ </bind>+</apply>
@@ -0,0 +1,51 @@+<apply template="/templates/special/base">+ <bind tag="special-main">+ <div class="w-full bg-gray-300">+ <div class="w-full px-3 py-1">+ <ema:taskGroups>+ <taskGroup>+ <section class="mt-4 mb-6">+ <div class="mt-1 mb-2 py-0.5 px-1 rounded flex flex-row-reverse">+ <t:note:breadcrumbs>+ <each-crumb>+ <li class="inline-flex items-center">+ <a class="text-gray-900 opacity-50 hover:opacity-100" href="${crumb:url}">+ <crumb:title />+ </a>+ <span class="text-gray-700 px-2">\</span>+ </li>+ </each-crumb>+ </t:note:breadcrumbs>++ <div class="flex-1">+ <a class="font-bold text-${theme}-700 hover:underline" href="${t:note:url}">+ <t:note:title />+ </a>+ </div>+ </div>+ <div class="">+ <t:tasks>+ <task>+ <div+ class="bg-white py-1.5 px-2 my-1 rounded shadow border-2 hover:border-${theme}-600">+ <apply template="/templates/components/checkbox-unchecked">+ <task:description>+ <PandocLink class="text-gray-600">+ <Internal class="font-bold hover:bg-gray-50" />+ <External class="hover:underline" target="_blank" rel="noopener" />+ </PandocLink>+ <OrderedList class="ml-4 space-y-1 list-decimal list-inside" />+ <BulletList class="ml-4 space-y-1 list-decimal list-inside" />+ </task:description>+ </apply>+ </div>+ </task>+ </t:tasks>+ </div>+ </section>+ </taskGroup>+ </ema:taskGroups>+ </div>+ </div>+ </bind>+</apply>
@@ -0,0 +1,214 @@+cabal-version: 2.4+name: emanote+version: 0.8.0.0+license: AGPL-3.0-only+copyright: 2022 Sridhar Ratnakumar+maintainer: srid@srid.ca+author: Sridhar Ratnakumar+category: Web+synopsis: Emanate a structured view of your plain-text notes+description:+ Create beautiful websites -- such as personal webpage, blog, wiki, Zettelkasten, notebook, knowledge-base, documentation, etc. from future-proof plain-text notes and arbitrary data -- with live preview that updates in real-time.++-- A URL where users can report bugs.+bug-reports: https://github.com/EmaApps/emanote/issues+extra-source-files:+ LICENSE+ README.md++data-dir: default+data-files:+ _emanote-bin/compile-css+ _emanote-live-server/**/*.css+ _emanote-static/**/*.ttf+ _emanote-static/*.css+ _emanote-static/*.svg+ _emanote-static/stork/flat.css+ _emanote-static/stork/stork.js+ _emanote-static/stork/stork.wasm+ favicon.svg+ index.md+ index.yaml+ templates/**/*.tpl++common haskell-common+ ghc-options:+ -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns+ -Wmissing-deriving-strategies -Wunused-foralls -Wunused-foralls+ -fprint-explicit-foralls -fprint-explicit-kinds++ default-extensions:+ NoImplicitPrelude+ NoStarIsType+ BangPatterns+ ConstraintKinds+ DataKinds+ DeriveDataTypeable+ DeriveFoldable+ DeriveFunctor+ DeriveGeneric+ DeriveLift+ DeriveTraversable+ DerivingStrategies+ DerivingVia+ EmptyCase+ EmptyDataDecls+ EmptyDataDeriving+ ExistentialQuantification+ ExplicitForAll+ FlexibleContexts+ FlexibleInstances+ GADTSyntax+ GeneralisedNewtypeDeriving+ ImportQualifiedPost+ KindSignatures+ LambdaCase+ MultiParamTypeClasses+ MultiWayIf+ NumericUnderscores+ OverloadedStrings+ PolyKinds+ PostfixOperators+ RankNTypes+ ScopedTypeVariables+ StandaloneDeriving+ StandaloneKindSignatures+ TupleSections+ TypeApplications+ TypeFamilies+ TypeOperators+ ViewPatterns++common library-common+ import: haskell-common+ default-language: Haskell2010++ -- TODO: We could use the ghcid flag trick in neuron.cabal to avoid rebuilds.+ hs-source-dirs: src+ build-depends:+ , aeson+ , aeson-extra+ , aeson-optics+ , async+ , base >=4.14 && <5+ , blaze-html+ , bytestring+ , commonmark+ , commonmark-extensions+ , commonmark-pandoc+ , commonmark-simple+ , containers+ , data-default+ , dependent-sum+ , directory+ , ema >=0.8.2+ , filepath+ , filepattern+ , fsnotify+ , hedgehog+ , heist-emanote >=1.2.1+ , heist-extra+ , hspec+ , hspec-hedgehog+ , ixset-typed >=0.5.1.0+ , lvar+ , map-syntax+ , megaparsec+ , monad-logger+ , monad-logger-extras+ , mtl+ , neat-interpolation+ , optics-core+ , optics-th+ , optparse-applicative+ , pandoc+ , pandoc-link-context >=1.4.0+ , pandoc-types+ , parsec+ , path-tree >=0.2+ , process-extras+ , profunctors+ , relude >=1.0+ , shower+ , some+ , stm+ , tagged+ , tagtree+ , tailwind >=0.3+ , text+ , time+ , tomland+ , unionmount >=0.2+ , unliftio+ , unordered-containers+ , uri-encode+ , url-slug+ , uuid+ , which+ , with-utf8+ , xmlhtml+ , yaml++ hs-source-dirs: src++library+ import: library-common+ exposed-modules:+ Emanote+ Emanote.CLI+ Emanote.Model+ Emanote.Model.Calendar+ Emanote.Model.Graph+ Emanote.Model.Link.Rel+ Emanote.Model.Link.RelSpec+ Emanote.Model.Link.Resolve+ Emanote.Model.Meta+ Emanote.Model.Note+ Emanote.Model.Note.Filter+ Emanote.Model.Query+ Emanote.Model.QuerySpec+ Emanote.Model.SData+ Emanote.Model.StaticFile+ Emanote.Model.Stork+ Emanote.Model.Stork.Index+ Emanote.Model.Task+ Emanote.Model.Title+ Emanote.Model.Type+ Emanote.Pandoc.BuiltinFilters+ Emanote.Pandoc.Link+ Emanote.Pandoc.Markdown.Parser+ Emanote.Pandoc.Markdown.Syntax.HashTag+ Emanote.Pandoc.Markdown.Syntax.Highlight+ Emanote.Pandoc.Markdown.Syntax.WikiLink+ Emanote.Pandoc.Renderer+ Emanote.Pandoc.Renderer.Embed+ Emanote.Pandoc.Renderer.Query+ Emanote.Pandoc.Renderer.Url+ Emanote.Prelude+ Emanote.Route+ Emanote.Route.Ext+ Emanote.Route.ModelRoute+ Emanote.Route.R+ Emanote.Route.SiteRoute+ Emanote.Route.SiteRoute.Class+ Emanote.Route.SiteRoute.Type+ Emanote.Source.Dynamic+ Emanote.Source.Loc+ Emanote.Source.Patch+ Emanote.Source.Pattern+ Emanote.View+ Emanote.View.Common+ Emanote.View.Export+ Emanote.View.LiveServerFiles+ Emanote.View.TagIndex+ Emanote.View.TaskIndex+ Emanote.View.Template+ Paths_emanote+ Spec++ autogen-modules: Paths_emanote++executable emanote+ import: library-common+ main-is: Main.hs+ ghc-options: -threaded -rtsopts -with-rtsopts=-N
@@ -0,0 +1,160 @@+{-# LANGUAGE RecordWildCards #-}+{-# OPTIONS_GHC -Wno-orphans #-}++module Emanote+ ( run,+ defaultEmanoteConfig,+ )+where++import Control.Monad.Logger (LogLevel (LevelError), runStderrLoggingT, runStdoutLoggingT)+import Control.Monad.Logger.Extras (Logger (Logger), logToStderr, runLoggerLoggingT)+import Control.Monad.Writer.Strict (MonadWriter (tell), WriterT (runWriterT))+import Data.Default (def)+import Data.Dependent.Sum (DSum ((:=>)))+import Data.Map.Strict qualified as Map+import Ema+ ( EmaSite (..),+ IsRoute (..),+ fromPrism_,+ runSiteWithCli,+ toPrism_,+ )+import Ema.CLI qualified+import Ema.Dynamic (Dynamic (Dynamic))+import Emanote.CLI qualified as CLI+import Emanote.Model.Link.Rel (ResolvedRelTarget (..))+import Emanote.Model.Type (modelCompileTailwind)+import Emanote.Model.Type qualified as Model+import Emanote.Pandoc.Renderer+import Emanote.Pandoc.Renderer.Embed qualified as PF+import Emanote.Pandoc.Renderer.Query qualified as PF+import Emanote.Pandoc.Renderer.Url qualified as PF+import Emanote.Prelude (log, logE, logW)+import Emanote.Route.ModelRoute (LMLRoute, lmlRouteCase)+import Emanote.Route.SiteRoute.Class (emanoteGeneratableRoutes, emanoteRouteEncoder)+import Emanote.Route.SiteRoute.Type (SiteRoute)+import Emanote.Source.Dynamic (EmanoteConfig (..), emanoteSiteInput)+import Emanote.View.Common (generatedCssFile)+import Emanote.View.Export qualified as Export+import Emanote.View.Template qualified as View+import Optics.Core ((%), (.~), (^.))+import Relude+import System.FilePath ((</>))+import UnliftIO (MonadUnliftIO)+import Web.Tailwind qualified as Tailwind++instance IsRoute SiteRoute where+ type RouteModel SiteRoute = Model.ModelEma+ routePrism = toPrism_ . emanoteRouteEncoder+ routeUniverse = emanoteGeneratableRoutes++instance EmaSite SiteRoute where+ type SiteArg SiteRoute = EmanoteConfig+ siteInput = emanoteSiteInput+ siteOutput = View.emanoteSiteOutput++defaultEmanoteConfig :: CLI.Cli -> EmanoteConfig+defaultEmanoteConfig cli =+ EmanoteConfig cli id defaultEmanotePandocRenderers False++run :: EmanoteConfig -> IO ()+run cfg@EmanoteConfig {..} = do+ case CLI.cmd _emanoteConfigCli of+ CLI.Cmd_Ema emaCli ->+ Ema.runSiteWithCli @SiteRoute emaCli cfg+ >>= postRun cfg+ CLI.Cmd_Export -> do+ Dynamic (unModelEma -> model0, _) <-+ flip runLoggerLoggingT oneOffLogger $+ siteInput @SiteRoute (Ema.CLI.action def) cfg+ putLBSLn $ Export.renderGraphExport model0+ where+ -- A logger suited for running one-off commands.+ oneOffLogger =+ logToStderr+ & allowLogLevelFrom LevelError+ where+ allowLogLevelFrom :: LogLevel -> Logger -> Logger+ allowLogLevelFrom minLevel (Logger f) = Logger $ \loc src level msg ->+ if level >= minLevel+ then f loc src level msg+ else pass++postRun :: EmanoteConfig -> (Model.ModelEma, DSum Ema.CLI.Action Identity) -> IO ()+postRun EmanoteConfig {..} = \case+ (unModelEma -> model0, Ema.CLI.Generate outPath :=> Identity genPaths) -> do+ when (model0 ^. modelCompileTailwind) $+ compileTailwindCss (outPath </> generatedCssFile) genPaths+ checkBrokenLinks _emanoteConfigCli $ Export.modelRels model0+ checkBadMarkdownFiles $ Model.modelNoteErrors model0+ _ ->+ pass++unModelEma :: Model.ModelEma -> Model.Model+unModelEma m = Model.withRoutePrism (fromPrism_ $ routePrism @SiteRoute m) m++checkBadMarkdownFiles :: Map LMLRoute [Text] -> IO ()+checkBadMarkdownFiles noteErrs = runStderrLoggingT $ do+ forM_ (Map.toList noteErrs) $ \(noteRoute, errs) -> do+ logW $ "Bad markdown file: " <> show noteRoute+ forM_ errs $ \err -> do+ logE $ " - " <> err+ unless (null noteErrs) $ do+ logE "Errors found."+ exitFailure++checkBrokenLinks :: CLI.Cli -> Map LMLRoute [Export.Link] -> IO ()+checkBrokenLinks cli modelRels = runStderrLoggingT $ do+ ((), res :: Sum Int) <- runWriterT $+ forM_ (Map.toList modelRels) $ \(noteRoute, rels) ->+ forM_ (sortNub rels) $ \(Export.Link urt rrt) ->+ case rrt of+ RRTFound _ -> pass+ RRTMissing -> do+ logW $ "Broken link: " <> show (lmlRouteCase noteRoute) <> " -> " <> show urt+ tell 1+ RRTAmbiguous ls -> do+ logW $ "Ambiguous link: " <> show (lmlRouteCase noteRoute) <> " -> " <> show urt <> " ambiguities: " <> show ls+ tell 1+ if res == 0+ then do+ log "No broken links detected."+ else unless (CLI.allowBrokenLinks cli) $ do+ logE $ "Found " <> show (getSum res) <> " broken links! Emanote generated the site, but the generated site has broken links."+ log "(Tip: use `--allow-broken-links` to ignore this check.)"+ exitFailure++compileTailwindCss :: MonadUnliftIO m => FilePath -> [FilePath] -> m ()+compileTailwindCss cssPath genPaths = do+ runStdoutLoggingT $ do+ log $ "Running Tailwind CSS v3 compiler to generate: " <> toText cssPath+ Tailwind.runTailwind $+ def+ & Tailwind.tailwindConfig % Tailwind.tailwindConfigContent .~ genPaths+ & Tailwind.tailwindOutput .~ cssPath+ & Tailwind.tailwindMode .~ Tailwind.Production++defaultEmanotePandocRenderers :: EmanotePandocRenderers Model.Model LMLRoute+defaultEmanotePandocRenderers =+ let blockRenderers =+ PandocRenderers+ [ PF.embedInlineWikiLinkResolvingSplice, -- embedInlineWikiLinkResolvingSplice should be first to recognize inline Link elements first+ PF.urlResolvingSplice+ ]+ [ PF.embedBlockWikiLinkResolvingSplice,+ PF.embedBlockRegularLinkResolvingSplice,+ PF.queryResolvingSplice+ ]+ inlineRenderers =+ PandocRenderers+ [ PF.embedInlineWikiLinkResolvingSplice, -- embedInlineWikiLinkResolvingSplice should be first to recognize inline Link elements first+ PF.urlResolvingSplice+ ]+ mempty+ linkInlineRenderers =+ PandocRenderers+ [ PF.plainifyWikiLinkSplice+ ]+ mempty+ in EmanotePandocRenderers {..}
@@ -0,0 +1,71 @@+{-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE RecordWildCards #-}++module Emanote.CLI+ ( Cli (..),+ Cmd (..),+ parseCli,+ cliParser,+ )+where++import Data.Text qualified as T+import Data.Version (showVersion)+import Ema.CLI qualified+import Options.Applicative hiding (action)+import Paths_emanote qualified+import Relude+import UnliftIO.Directory (getCurrentDirectory)++data Cli = Cli+ { layers :: NonEmpty FilePath,+ test :: Bool,+ allowBrokenLinks :: Bool,+ cmd :: Cmd+ }++data Cmd+ = Cmd_Ema Ema.CLI.Cli+ | Cmd_Export++cliParser :: FilePath -> Parser Cli+cliParser cwd = do+ layers <- pathList (one cwd)+ test <- switch (long "test" <> help "Run tests")+ allowBrokenLinks <- switch (long "allow-broken-links" <> help "Report but do not fail on broken links")+ cmd <-+ fmap Cmd_Ema Ema.CLI.cliParser+ <|> subparser (command "export" (info (pure Cmd_Export) (progDesc "Export metadata JSON")))+ pure Cli {..}+ where+ pathList defaultPath = do+ option pathListReader $+ mconcat+ [ long "layers",+ short 'L',+ metavar "LAYERS",+ value defaultPath,+ help "List of (semicolon delimited) notebook folders to 'union mount', with the left-side folders being overlaid on top of the right-side ones. The default layer is implicitly included at the end of this list."+ ]+ pathListReader :: ReadM (NonEmpty FilePath)+ pathListReader =+ maybeReader $ \paths ->+ nonEmpty $ fmap toString $ T.split (== ';') . toText $ paths++parseCli' :: FilePath -> ParserInfo Cli+parseCli' cwd =+ info+ (versionOption <*> cliParser cwd <**> helper)+ ( fullDesc+ <> progDesc "Emanote - A spiritual successor to Neuron"+ <> header "Emanote"+ )+ where+ versionOption =+ infoOption+ (showVersion Paths_emanote.version)+ (long "version" <> help "Show version")++parseCli :: IO Cli+parseCli =+ execParser . parseCli' =<< getCurrentDirectory
@@ -0,0 +1,3 @@+module Emanote.Model (module X) where++import Emanote.Model.Type as X
@@ -0,0 +1,46 @@+-- | A primitive module to eventually pave way towards first-class "calendar"+-- (daily notes, etc.) support in Emanote; either built-in or as plugin.+module Emanote.Model.Calendar where++import Data.Time.Calendar (Day, fromGregorianValid)+import Emanote.Model.Note qualified as N+import Emanote.Model.Title (Title)+import Emanote.Model.Type (Model, modelLookupTitle)+import Emanote.Route (LMLRoute)+import Emanote.Route qualified as R+import Relude+import Text.Megaparsec qualified as M+import Text.Megaparsec.Char qualified as M++-- HACK: This is so that calendar backlinks are sorted properly.+backlinkSortKey :: Model -> LMLRoute -> Down Title+backlinkSortKey model =+ Down . flip modelLookupTitle model++-- HACK: Until we have a proper search support. This sorts query results for+-- timeline+noteSortKey :: N.Note -> (Down (Maybe Text), LMLRoute)+noteSortKey note =+ (Down $ N.lookupMeta @Text (one "date") note, N._noteRoute note)++isDailyNote :: LMLRoute -> Bool+isDailyNote =+ isJust . parseRouteDay++parseRouteDay :: LMLRoute -> Maybe Day+parseRouteDay =+ M.parseMaybe parse . R.withLmlRoute R.routeBaseName+ where+ parse :: M.Parsec Void Text Day+ parse = do+ let asInt = maybe (fail "Not an int") pure . readMaybe+ -- Year+ year <- asInt =<< replicateM 4 M.digitChar+ void $ M.string "-"+ -- Month+ month <- asInt =<< replicateM 2 M.digitChar+ void $ M.string "-"+ -- Day+ day <- asInt =<< replicateM 2 M.digitChar+ maybe (fail "Not a date") pure $+ fromGregorianValid year (fromInteger month) (fromInteger day)
@@ -0,0 +1,115 @@+module Emanote.Model.Graph where++import Data.IxSet.Typed ((@+), (@=))+import Data.IxSet.Typed qualified as Ix+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Tree (Forest, Tree (Node))+import Emanote.Model.Calendar qualified as Calendar+import Emanote.Model.Link.Rel qualified as Rel+import Emanote.Model.Link.Resolve qualified as Resolve+import Emanote.Model.Meta (lookupRouteMeta)+import Emanote.Model.Note qualified as MN+import Emanote.Model.Type (Model, modelRels, resolveLmlRoute)+import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Emanote.Route qualified as R+import Emanote.Route.ModelRoute (ModelRoute)+import Optics.Operators as Lens ((^.))+import Relude hiding (empty)+import Text.Pandoc.Definition qualified as B++-- TODO: Do breadth-first instead of depth-first+modelFolgezettelAncestorTree :: ModelRoute -> Model -> Forest R.LMLRoute+modelFolgezettelAncestorTree r0 model =+ fst $ usingState mempty $ go r0+ where+ go :: MonadState (Set ModelRoute) m => ModelRoute -> m (Forest R.LMLRoute)+ go r = do+ let folgezettelBacklinks =+ backlinkRels r model+ & filter (isFolgezettel . (^. Rel.relTo))+ <&> (^. Rel.relFrom)+ -- Handle reverse folgezettel links here+ folgezettelFrontlinks =+ frontlinkRels r model+ & mapMaybe (lookupWikiLink <=< selectReverseFolgezettel . (^. Rel.relTo))+ -- Folders are automatically made a folgezettel+ folgezettelFolder =+ maybeToList $ do+ lmlR <- leftToMaybe (R.modelRouteCase r)+ guard $ lookupRouteMeta True ("emanote" :| ["folder-folgezettel"]) lmlR model+ parentLmlRoute model lmlR+ folgezettelParents =+ mconcat+ [ folgezettelBacklinks,+ folgezettelFrontlinks,+ folgezettelFolder+ ]+ fmap catMaybes . forM folgezettelParents $ \parentR -> do+ let parentModelR = R.ModelRoute_LML parentR+ gets (parentModelR `Set.member`) >>= \case+ True -> pure Nothing+ False -> do+ modify $ Set.insert parentModelR+ sub <- go parentModelR+ pure $ Just $ Node parentR sub+ isFolgezettel = \case+ Rel.URTWikiLink (WL.WikiLinkBranch, _wl) ->+ True+ _ ->+ False+ selectReverseFolgezettel :: Rel.UnresolvedRelTarget -> Maybe WL.WikiLink+ selectReverseFolgezettel = \case+ Rel.URTWikiLink (WL.WikiLinkTag, wl) -> Just wl+ _ -> Nothing+ lookupWikiLink :: WL.WikiLink -> Maybe R.LMLRoute+ lookupWikiLink wl = do+ note <- leftToMaybe <=< getFound $ Resolve.resolveWikiLinkMustExist model wl+ pure $ note ^. MN.noteRoute+ getFound :: Rel.ResolvedRelTarget a -> Maybe a+ getFound = \case+ Rel.RRTFound x -> Just x+ _ -> Nothing++-- | Return the route to parent folder (unless indexRoute is passed).+--+-- This will return the existing note (.org or .md) if possible. Otherwise+-- fallback to .md even if missing.+parentLmlRoute :: Model -> R.LMLRoute -> Maybe R.LMLRoute+parentLmlRoute model r = do+ pr <- do+ let lmlR = R.lmlRouteCase r+ -- Root index do not have a parent folder.+ guard $ lmlR /= Left R.indexRoute && lmlR /= Right R.indexRoute+ -- Consider the index route as parent folder for all+ -- top-level notes.+ pure $ fromMaybe R.indexRoute $ R.withLmlRoute R.routeParent r+ pure $ resolveLmlRoute model . coerce $ pr++modelLookupBacklinks :: ModelRoute -> Model -> [(R.LMLRoute, NonEmpty [B.Block])]+modelLookupBacklinks r model =+ sortOn (Calendar.backlinkSortKey model . fst) $+ groupNE $+ backlinkRels r model <&> \rel ->+ (rel ^. Rel.relFrom, rel ^. Rel.relCtx)+ where+ groupNE :: forall a b. Ord a => [(a, b)] -> [(a, NonEmpty b)]+ groupNE =+ Map.toList . foldl' f Map.empty+ where+ f :: Map a (NonEmpty b) -> (a, b) -> Map a (NonEmpty b)+ f m (x, y) =+ case Map.lookup x m of+ Nothing -> Map.insert x (one y) m+ Just ys -> Map.insert x (ys <> one y) m++backlinkRels :: ModelRoute -> Model -> [Rel.Rel]+backlinkRels r model =+ let allPossibleLinks = Rel.unresolvedRelsTo r+ in Ix.toList $ (model ^. modelRels) @+ allPossibleLinks++frontlinkRels :: ModelRoute -> Model -> [Rel.Rel]+frontlinkRels r model =+ maybeToMonoid $ do+ lmlR <- leftToMaybe $ R.modelRouteCase r+ pure $ Ix.toList $ (model ^. modelRels) @= lmlR
@@ -0,0 +1,144 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE TemplateHaskell #-}++module Emanote.Model.Link.Rel where++import Data.Aeson (ToJSON)+import Data.IxSet.Typed (Indexable (..), IxSet, ixFun, ixList)+import Data.IxSet.Typed qualified as Ix+import Data.List.NonEmpty qualified as NEL+import Data.Map.Strict qualified as Map+import Data.Text qualified as T+import Emanote.Model.Note (Note, noteDoc, noteRoute)+import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Emanote.Route (LMLRoute, ModelRoute)+import Emanote.Route qualified as R+import Emanote.Route.SiteRoute.Type qualified as SR+import Optics.Operators as Lens ((^.))+import Optics.TH (makeLenses)+import Relude+import System.FilePath (normalise, (</>))+import Text.Pandoc.Definition qualified as B+import Text.Pandoc.LinkContext qualified as LC++-- | A relation from one note to anywhere in the model.+--+-- Target will remain unresolved in the `Rel`, and can be resolved at a latter+-- time (eg: during rendering).+data Rel = Rel+ { -- The note containing this relation+ _relFrom :: LMLRoute,+ -- The target of the relation (can be a note or anything)+ _relTo :: UnresolvedRelTarget,+ -- | The relation context in LML+ _relCtx :: [B.Block]+ }+ deriving stock (Eq, Ord, Show)++-- | A link target that has not been resolved (using model) yet.+--+-- Resolving this may or may not result in a resource in the model. The ADT+-- constructors capture the different possible types of links the user is+-- allowed to link to.+data UnresolvedRelTarget+ = URTWikiLink (WL.WikiLinkType, WL.WikiLink)+ | URTResource ModelRoute+ | URTVirtual SR.VirtualRoute+ deriving stock (Eq, Show, Ord, Generic)+ deriving anyclass (ToJSON)++type RelIxs = '[LMLRoute, UnresolvedRelTarget]++type IxRel = IxSet RelIxs Rel++instance Indexable RelIxs Rel where+ indices =+ ixList+ (ixFun $ one . _relFrom)+ (ixFun $ one . _relTo)++makeLenses ''Rel++noteRels :: Note -> IxRel+noteRels note =+ extractLinks . LC.queryLinksWithContext $ note ^. noteDoc+ where+ extractLinks :: Map Text (NonEmpty ([(Text, Text)], [B.Block])) -> IxRel+ extractLinks m =+ Ix.fromList $+ flip concatMap (Map.toList m) $ \(url, instances) -> do+ flip mapMaybe (toList instances) $ \(attrs, ctx) -> do+ let parentR = R.withLmlRoute R.routeParent $ note ^. noteRoute+ (target, _manchor) <- parseUnresolvedRelTarget parentR attrs url+ pure $ Rel (note ^. noteRoute) target ctx++unresolvedRelsTo :: ModelRoute -> [UnresolvedRelTarget]+unresolvedRelsTo r =+ let wls = either (R.withLmlRoute WL.allowedWikiLinks) WL.allowedWikiLinks $ R.modelRouteCase r+ in (URTWikiLink <$> toList wls)+ <> [URTResource r]++-- | Parse a relative URL string for later resolution.+--+-- TODO: Need tests for this function.+parseUnresolvedRelTarget :: Maybe (R.R 'R.Folder) -> [(Text, Text)] -> Text -> Maybe (UnresolvedRelTarget, Maybe WL.Anchor)+parseUnresolvedRelTarget baseDir attrs url = do+ (wlRes, manchor) <- WL.delineateLink attrs url+ res <- case wlRes of+ Left wl ->+ pure $ URTWikiLink wl+ Right fp ->+ fmap URTVirtual (SR.decodeVirtualRoute fp)+ <|> fmap+ URTResource+ ( fp+ & relocateRelUrlUnder (R.encodeRoute <$> baseDir)+ & R.mkModelRouteFromFilePath+ )+ pure (res, manchor)++relocateRelUrlUnder :: Maybe FilePath -> FilePath -> FilePath+relocateRelUrlUnder mbase fp =+ normalizeIgnoringSymlinks $+ case mbase of+ Nothing -> fp+ Just x -> x </> fp++-- | Like `System.FilePath.normalise` but also normalises '..'+normalizeIgnoringSymlinks :: FilePath -> FilePath+normalizeIgnoringSymlinks = dropDotDot . normalise++-- Remove '..' from path component.+--+-- `System.FilePath.normalize` ought to do this already, but it doesn't due to+-- symlinks (which we don't use anyway.)+--+-- See https://github.com/haskell/filepath/issues/87+dropDotDot :: FilePath -> FilePath+dropDotDot =+ let go :: Int -> NonEmpty Text -> [Text]+ go n = \case+ (".." :| xs) -> maybe [] (go $ n + 1) $ nonEmpty xs+ (x :| xs) | n == 0 -> x : maybe [] (go 0) (nonEmpty xs)+ x -> maybe [] (go 0) $ nonEmpty $ NEL.drop n x+ in toString . T.intercalate "/" . maybe [] (reverse . go 0 . NEL.reverse) . nonEmpty . T.splitOn "/" . toText++-- | An `UnresolvedRelTarget` that has been resolved.+--+-- See @Model.Link.Resolve@ for actual resolution logic.+data ResolvedRelTarget a+ = RRTMissing+ | RRTAmbiguous (NonEmpty a)+ | RRTFound a+ deriving stock (Eq, Show, Ord, Functor, Generic)+ deriving anyclass (ToJSON)++resolvedRelTargetFromCandidates :: [a] -> ResolvedRelTarget a+resolvedRelTargetFromCandidates xs =+ case nonEmpty xs of+ Nothing ->+ RRTMissing+ Just (x :| []) ->+ RRTFound x+ Just xs' ->+ RRTAmbiguous xs'
@@ -0,0 +1,32 @@+module Emanote.Model.Link.RelSpec where++import Emanote.Model.Link.Rel+import Hedgehog+import Relude+import Test.Hspec+import Test.Hspec.Hedgehog++spec :: Spec+spec = do+ describe "dropDotDot" $ do+ it "simple" . hedgehog $ do+ dropDotDot "foo/bar/qux" === "foo/bar/qux"+ dropDotDot "foo/../qux" === "qux"+ dropDotDot "bar/foo/../qux" === "bar/qux"+ dropDotDot "bar/foo/.." === "bar"+ it "dotInfix" . hedgehog $ do+ dropDotDot "bar/foo/../../qux" === "qux"+ dropDotDot "bar/foo/../../qux/../foo" === "foo"+ it "dotPrefix" . hedgehog $ do+ dropDotDot "../../foo" === "foo"+ dropDotDot "../foo" === "foo"+ dropDotDot "./../foo" === "foo"+ dropDotDot "./foo" === "./foo"+ it "dotSuffix" . hedgehog $ do+ dropDotDot "foo/.." === ""+ dropDotDot "foo/bar/.." === "foo"+ dropDotDot "foo/bar/../.." === ""++-- https://github.com/hedgehogqa/haskell-hedgehog/issues/121+once :: PropertyT IO () -> Property+once = withTests 1 . property
@@ -0,0 +1,45 @@+module Emanote.Model.Link.Resolve where++import Emanote.Model.Link.Rel qualified as Rel+import Emanote.Model.Note qualified as MN+import Emanote.Model.StaticFile qualified as SF+import Emanote.Model.Type (Model)+import Emanote.Model.Type qualified as M+import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Emanote.Route qualified as R+import Emanote.Route.SiteRoute qualified as SR+import Relude++resolveUnresolvedRelTarget ::+ Model ->+ Rel.UnresolvedRelTarget ->+ Rel.ResolvedRelTarget SR.SiteRoute+resolveUnresolvedRelTarget model = \case+ Rel.URTWikiLink (_wlType, wl) -> do+ resolveWikiLinkMustExist model wl+ <&> resourceSiteRoute+ Rel.URTResource r ->+ resolveModelRoute model r+ <&> resourceSiteRoute+ Rel.URTVirtual virtualRoute -> do+ Rel.RRTFound $+ SR.SiteRoute_VirtualRoute+ virtualRoute++resolveWikiLinkMustExist ::+ Model -> WL.WikiLink -> Rel.ResolvedRelTarget (Either MN.Note SF.StaticFile)+resolveWikiLinkMustExist model wl =+ Rel.resolvedRelTargetFromCandidates $ M.modelWikiLinkTargets wl model++resolveModelRoute ::+ Model -> R.ModelRoute -> Rel.ResolvedRelTarget (Either MN.Note SF.StaticFile)+resolveModelRoute model lr =+ bitraverse+ (`M.modelLookupNoteByRoute` model)+ (`M.modelLookupStaticFileByRoute` model)+ (R.modelRouteCase lr)+ & maybe Rel.RRTMissing Rel.RRTFound++resourceSiteRoute :: Either MN.Note SF.StaticFile -> SR.SiteRoute+resourceSiteRoute =+ either SR.noteFileSiteRoute SR.staticFileSiteRoute
@@ -0,0 +1,43 @@+module Emanote.Model.Meta+ ( lookupRouteMeta,+ getEffectiveRouteMeta,+ getEffectiveRouteMetaWith,+ )+where++import Data.Aeson (FromJSON)+import Data.Aeson qualified as Aeson+import Data.IxSet.Typed qualified as Ix+import Emanote.Model (ModelT, modelLookupNoteByRoute, modelSData)+import Emanote.Model.Note (_noteMeta)+import Emanote.Model.SData (sdataValue)+import Emanote.Model.SData qualified as SData+import Emanote.Route qualified as R+import Optics.Operators as Lens ((^.))+import Relude++-- | Look up a specific key in the meta for a given route.+lookupRouteMeta :: FromJSON a => a -> NonEmpty Text -> R.LMLRoute -> ModelT f -> a+lookupRouteMeta x k r =+ SData.lookupAeson x k . getEffectiveRouteMeta r++-- | Get the (final) metadata of a note at the given route, by merging it with+-- the defaults specified in parent routes all the way upto index.yaml.+getEffectiveRouteMeta :: R.LMLRoute -> ModelT f -> Aeson.Value+getEffectiveRouteMeta mr model =+ let mNote = modelLookupNoteByRoute mr model+ in getEffectiveRouteMetaWith (maybe Aeson.Null _noteMeta mNote) mr model++getEffectiveRouteMetaWith :: Aeson.Value -> R.LMLRoute -> ModelT f -> Aeson.Value+getEffectiveRouteMetaWith frontmatter mr model =+ let defaultFiles = R.routeInits @'R.Yaml (R.withLmlRoute coerce mr)+ defaults = flip mapMaybe (toList defaultFiles) $ \r -> do+ v <- getYamlMeta r model+ guard $ v /= Aeson.Null+ pure v+ metas = defaults <> maybe mempty one (guard (frontmatter /= Aeson.Null) >> pure frontmatter)+ in maybe Aeson.Null SData.mergeAesons $ nonEmpty metas++getYamlMeta :: R.R 'R.Yaml -> ModelT f -> Maybe Aeson.Value+getYamlMeta r model =+ fmap (^. sdataValue) . Ix.getOne . Ix.getEQ r $ model ^. modelSData
@@ -0,0 +1,345 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++module Emanote.Model.Note where++import Control.Monad.Logger (MonadLogger)+import Control.Monad.Writer (MonadWriter (tell), WriterT, runWriterT)+import Data.Aeson qualified as Aeson+import Data.Aeson.Optics qualified as AO+import Data.Default (Default (def))+import Data.IxSet.Typed (Indexable (..), IxSet, ixFun, ixList)+import Data.IxSet.Typed qualified as Ix+import Data.Map.Strict qualified as Map+import Data.Text qualified as T+import Emanote.Model.Note.Filter (applyPandocFilters)+import Emanote.Model.SData qualified as SData+import Emanote.Model.Title qualified as Tit+import Emanote.Pandoc.Markdown.Parser qualified as Markdown+import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as HT+import Emanote.Pandoc.Markdown.Syntax.WikiLink (plainify)+import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Emanote.Route (FileType (Folder), R)+import Emanote.Route qualified as R+import Network.URI.Slug (Slug)+import Optics.Core ((%), (.~))+import Optics.TH (makeLenses)+import Relude+import System.FilePath ((</>))+import Text.Pandoc (runPure)+import Text.Pandoc.Builder qualified as B+import Text.Pandoc.Definition (Pandoc (..))+import Text.Pandoc.Readers.Org (readOrg)+import Text.Pandoc.Walk qualified as W++data Note = Note+ { _noteRoute :: R.LMLRoute,+ _noteDoc :: Pandoc,+ _noteMeta :: Aeson.Value,+ _noteTitle :: Tit.Title,+ _noteErrors :: [Text]+ }+ deriving stock (Eq, Ord, Show, Generic)+ deriving anyclass (Aeson.ToJSON)++newtype RAncestor = RAncestor {unRAncestor :: R 'R.Folder}+ deriving stock (Eq, Ord, Show, Generic)+ deriving anyclass (Aeson.ToJSON)++type NoteIxs =+ '[ -- Route to this note+ R.LMLRoute,+ -- Allowed ways to wiki-link to this note.+ WL.WikiLink,+ -- HTML route for this note+ R 'R.Html,+ -- Ancestor folder routes+ RAncestor,+ -- Parent folder+ R 'R.Folder,+ -- Tag+ HT.Tag,+ -- Alias route for this note. Can be "foo" or "foo/bar".+ NonEmpty Slug+ ]++type IxNote = IxSet NoteIxs Note++instance Indexable NoteIxs Note where+ indices =+ ixList+ (ixFun $ one . _noteRoute)+ (ixFun $ toList . noteSelfRefs)+ (ixFun $ one . noteHtmlRoute)+ (ixFun noteAncestors)+ (ixFun $ maybeToList . noteParent)+ (ixFun noteTags)+ (ixFun $ maybeToList . noteSlug)++-- | All possible wiki-links that refer to this note.+noteSelfRefs :: Note -> NonEmpty WL.WikiLink+noteSelfRefs =+ routeSelfRefs+ . _noteRoute+ where+ routeSelfRefs :: R.LMLRoute -> NonEmpty WL.WikiLink+ routeSelfRefs =+ fmap snd+ . R.withLmlRoute WL.allowedWikiLinks++noteAncestors :: Note -> [RAncestor]+noteAncestors =+ maybe [] (toList . fmap RAncestor . R.routeInits) . noteParent++noteParent :: Note -> Maybe (R 'R.Folder)+noteParent = R.withLmlRoute R.routeParent . _noteRoute++hasChildNotes :: R 'Folder -> IxNote -> Bool+hasChildNotes r =+ not . Ix.null . Ix.getEQ r++noteTags :: Note -> [HT.Tag]+noteTags =+ fmap HT.Tag . maybeToMonoid . lookupMeta (one "tags")++noteSlug :: Note -> Maybe (NonEmpty Slug)+noteSlug note = do+ slugPath :: Text <- lookupMeta (one "slug") note+ fmap R.unRoute $ R.mkRouteFromFilePath @_ @'R.AnyExt $ toString slugPath++lookupMeta :: Aeson.FromJSON a => NonEmpty Text -> Note -> Maybe a+lookupMeta k =+ SData.lookupAeson Nothing k . _noteMeta++queryNoteTitle :: R.LMLRoute -> Pandoc -> Aeson.Value -> (Pandoc, Tit.Title)+queryNoteTitle r doc meta =+ let yamlNoteTitle = fromString <$> SData.lookupAeson Nothing (one "title") meta+ fileNameTitle = Tit.fromRoute r+ notePandocTitle = do+ case r of+ R.LMLRoute_Md _ ->+ getPandocTitle doc+ R.LMLRoute_Org _ ->+ getPandocMetaTitle doc+ in fromMaybe (doc, fileNameTitle) $+ fmap (doc,) yamlNoteTitle <|> fmap (withoutH1 doc,) notePandocTitle+ where+ getPandocTitle :: Pandoc -> Maybe Tit.Title+ getPandocTitle =+ fmap Tit.fromInlines . getPandocH1+ where+ getPandocH1 :: Pandoc -> Maybe [B.Inline]+ getPandocH1 (Pandoc _ (B.Header 1 _ inlines : _rest)) =+ Just inlines+ getPandocH1 _ =+ Nothing+ getPandocMetaTitle :: Pandoc -> Maybe Tit.Title+ getPandocMetaTitle (Pandoc docMeta _) = do+ B.MetaInlines inlines <- B.lookupMeta "title" docMeta+ pure $ Tit.fromInlines inlines+ withoutH1 :: B.Pandoc -> B.Pandoc+ withoutH1 (B.Pandoc m (B.Header 1 _ _ : rest)) =+ B.Pandoc m rest+ withoutH1 x =+ x++-- | The HTML route intended by user for this note.+noteHtmlRoute :: Note -> R 'R.Html+noteHtmlRoute note@Note {..} =+ -- Favour slug if one exists, otherwise use the full path.+ case noteSlug note of+ Nothing ->+ R.withLmlRoute coerce _noteRoute+ Just slugs ->+ R.mkRouteFromSlugs slugs++lookupNotesByHtmlRoute :: R 'R.Html -> IxNote -> [Note]+lookupNotesByHtmlRoute htmlRoute =+ Ix.toList . Ix.getEQ htmlRoute++lookupNotesByRoute :: HasCallStack => R.LMLRoute -> IxNote -> Maybe Note+lookupNotesByRoute r ix = do+ res <- nonEmpty $ Ix.toList $ Ix.getEQ r ix+ case res of+ note :| [] -> pure note+ _ -> error $ "ambiguous notes for route " <> show r++ancestorPlaceholderNote :: R.R 'Folder -> Note+ancestorPlaceholderNote r =+ let placeHolder =+ [ folderListingQuery,+ -- TODO: Ideally, we should use semantic tags, like <aside> (rather+ -- than <div>), to render these non-relevant content.+ B.Div (cls "emanote:placeholder-message") . one . B.Para $+ [ B.Str+ "Note: To override the auto-generated content here, create a file named one of: ",+ -- TODO: or, .org+ B.Span (cls "font-mono text-sm") $+ one $+ B.Str $+ oneOfLmlFilenames r+ ]+ ]+ in mkEmptyNoteWith (R.defaultLmlRoute r) placeHolder+ where+ folderListingQuery =+ B.CodeBlock (cls "query") "path:./*"++cls :: Text -> B.Attr+cls x =+ ("", one x, mempty) :: B.Attr++missingNote :: R.R ext -> Text -> Note+missingNote route404 urlPath =+ mkEmptyNoteWith (R.defaultLmlRoute route404) $+ one $+ B.Para+ [ B.Str "No note has the URL ",+ B.Code B.nullAttr $ "/" <> urlPath,+ -- TODO: org+ B.Span (cls "font-mono text-sm") $+ one $+ B.Str $+ ". You may create a file with that name, ie. one of: " <> oneOfLmlFilenames route404+ ]++oneOfLmlFilenames :: R ext -> Text+oneOfLmlFilenames r =+ T.intercalate+ ", "+ (toText . R.withLmlRoute R.encodeRoute <$> R.possibleLmlRoutes r)++ambiguousNoteURL :: FilePath -> NonEmpty R.LMLRoute -> Note+ambiguousNoteURL urlPath rs =+ mkEmptyNoteWith (head rs) $+ [ B.Para+ [ B.Str "The URL ",+ B.Code B.nullAttr $ toText urlPath,+ B.Str " is ambiguous, as more than one note (see list below) use it. To fix this, specify a different slug for these notes:"+ ]+ ]+ <> one candidates+ where+ candidates :: B.Block+ candidates =+ B.BulletList $+ toList rs <&> \(R.lmlRouteCase -> r) ->+ [ B.Plain $ one $ B.Str " ",+ B.Plain $ one $ B.Code B.nullAttr $ show r+ ]++mkEmptyNoteWith :: R.LMLRoute -> [B.Block] -> Note+mkEmptyNoteWith someR (Pandoc mempty -> doc) =+ mkNoteWith someR doc meta mempty+ where+ meta = Aeson.Null++mkNoteWith :: R.LMLRoute -> Pandoc -> Aeson.Value -> [Text] -> Note+mkNoteWith r doc' meta errs =+ let (doc'', tit) = queryNoteTitle r doc' meta+ doc = if null errs then doc'' else pandocPrepend (errorDiv errs) doc''+ in Note r doc meta tit errs+ where+ -- Prepend to block to the beginning of a Pandoc document (never before H1)+ pandocPrepend :: B.Block -> Pandoc -> Pandoc+ pandocPrepend prefix (Pandoc docMeta blocks) =+ let blocks' = case blocks of+ (h1@(B.Header 1 _ _) : rest) ->+ h1 : prefix : rest+ _ -> prefix : blocks+ in Pandoc docMeta blocks'+ errorDiv :: [Text] -> B.Block+ errorDiv s =+ B.Div (cls "emanote:error") $ B.Para [B.Strong $ one $ B.Str "Emanote Errors 😔"] : (B.Para . one . B.Str <$> s)++parseNote ::+ forall m.+ (MonadIO m, MonadLogger m) =>+ FilePath ->+ R.LMLRoute ->+ FilePath ->+ Text ->+ m Note+parseNote pluginBaseDir r fp s = do+ ((doc, meta), errs) <- runWriterT $ do+ case r of+ R.LMLRoute_Md _ ->+ parseNoteMarkdown pluginBaseDir fp s+ R.LMLRoute_Org _ -> do+ parseNoteOrg s+ pure $ mkNoteWith r doc meta errs++parseNoteOrg :: (MonadWriter [Text] m) => Text -> m (Pandoc, Aeson.Value)+parseNoteOrg s =+ case runPure $ readOrg def s of+ Left err -> do+ tell [show err]+ pure (mempty, defaultFrontMatter)+ Right doc ->+ -- TODO: Merge Pandoc's Meta in here?+ pure (doc, defaultFrontMatter)++parseNoteMarkdown :: (MonadIO m, MonadLogger m) => FilePath -> FilePath -> Text -> WriterT [Text] m (Pandoc, Aeson.Value)+parseNoteMarkdown pluginBaseDir fp md = do+ case Markdown.parseMarkdown fp md of+ Left err -> do+ tell [err]+ pure (mempty, defaultFrontMatter)+ Right (withAesonDefault defaultFrontMatter -> frontmatter, doc') -> do+ -- Apply the various transformation filters.+ --+ -- Some are user-defined; some builtin. They operate on Pandoc, or the+ -- frontmatter meta.+ let filterPaths = (pluginBaseDir </>) <$> SData.lookupAeson @[FilePath] mempty ("pandoc" :| ["filters"]) frontmatter+ doc <- applyPandocFilters filterPaths doc'+ let meta = applyNoteMetaFilters doc frontmatter+ pure (doc, meta)+ where+ withAesonDefault default_ mv =+ fromMaybe default_ mv+ `SData.mergeAeson` default_++defaultFrontMatter :: Aeson.Value+defaultFrontMatter =+ Aeson.toJSON $ Map.fromList @Text @[Text] $ one ("tags", [])++applyNoteMetaFilters :: Pandoc -> Aeson.Value -> Aeson.Value+applyNoteMetaFilters doc =+ addTagsFromBody+ >>> addDescriptionFromBody+ >>> addImageFromBody+ where+ -- Merge frontmatter tags with inline tags in Pandoc document.+ -- DESIGN: In retrospect, this is like a Pandoc lua filter?+ addTagsFromBody frontmatter =+ frontmatter+ & AO.key "tags" % AO._Array+ .~ ( fromList . fmap Aeson.toJSON $+ ordNub $+ SData.lookupAeson @[HT.Tag] mempty (one "tags") frontmatter+ <> HT.inlineTagsInPandoc doc+ )+ addDescriptionFromBody =+ overrideAesonText ("page" :| ["description"]) $ \case+ B.Para is -> [plainify is]+ _ -> mempty+ -- FIXME this doesn't take splice rendering into account. Specifically,+ -- `![[foo.jpeg]]` is not handled at all.+ addImageFromBody =+ overrideAesonText ("page" :| ["image"]) $ \case+ B.Image _ _ (url, _) -> [url]+ _ -> mempty+ overrideAesonText :: forall a. (W.Walkable a Pandoc) => NonEmpty Text -> (a -> [Text]) -> Aeson.Value -> Aeson.Value+ overrideAesonText key f frontmatter =+ SData.mergeAesons $+ frontmatter+ :| maybeToList+ ( do+ guard $ "" == SData.lookupAeson @Text "" key frontmatter+ val <- viaNonEmpty head $ W.query f doc+ pure $ SData.oneAesonText (toList key) val+ )++makeLenses ''Note
@@ -0,0 +1,50 @@+module Emanote.Model.Note.Filter (applyPandocFilters) where++import Control.Monad.Logger (MonadLogger)+import Control.Monad.Writer.Strict (MonadWriter (tell))+import Data.Default (def)+import Emanote.Prelude (logE, logW)+import Relude+import System.Directory (doesFileExist)+import System.FilePath (takeExtension)+import Text.Pandoc (runIO)+import Text.Pandoc.Definition (Pandoc (..))+import Text.Pandoc.Filter qualified as PF+import UnliftIO.Exception (handle)++applyPandocFilters :: (MonadIO m, MonadLogger m, MonadWriter [Text] m) => [FilePath] -> Pandoc -> m Pandoc+applyPandocFilters paths doc = do+ res <- traverse mkLuaFilter paths+ forM_ (lefts res) $ \err ->+ tell [err]+ case rights res of+ [] ->+ pure doc+ filters ->+ applyPandocLuaFilters filters doc >>= \case+ Left err -> tell [err] >> pure doc+ Right x -> pure x++mkLuaFilter :: MonadIO m => FilePath -> m (Either Text PF.Filter)+mkLuaFilter relPath = do+ if takeExtension relPath == ".lua"+ then do+ liftIO (doesFileExist relPath) >>= \case+ True -> pure $ Right $ PF.LuaFilter relPath+ False -> pure $ Left $ toText $ "Lua filter missing: " <> relPath+ else pure $ Left $ "Unsupported filter: " <> toText relPath++applyPandocLuaFilters :: (MonadIO m, MonadLogger m) => [PF.Filter] -> Pandoc -> m (Either Text Pandoc)+applyPandocLuaFilters filters x = do+ logW $ "[Experimental feature] Applying pandoc filters: " <> show filters+ -- TODO: Can we constrain this to run Lua code purely (embedded) without using IO?+ liftIO (runIOCatchingErrors $ PF.applyFilters def filters ["markdown"] x) >>= \case+ Left err -> do+ logE $ "Error applying pandoc filters: " <> show err+ pure $ Left (show err)+ Right x' -> pure $ Right x'+ where+ -- `runIO` can throw `PandocError`. Fix this nonsense behaviour, by catching+ -- it and returning a `Left`.+ runIOCatchingErrors =+ handle (pure . Left) . runIO
@@ -0,0 +1,92 @@+module Emanote.Model.Query where++import Data.IxSet.Typed ((@+), (@=))+import Data.IxSet.Typed qualified as Ix+import Data.Text qualified as T+import Emanote.Model.Calendar qualified as Calendar+import Emanote.Model.Note (Note)+import Emanote.Model.Note qualified as N+import Emanote.Model.Type (Model, modelNotes, modelTags)+import Emanote.Pandoc.Markdown.Syntax.HashTag (TagPattern)+import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as HT+import Emanote.Route qualified as R+import Optics.Operators ((^.))+import Relude+import System.FilePattern (FilePattern, (?==))+import Text.Megaparsec qualified as M+import Text.Megaparsec.Char qualified as M+import Text.Show qualified as Show++data Query+ = QueryByTag HT.Tag+ | QueryByTagPattern TagPattern+ | QueryByPath FilePath+ | QueryByPathPattern FilePattern+ deriving stock (Eq)++instance Show.Show Query where+ show = \case+ QueryByTag tag ->+ toString $ "Pages tagged #" <> HT.unTag tag+ QueryByTagPattern pat ->+ toString $ "Pages tagged by '" <> HT.unTagPattern pat <> "'"+ QueryByPath p ->+ "Pages under path '/" <> p <> "'"+ QueryByPathPattern pat ->+ "Pages matching path '" <> pat <> "'"++parseQuery :: Text -> Maybe Query+parseQuery = do+ rightToMaybe . parse queryParser "<pandoc:code:query>"+ where+ parse :: M.Parsec Void Text a -> String -> Text -> Either Text a+ parse p fn =+ first (toText . M.errorBundlePretty)+ . M.parse (p <* M.eof) fn++queryParser :: M.Parsec Void Text Query+queryParser = do+ (M.string "tag:#" *> fmap (QueryByTag . HT.Tag . T.strip) M.takeRest)+ <|> (M.string "tag:" *> fmap (QueryByTagPattern . HT.mkTagPattern . T.strip) M.takeRest)+ <|> (M.string "path:" *> fmap (fromUserPath . T.strip) M.takeRest)+ where+ fromUserPath s =+ if+ | "*" `T.isInfixOf` s ->+ QueryByPathPattern (toString s)+ | "/" `T.isPrefixOf` s ->+ QueryByPath (toString $ T.drop 1 s)+ | otherwise ->+ QueryByPathPattern (toString $ "**/" <> s <> "/**")++runQuery :: R.LMLRoute -> Model -> Query -> [Note]+runQuery currentRoute model =+ sortOn Calendar.noteSortKey . \case+ QueryByTag tag ->+ Ix.toList $ (model ^. modelNotes) @= tag+ QueryByTagPattern pat ->+ let allTags = fst <$> modelTags model+ matchingTags = filter (HT.tagMatch pat) allTags+ in Ix.toList $ (model ^. modelNotes) @+ matchingTags+ QueryByPath path ->+ maybeToMonoid $ do+ r <- R.mkRouteFromFilePath path+ pure $ Ix.toList $ (model ^. modelNotes) @= N.RAncestor r+ QueryByPathPattern (resolveDotInFilePattern -> pat) ->+ let notes = Ix.toList $ model ^. modelNotes+ in flip mapMaybe notes $ \note -> do+ guard $ pat ?== R.withLmlRoute R.encodeRoute (note ^. N.noteRoute)+ pure note+ where+ -- Resolve the ./ prefix which will for substituting "$PWD" in current+ -- note's route context.+ resolveDotInFilePattern (toText -> pat) =+ if "./" `T.isPrefixOf` pat+ then+ let folderR :: R.R 'R.Folder = R.withLmlRoute coerce currentRoute+ in if folderR == R.indexRoute+ then -- If in "index.md", discard the ./+ toString (T.drop 2 pat)+ else -- If in "$folder.md", discard the ./ and prepend with folder path prefix+ R.encodeRoute folderR <> "/" <> toString (T.drop 2 pat)+ else toString pat
@@ -0,0 +1,19 @@+module Emanote.Model.QuerySpec where++import Data.TagTree (Tag (Tag))+import Emanote.Model.Query+import Hedgehog+import Hedgehog.Gen qualified as Gen+import Hedgehog.Range qualified as Range+import Relude+import Test.Hspec+import Test.Hspec.Hedgehog (hedgehog)++spec :: Spec+spec = do+ describe "tags" $ do+ it "tags can begin with hash" . hedgehog $ do+ s <- forAll $ Gen.text (Range.linear 2 20) Gen.alphaNum+ let q = "tag:#" <> s+ tag = Tag s+ parseQuery q === Just (QueryByTag tag)
@@ -0,0 +1,74 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE TemplateHaskell #-}++module Emanote.Model.SData where++import Data.Aeson qualified as Aeson+import Data.Aeson.Extra.Merge qualified as AesonMerge+import Data.Aeson.KeyMap qualified as KM+import Data.Data (Data)+import Data.IxSet.Typed (Indexable (..), IxSet, ixGen, ixList)+import Data.List.NonEmpty qualified as NE+import Data.Yaml qualified as Yaml+import Emanote.Route qualified as R+import Optics.TH (makeLenses)+import Relude++-- | `S` for "structured". Refers to a per-route data file represented by Aeson+-- value. Example: /foo/bar.yaml file+data SData = SData+ { _sdataValue :: Aeson.Value,+ -- | Location of this data file+ _sdataRoute :: R.R 'R.Yaml+ }+ deriving stock (Eq, Ord, Data, Show, Generic)+ deriving anyclass (Aeson.ToJSON)++type SDataIxs = '[R.R 'R.Yaml]++type IxSData = IxSet SDataIxs SData++instance Indexable SDataIxs SData where+ indices =+ ixList+ (ixGen $ Proxy @(R.R 'R.Yaml))++makeLenses ''SData++parseSDataCascading :: R.R 'R.Yaml -> NonEmpty (FilePath, ByteString) -> Either Text SData+parseSDataCascading r bs = do+ vals <- forM bs $ \(fp, b) ->+ (first (\err -> toText $ "Failed to parse " <> fp <> " :" <> Yaml.prettyPrintParseException err) . Yaml.decodeEither') b+ let val = mergeAesons vals+ pure $ SData val r++-- | Later values override former.+mergeAesons :: NonEmpty Aeson.Value -> Aeson.Value+mergeAesons =+ last . NE.scanl1 mergeAeson++mergeAeson :: Aeson.Value -> Aeson.Value -> Aeson.Value+mergeAeson = AesonMerge.lodashMerge++-- TODO: Use https://hackage.haskell.org/package/lens-aeson+lookupAeson :: forall a. Aeson.FromJSON a => a -> NonEmpty Text -> Aeson.Value -> a+lookupAeson x (k :| ks) meta =+ fromMaybe x $ do+ Aeson.Object obj <- pure meta+ val <- KM.lookup (fromString . toString $ k) obj+ case nonEmpty ks of+ Nothing -> resultToMaybe $ Aeson.fromJSON val+ Just ks' -> pure $ lookupAeson x ks' val+ where+ resultToMaybe :: Aeson.Result b -> Maybe b+ resultToMaybe = \case+ Aeson.Error _ -> Nothing+ Aeson.Success b -> pure b++oneAesonText :: [Text] -> Text -> Aeson.Value+oneAesonText k v =+ case nonEmpty k of+ Nothing ->+ Aeson.String v+ Just (x :| xs) ->+ Aeson.object [(fromString . toString) x Aeson..= oneAesonText (toList xs) v]
@@ -0,0 +1,39 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE TemplateHaskell #-}++module Emanote.Model.StaticFile where++import Data.Aeson qualified as Aeson+import Data.IxSet.Typed (Indexable (..), IxSet, ixFun, ixList)+import Data.Time (UTCTime)+import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Emanote.Route qualified as R+import Optics.TH (makeLenses)+import Relude++data StaticFile = StaticFile+ { _staticFileRoute :: R.R 'R.AnyExt,+ _staticFilePath :: FilePath,+ -- | Indicates that this file was updated no latter than the given time.+ _staticFileTime :: UTCTime+ }+ deriving stock (Eq, Ord, Show, Generic)+ deriving anyclass (Aeson.ToJSON)++type StaticFileIxs = '[R.R 'R.AnyExt, WL.WikiLink]++type IxStaticFile = IxSet StaticFileIxs StaticFile++instance Indexable StaticFileIxs StaticFile where+ indices =+ ixList+ (ixFun $ one . _staticFileRoute)+ (ixFun $ toList . staticFileSelfRefs)++staticFileSelfRefs :: StaticFile -> NonEmpty WL.WikiLink+staticFileSelfRefs =+ fmap snd+ . WL.allowedWikiLinks+ . _staticFileRoute++makeLenses ''StaticFile
@@ -0,0 +1,31 @@+module Emanote.Model.Stork+ ( renderStorkIndex,+ )+where++import Control.Monad.Logger (MonadLoggerIO)+import Data.IxSet.Typed qualified as Ix+import Emanote.Model.Note qualified as N+import Emanote.Model.Stork.Index (File (File), Input (Input), readOrBuildStorkIndex)+import Emanote.Model.Title qualified as Tit+import Emanote.Model.Type (Model)+import Emanote.Model.Type qualified as M+import Emanote.Route qualified as R+import Emanote.Route.SiteRoute qualified as SR+import Emanote.Source.Loc qualified as Loc+import Optics.Core ((^.))+import Relude+import System.FilePath ((</>))++renderStorkIndex :: (MonadIO m, MonadLoggerIO m) => Model -> m LByteString+renderStorkIndex model = do+ readOrBuildStorkIndex (model ^. M.modelStorkIndex) (Input $ storkFiles model)++storkFiles :: Model -> [File]+storkFiles model =+ let baseDir = Loc.locPath . Loc.primaryLayer $ model ^. M.modelLayers+ in Ix.toList (model ^. M.modelNotes) <&> \note ->+ File+ ((baseDir </>) $ R.withLmlRoute R.encodeRoute $ note ^. N.noteRoute)+ (SR.siteRouteUrl model $ SR.lmlSiteRoute $ note ^. N.noteRoute)+ (Tit.toPlain $ note ^. N.noteTitle)
@@ -0,0 +1,104 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE TemplateHaskell #-}++module Emanote.Model.Stork.Index+ ( IndexVar,+ newIndex,+ clearStorkIndex,+ readOrBuildStorkIndex,+ File (File),+ Input (Input),+ )+where++import Control.Monad.Logger (MonadLoggerIO)+import Data.Text qualified as T+import Data.Time (NominalDiffTime, diffUTCTime, getCurrentTime)+import Emanote.Prelude (log, logD, logW)+import Numeric (showGFloat)+import Relude+import System.Process.ByteString (readProcessWithExitCode)+import System.Which (staticWhich)+import Toml (TomlCodec, encode, list, string, text, (.=))++-- | In-memory Stork index tracked in a @TVar@+newtype IndexVar = IndexVar (TVar (Maybe LByteString))++newIndex :: MonadIO m => m IndexVar+newIndex =+ IndexVar <$> newTVarIO mempty++clearStorkIndex :: (MonadIO m) => IndexVar -> m ()+clearStorkIndex (IndexVar var) = atomically $ writeTVar var mempty++readOrBuildStorkIndex :: (MonadIO m, MonadLoggerIO m) => IndexVar -> Input -> m LByteString+readOrBuildStorkIndex (IndexVar indexVar) input = do+ readTVarIO indexVar >>= \case+ Just index -> do+ logD "STORK: Returning cached search index"+ pure index+ Nothing -> do+ -- TODO: What if there are concurrent reads? We probably need a lock.+ -- And we want to encapsulate this whole thing.+ logW "STORK: Generating search index (this may be expensive)"+ (diff, !index) <- timeIt $ runStork input+ log $ toText $ "STORK: Done generating search index in " <> showGFloat (Just 2) diff "" <> " seconds"+ atomically $ modifyTVar' indexVar $ \_ -> Just index+ pure index+ where+ timeIt :: MonadIO m => m b -> m (Double, b)+ timeIt m = do+ t0 <- liftIO getCurrentTime+ !x <- m+ t1 <- liftIO getCurrentTime+ let diff :: NominalDiffTime = diffUTCTime t1 t0+ pure (realToFrac diff, x)++storkBin :: FilePath+storkBin = $(staticWhich "stork")++runStork :: MonadIO m => Input -> m LByteString+runStork input = do+ let storkToml = handleTomlandBug $ Toml.encode inputCodec input+ (_, !index, _) <-+ liftIO $+ readProcessWithExitCode+ storkBin+ -- NOTE: Cannot use "--output -" due to bug in Rust or Stork:+ -- https://github.com/jameslittle230/stork/issues/262+ ["build", "-t", "--input", "-", "--output", "/dev/stdout"]+ (encodeUtf8 storkToml)+ pure $ toLazy index+ where+ handleTomlandBug =+ -- HACK: Deal with tomland's bug.+ -- https://github.com/EmaApps/emanote/issues/336+ -- https://github.com/kowainik/tomland/issues/408+ --+ -- This could be problematic if the user literally uses \\U in their note+ -- title (but why would they?)+ T.replace "\\\\U" "\\U"++newtype Input = Input+ { inputFiles :: [File]+ }+ deriving stock (Eq, Show)++data File = File+ { filePath :: FilePath,+ fileUrl :: Text,+ fileTitle :: Text+ }+ deriving stock (Eq, Show)++fileCodec :: TomlCodec File+fileCodec =+ File+ <$> Toml.string "path" .= filePath+ <*> Toml.text "url" .= fileUrl+ <*> Toml.text "title" .= fileTitle++inputCodec :: TomlCodec Input+inputCodec =+ Input+ <$> Toml.list fileCodec "input.files" .= inputFiles
@@ -0,0 +1,51 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE TemplateHaskell #-}++module Emanote.Model.Task where++import Data.Aeson qualified as Aeson+import Data.IxSet.Typed (Indexable (..), IxSet, ixFun, ixList)+import Data.IxSet.Typed qualified as Ix+import Emanote.Model.Note (Note)+import Emanote.Model.Note qualified as N+import Emanote.Route qualified as R+import Heist.Extra.Splices.Pandoc.TaskList qualified as TaskList+import Optics.Operators ((^.))+import Optics.TH (makeLenses)+import Relude+import Text.Pandoc.Builder qualified as B++data Task = Task+ { _taskRoute :: R.LMLRoute,+ -- Index of this task within the containing note. Used to sort tasks by+ -- their original order of appearance in the Markdown file.+ _taskNum :: Word,+ _taskDescription :: [B.Inline],+ _taskChecked :: Bool+ }+ deriving stock (Eq, Show, Generic)+ deriving anyclass (Aeson.ToJSON)++instance Ord Task where+ (<=) = (<=) `on` (_taskRoute &&& _taskNum)++type TaskIxs =+ '[ -- Route to the note containing this task+ R.LMLRoute+ ]++type IxTask = IxSet TaskIxs Task++instance Indexable TaskIxs Task where+ indices =+ ixList+ (ixFun $ one . _taskRoute)++noteTasks :: Note -> IxTask+noteTasks note =+ let taskListItems = TaskList.queryTasks $ note ^. N.noteDoc+ in Ix.fromList $+ zip [1 ..] taskListItems <&> \(idx, (checked, doc)) ->+ Task (note ^. N.noteRoute) idx doc checked++makeLenses ''Task
@@ -0,0 +1,88 @@+{-# LANGUAGE DeriveAnyClass #-}++module Emanote.Model.Title+ ( Title,++ -- * Title conversion+ fromRoute,+ fromInlines,+ toInlines,++ -- * Rendering a Title+ titleSplice,+ titleSpliceNoHtml,+ toPlain,+ )+where++import Data.Aeson (ToJSON)+import Emanote.Pandoc.Markdown.Syntax.WikiLink (plainify)+import Emanote.Route qualified as R+import Heist.Extra.Splices.Pandoc qualified as HP+import Heist.Interpreted qualified as HI+import Relude+import Text.Pandoc.Definition qualified as B+import Text.Pandoc.Walk qualified as W++data Title+ = TitlePlain Text+ | TitlePandoc [B.Inline]+ deriving stock (Show, Generic)+ deriving anyclass (ToJSON)++instance Eq Title where+ (==) =+ -- Use toPlain here, rather than toInlines, because the same text can have+ -- different inlines structure. For example, "Foo Bar" can be represented as+ -- [Str "Foo", Space, Str "Bar"],+ -- or as,+ -- [Str "Foo Bar"]+ on (==) toPlain++instance Ord Title where+ compare =+ on compare toPlain++instance Semigroup Title where+ TitlePlain a <> TitlePlain b =+ TitlePlain (a <> b)+ x <> y =+ TitlePandoc $ on (<>) toInlines x y++instance IsString Title where+ fromString = TitlePlain . toText++fromRoute :: R.LMLRoute -> Title+fromRoute =+ TitlePlain . R.withLmlRoute R.routeBaseName++fromInlines :: [B.Inline] -> Title+fromInlines = TitlePandoc++toInlines :: Title -> [B.Inline]+toInlines = \case+ TitlePlain s -> one (B.Str s)+ TitlePandoc is -> is++toPlain :: Title -> Text+toPlain = \case+ TitlePlain s -> s+ TitlePandoc is -> plainify is++titleSplice ::+ forall b.+ (W.Walkable B.Inline b, b ~ [B.Inline]) =>+ HP.RenderCtx ->+ (b -> b) ->+ Title ->+ HI.Splice Identity+titleSplice ctx f = \case+ TitlePlain x ->+ HI.textSplice x+ TitlePandoc is -> do+ let titleDoc = B.Pandoc mempty $ one $ B.Plain $ f is+ HP.pandocSplice ctx titleDoc++titleSpliceNoHtml :: Title -> HI.Splice Identity+titleSpliceNoHtml =+ HI.textSplice . toPlain
@@ -0,0 +1,271 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++module Emanote.Model.Type where++import Data.Aeson qualified as Aeson+import Data.Default (Default (def))+import Data.IxSet.Typed ((@=))+import Data.IxSet.Typed qualified as Ix+import Data.Map.Strict qualified as Map+import Data.Some (Some)+import Data.Time (UTCTime)+import Data.Tree (Tree)+import Data.Tree.Path qualified as PathTree+import Data.UUID (UUID)+import Ema.CLI qualified+import Emanote.Model.Link.Rel (IxRel)+import Emanote.Model.Link.Rel qualified as Rel+import Emanote.Model.Note+ ( IxNote,+ Note,+ )+import Emanote.Model.Note qualified as N+import Emanote.Model.SData (IxSData, SData, sdataRoute)+import Emanote.Model.StaticFile+ ( IxStaticFile,+ StaticFile (StaticFile),+ )+import Emanote.Model.Stork.Index qualified as Stork+import Emanote.Model.Task (IxTask)+import Emanote.Model.Task qualified as Task+import Emanote.Model.Title qualified as Tit+import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as HT+import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Emanote.Pandoc.Renderer (EmanotePandocRenderers)+import Emanote.Route (FileType (AnyExt), LMLRoute, R)+import Emanote.Route qualified as R+import Emanote.Route.SiteRoute.Type (SiteRoute)+import Emanote.Source.Loc (Loc)+import Heist.Extra.TemplateState (TemplateState)+import Network.URI.Slug (Slug)+import Optics.Core (Prism')+import Optics.Operators ((%~), (.~), (^.))+import Optics.TH (makeLenses)+import Relude++data Status = Status_Loading | Status_Ready+ deriving stock (Eq, Show)++data ModelT encF = Model+ { _modelStatus :: Status,+ _modelLayers :: Set Loc,+ _modelEmaCLIAction :: Some Ema.CLI.Action,+ _modelRoutePrism :: encF (Prism' FilePath SiteRoute),+ -- | Dictates how exactly to render `Pandoc` to Heist nodes.+ _modelPandocRenderers :: EmanotePandocRenderers Model LMLRoute,+ _modelCompileTailwind :: Bool,+ -- | An unique ID for this process's model. ID changes across processes.+ _modelInstanceID :: UUID,+ _modelNotes :: IxNote,+ _modelRels :: IxRel,+ _modelSData :: IxSData,+ _modelStaticFiles :: IxStaticFile,+ _modelTasks :: IxTask,+ _modelNav :: [Tree Slug],+ _modelHeistTemplate :: TemplateState,+ _modelStorkIndex :: Stork.IndexVar+ }+ deriving stock (Generic)++type Model = ModelT Identity++-- | A bare version of `Model` that is managed by the Ema app.+--+-- The only difference is that this one has no `RouteEncoder`.+type ModelEma = ModelT (Const ())++deriving stock instance Generic ModelEma++deriving stock instance Generic Model++makeLenses ''ModelT++withoutRoutePrism :: Model -> (Prism' FilePath SiteRoute, ModelEma)+withoutRoutePrism model@Model {..} =+ let _modelRoutePrism = Const ()+ in (runIdentity $ model ^. modelRoutePrism, Model {..})++withRoutePrism :: Prism' FilePath SiteRoute -> ModelEma -> Model+withRoutePrism enc Model {..} =+ let _modelRoutePrism = Identity enc+ in Model {..}++emptyModel :: Set Loc -> Some Ema.CLI.Action -> EmanotePandocRenderers Model LMLRoute -> Bool -> UUID -> Stork.IndexVar -> ModelEma+emptyModel layers act ren ctw instanceId =+ Model Status_Loading layers act (Const ()) ren ctw instanceId Ix.empty Ix.empty Ix.empty Ix.empty mempty mempty def++modelReadyForView :: ModelT f -> ModelT f+modelReadyForView =+ modelStatus .~ Status_Ready++-- | Are we running in live server, or statically generated website?+inLiveServer :: Model -> Bool+inLiveServer = Ema.CLI.isLiveServer . _modelEmaCLIAction++modelInsertNote :: Note -> ModelT f -> ModelT f+modelInsertNote note =+ modelNotes+ %~ ( Ix.updateIx r note+ -- Insert folder placeholder automatically for ancestor paths+ >>> flip (foldr injectAncestor) (N.noteAncestors note)+ )+ >>> modelRels+ %~ updateIxMulti r (Rel.noteRels note)+ >>> modelTasks+ %~ updateIxMulti r (Task.noteTasks note)+ >>> modelNav+ %~ PathTree.treeInsertPath (R.withLmlRoute R.unRoute r)+ where+ r = note ^. N.noteRoute++injectAncestor :: N.RAncestor -> IxNote -> IxNote+injectAncestor (N.unRAncestor -> folderR) ns =+ case resolveLmlRouteIfExists ns folderR of+ Just _ -> ns+ Nothing ->+ let r = R.defaultLmlRoute folderR+ in Ix.updateIx r (N.ancestorPlaceholderNote folderR) ns++modelDeleteNote :: LMLRoute -> ModelT f -> ModelT f+modelDeleteNote k model =+ model+ & modelNotes+ %~ ( Ix.deleteIx k+ -- Restore folder placeholder, if $folder.md gets deleted (with $folder/*.md still present)+ -- TODO: If $k.md is the only file in its parent, delete unnecessary ancestors+ >>> maybe id restoreFolderPlaceholder mFolderR+ )+ & modelRels+ %~ deleteIxMulti k+ & modelTasks+ %~ deleteIxMulti k+ & modelNav+ %~ maybe (PathTree.treeDeletePath (R.withLmlRoute R.unRoute k)) (const id) mFolderR+ where+ -- If the note being deleted is $folder.md *and* folder/ has .md files, this+ -- will be `Just folderRoute`.+ mFolderR = do+ let folderR = R.withLmlRoute coerce k+ guard $ N.hasChildNotes folderR $ model ^. modelNotes+ pure folderR+ restoreFolderPlaceholder =+ injectAncestor . N.RAncestor++-- | Like `Ix.updateIx`, but works for multiple items.+updateIxMulti ::+ (Ix.IsIndexOf ix ixs, Ix.Indexable ixs a) =>+ ix ->+ Ix.IxSet ixs a ->+ Ix.IxSet ixs a ->+ Ix.IxSet ixs a+updateIxMulti r new rels =+ let old = rels @= r+ deleteMany = foldr Ix.delete+ in new `Ix.union` (rels `deleteMany` old)++-- | Like `Ix.deleteIx`, but works for multiple items+deleteIxMulti ::+ (Ix.Indexable ixs a, Ix.IsIndexOf ix ixs) =>+ ix ->+ Ix.IxSet ixs a ->+ Ix.IxSet ixs a+deleteIxMulti r rels =+ let candidates = Ix.toList $ Ix.getEQ r rels+ in flipfoldl' Ix.delete rels candidates++modelLookupStaticFile :: FilePath -> ModelT f -> Maybe StaticFile+modelLookupStaticFile fp m = do+ r :: R.R 'AnyExt <- R.mkRouteFromFilePath fp+ Ix.getOne $ Ix.getEQ r $ m ^. modelStaticFiles++modelInsertStaticFile :: UTCTime -> R.R 'AnyExt -> FilePath -> ModelT f -> ModelT f+modelInsertStaticFile t r fp =+ modelStaticFiles %~ Ix.updateIx r staticFile+ where+ staticFile = StaticFile r fp t++modelDeleteStaticFile :: R.R 'AnyExt -> ModelT f -> ModelT f+modelDeleteStaticFile r =+ modelStaticFiles %~ Ix.deleteIx r++modelInsertData :: SData -> ModelT f -> ModelT f+modelInsertData v =+ modelSData %~ Ix.updateIx (v ^. sdataRoute) v++modelDeleteData :: R.R 'R.Yaml -> ModelT f -> ModelT f+modelDeleteData k =+ modelSData %~ Ix.deleteIx k++modelLookupNoteByRoute :: LMLRoute -> ModelT f -> Maybe Note+modelLookupNoteByRoute r (_modelNotes -> notes) =+ N.lookupNotesByRoute r notes++modelLookupNoteByHtmlRoute :: R 'R.Html -> ModelT f -> Rel.ResolvedRelTarget Note+modelLookupNoteByHtmlRoute r =+ Rel.resolvedRelTargetFromCandidates+ . N.lookupNotesByHtmlRoute r+ . _modelNotes++modelLookupTitle :: LMLRoute -> ModelT f -> Tit.Title+modelLookupTitle r =+ maybe (Tit.fromRoute r) N._noteTitle . modelLookupNoteByRoute r++-- Lookup the wiki-link and return its candidates in the model.+modelWikiLinkTargets :: WL.WikiLink -> Model -> [Either Note StaticFile]+modelWikiLinkTargets wl model =+ let notes =+ Ix.toList $+ (model ^. modelNotes) @= wl+ staticFiles =+ Ix.toList $+ (model ^. modelStaticFiles) @= wl+ in fmap Right staticFiles <> fmap Left notes++modelLookupStaticFileByRoute :: R 'AnyExt -> ModelT f -> Maybe StaticFile+modelLookupStaticFileByRoute r =+ Ix.getOne . Ix.getEQ r . _modelStaticFiles++modelTags :: ModelT f -> [(HT.Tag, [Note])]+modelTags =+ Ix.groupAscBy @HT.Tag . _modelNotes++modelNoteRels :: Model -> [Rel.Rel]+modelNoteRels =+ Ix.toList . _modelRels++modelNoteMetas :: Model -> Map LMLRoute (Tit.Title, LMLRoute, Aeson.Value)+modelNoteMetas model =+ Map.fromList $+ Ix.toList (_modelNotes model) <&> \note ->+ (note ^. N.noteRoute, (note ^. N.noteTitle, note ^. N.noteRoute, note ^. N.noteMeta))++modelNoteErrors :: Model -> Map LMLRoute [Text]+modelNoteErrors model =+ Map.fromList $+ flip mapMaybe (Ix.toList (_modelNotes model)) $ \note -> do+ let errs = note ^. N.noteErrors+ guard $ not $ null errs+ pure (note ^. N.noteRoute, errs)++-- | Return the most suitable index LML route+--+-- If index.org exist, use that. Otherwise, fallback to index.md.+modelIndexRoute :: ModelT f -> LMLRoute+modelIndexRoute model = do+ resolveLmlRoute model R.indexRoute++resolveLmlRoute :: forall lmlType f. ModelT f -> R ('R.LMLType lmlType) -> LMLRoute+resolveLmlRoute model r =+ fromMaybe (R.defaultLmlRoute r) $ resolveLmlRouteIfExists (model ^. modelNotes) r++resolveLmlRouteIfExists :: forall ext. IxNote -> R ext -> Maybe LMLRoute+resolveLmlRouteIfExists notes r = do+ -- TODO: Refactor using `[minBound..maxBound] :: [LML]`+ note <-+ asum+ [ N.lookupNotesByRoute (R.LMLRoute_Org $ coerce r) notes,+ N.lookupNotesByRoute (R.LMLRoute_Md $ coerce r) notes+ ]+ pure $ note ^. N.noteRoute
@@ -0,0 +1,53 @@+module Emanote.Pandoc.BuiltinFilters+ ( prepareNoteDoc,+ preparePandoc,+ )+where++import Emanote.Model.Note qualified as N+import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as HT+import Emanote.Route (encodeRoute)+import Emanote.Route.SiteRoute.Type (encodeTagIndexR)+import Optics.Core ((^.))+import Relude+import Text.Pandoc.Definition qualified as B+import Text.Pandoc.Walk qualified as W++-- TODO: Run this in `parseNote`?+prepareNoteDoc :: N.Note -> B.Pandoc+prepareNoteDoc note =+ preparePandoc $ note ^. N.noteDoc++preparePandoc :: W.Walkable B.Inline b => b -> b+preparePandoc =+ linkifyInlineTags+ >>> fixEmojiFontFamily++-- HashTag.hs generates a Span for inline tags.+-- Here, we must link them to the special tag index page.+linkifyInlineTags :: W.Walkable B.Inline b => b -> b+linkifyInlineTags =+ W.walk $ \case+ inline@(B.Span attr is) ->+ if+ | Just inlineTag <- HT.getTagFromInline inline ->+ B.Span attr [B.Link mempty is (tagUrl inlineTag, "Tag")]+ | otherwise ->+ inline+ x ->+ x+ where+ tagUrl =+ toText . encodeRoute . encodeTagIndexR . toList . HT.deconstructTag++-- Undo font-familly on emoji spans, so the browser uses an emoji font.+-- Ref: https://github.com/jgm/commonmark-hs/blob/3d545d7afa6c91820b4eebf3efeeb80bf1b27128/commonmark-extensions/src/Commonmark/Extensions/Emoji.hs#L30-L33+fixEmojiFontFamily :: W.Walkable B.Inline b => b -> b+fixEmojiFontFamily =+ W.walk $ \case+ B.Span (id', classes, attrs) is+ | classes == ["emoji"] ->+ let emojiFontAttr = ("style", "font-family: emoji")+ newAttrs = attrs <> one emojiFontAttr+ in B.Span (id', classes, newAttrs) is+ x -> x
@@ -0,0 +1,37 @@+module Emanote.Pandoc.Link where++import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Relude+import Text.Pandoc.Definition qualified as B++-- | A Pandoc inline node that refers to something else.+--+-- There are, currently, only two possible nodes: link & image.+data InlineRef+ = InlineLink+ | InlineImage+ deriving stock (Eq, Show)++parseInlineRef :: B.Inline -> Maybe (InlineRef, B.Attr, [B.Inline], (Text, Text))+parseInlineRef = \case+ B.Link attr is (url, tit) ->+ pure (InlineLink, attr, is, (url, tit))+ B.Image attr is (url, tit) ->+ pure (InlineImage, attr, is, (url, tit))+ _ ->+ Nothing++-- | Given an inline that is known to be an InlineRef, reconstruct and return+-- its orginal Markdown source.+unParseLink :: HasCallStack => B.Inline -> Text+unParseLink inl =+ case WL.wikiLinkInlineRendered inl of+ Just url ->+ url+ Nothing ->+ let (inlRef, _, is, (url, _tit)) = parseInlineRefMust inl+ prefix = if inlRef == InlineImage then ""+ where+ parseInlineRefMust =+ fromMaybe (error "Non-InlineRef Inline") . parseInlineRef
@@ -0,0 +1,51 @@+module Emanote.Pandoc.Markdown.Parser+ ( parseMarkdown,+ )+where++import Commonmark qualified as CM+import Commonmark.Extensions qualified as CE+import Commonmark.Simple (parseMarkdownWithFrontMatter)+import Data.Aeson qualified as Aeson+import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as IT+import Emanote.Pandoc.Markdown.Syntax.Highlight qualified as IH+import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Relude+import Text.Pandoc.Definition (Pandoc)++parseMarkdown :: FilePath -> Text -> Either Text (Maybe Aeson.Value, Pandoc)+parseMarkdown =+ parseMarkdownWithFrontMatter @Aeson.Value $+ -- As the commonmark documentation states, pipeTableSpec should be placed after+ -- fancyListSpec and defaultSyntaxSpec to avoid bad results when parsing+ -- non-table lines.+ -- see https://github.com/jgm/commonmark-hs/issues/52+ baseExtsSansPipeTable+ <> gfmExtensionsSansPipeTable+ <> CE.pipeTableSpec+ <> WL.wikilinkSpec+ -- ASK: Can we conditionally disable this?+ -- cf. https://github.com/srid/emanote/issues/167+ <> IT.hashTagSpec+ <> IH.highlightSpec+ where+ baseExtsSansPipeTable =+ mconcat+ [ CE.fancyListSpec,+ CE.footnoteSpec,+ CE.mathSpec,+ CE.smartPunctuationSpec,+ CE.definitionListSpec,+ CE.attributesSpec,+ CE.rawAttributeSpec,+ CE.fencedDivSpec,+ CE.bracketedSpanSpec,+ CE.autolinkSpec,+ CM.defaultSyntaxSpec+ ]+ gfmExtensionsSansPipeTable =+ CE.emojiSpec+ <> CE.strikethroughSpec+ <> CE.autolinkSpec+ <> CE.autoIdentifiersSpec+ <> CE.taskListSpec
@@ -0,0 +1,82 @@+module Emanote.Pandoc.Markdown.Syntax.HashTag+ ( hashTagSpec,+ inlineTagsInPandoc,+ getTagFromInline,+ TT.Tag (..),+ TT.TagPattern (..),+ TT.TagNode (..),+ TT.mkTagPattern,+ TT.tagMatch,+ TT.constructTag,+ TT.deconstructTag,+ TT.tagTree,+ )+where++import Commonmark (TokType (..))+import Commonmark qualified as CM+import Commonmark.Inlines qualified as CM+import Commonmark.Pandoc qualified as CP+import Commonmark.TokParsers (noneOfToks, symbol)+import Data.Map.Strict qualified as Map+import Data.TagTree qualified as TT+import Data.Text qualified as T+import Relude+import Text.Pandoc.Builder qualified as B+import Text.Pandoc.Walk qualified as W+import Text.Parsec qualified as P++inlineTagsInPandoc :: B.Pandoc -> [TT.Tag]+inlineTagsInPandoc = W.query $ maybeToList . getTagFromInline++getTagFromInline :: B.Inline -> Maybe TT.Tag+getTagFromInline = \case+ B.Span (_, _, Map.fromList -> attrs) _ -> do+ tag <- Map.lookup tagDataAttr attrs+ pure $ TT.Tag tag+ _ -> Nothing++class HasHashTag il where+ hashTag :: TT.Tag -> il++instance HasHashTag (CP.Cm b B.Inlines) where+ hashTag (TT.Tag tag) =+ let attrs =+ [ ("title", "Tag"),+ (tagDataAttr, tag)+ ]+ classes =+ [ "emanote:inline-tag",+ -- This must be placed *after* the class above, to allow the user to+ -- override generic styles (of the class above)+ "emanote:inline-tag:" <> tag+ ]+ in CP.Cm $ B.spanWith ("", classes, attrs) $ B.str $ "#" <> tag++tagDataAttr :: Text+tagDataAttr = "data-tag"++hashTagSpec ::+ (Monad m, CM.IsBlock il bl, CM.IsInline il, HasHashTag il) =>+ CM.SyntaxSpec m il bl+hashTagSpec =+ mempty+ { CM.syntaxInlineParsers = [pTag]+ }+ where+ pTag ::+ (Monad m, CM.IsInline il, HasHashTag il) =>+ CM.InlineParser m il+ pTag = P.try $ do+ _ <- symbol '#'+ tag <- CM.untokenize <$> tagP+ pure $ hashTag $ TT.Tag tag+ tagP :: Monad m => P.ParsecT [CM.Tok] s m [CM.Tok]+ tagP = do+ s <- some (noneOfToks disallowed)+ -- A tag cannot end with a slash (which is a separator in hierarchical tags)+ guard $ not $ "/" `T.isSuffixOf` CM.untokenize s+ pure s+ where+ disallowed = [Spaces, UnicodeSpace, LineEnd] <> fmap Symbol punctuation+ punctuation = "[];:,.?!"
@@ -0,0 +1,28 @@+module Emanote.Pandoc.Markdown.Syntax.Highlight+ ( highlightSpec,+ )+where++import Commonmark qualified as CM+import Commonmark.Inlines qualified as CM+import Commonmark.Pandoc qualified as CP+import Relude+import Text.Pandoc.Builder qualified as B++highlightSpec ::+ (Monad m, CM.IsInline il, HasHighlight il) =>+ CM.SyntaxSpec m il bl+highlightSpec =+ mempty+ { CM.syntaxFormattingSpecs =+ [ CM.FormattingSpec '=' True True Nothing (Just highlight) '='+ ]+ }++class HasHighlight a where+ highlight :: a -> a++instance HasHighlight (CP.Cm a B.Inlines) where+ highlight il = B.spanWith attr <$> il+ where+ attr = ("", one "highlight-inline", [])
@@ -0,0 +1,275 @@+{-# LANGUAGE DeriveAnyClass #-}++module Emanote.Pandoc.Markdown.Syntax.WikiLink+ ( WikiLink,+ WikiLinkType (..),+ wikilinkSpec,+ mkWikiLinkFromRoute,+ delineateLink,+ wikilinkInline,+ wikiLinkInlineRendered,+ mkWikiLinkFromInline,+ allowedWikiLinks,++ -- * Anchors in URLs+ Anchor,+ anchorSuffix,++ -- * Pandoc helper, which use wikilink somehow+ plainify,+ )+where++import Commonmark qualified as CM+import Commonmark.Pandoc qualified as CP+import Commonmark.TokParsers qualified as CT+import Control.Monad (liftM2)+import Data.Aeson (ToJSON (toJSON))+import Data.Data (Data)+import Data.List.NonEmpty qualified as NE+import Data.Map.Strict qualified as Map+import Data.Text qualified as T+import Emanote.Route.Ext qualified as Ext+import Emanote.Route.R (R (..))+import Network.URI.Encode qualified as UE+import Network.URI.Slug (Slug)+import Network.URI.Slug qualified as Slug+import Relude+import Text.Megaparsec qualified as M+import Text.Pandoc.Builder qualified as B+import Text.Pandoc.Walk qualified as W+import Text.Parsec qualified as P+import Text.Read (Read (readsPrec))+import Text.Show qualified (Show (show))++-- | Represents the "Foo" in [[Foo]]+--+-- As wiki links may contain multiple path components, it can also represent+-- [[Foo/Bar]], hence we use nonempty slug list.+newtype WikiLink = WikiLink {unWikiLink :: NonEmpty Slug}+ deriving stock (Eq, Ord, Typeable, Data)++instance ToJSON WikiLink where+ toJSON = toJSON . wikilinkUrl++instance Show WikiLink where+ show wl =+ toString $ "[[" <> wikilinkUrl wl <> "]]"++-- -----------------+-- Making wiki links+-- -----------------++mkWikiLinkFromRoute :: R ext -> WikiLink+mkWikiLinkFromRoute (R slugs) = WikiLink slugs++mkWikiLinkFromUrl :: (Monad m, Alternative m) => Text -> m WikiLink+mkWikiLinkFromUrl s = do+ slugs <- maybe empty pure $ nonEmpty $ Slug.decodeSlug <$> T.splitOn "/" s+ pure $ WikiLink slugs++mkWikiLinkFromInline :: B.Inline -> Maybe (WikiLink, [B.Inline])+mkWikiLinkFromInline inl = do+ B.Link (_id, _class, otherAttrs) is (url, tit) <- pure inl+ (Left (_, wl), _manchor) <- delineateLink (otherAttrs <> one ("title", tit)) url+ pure (wl, is)++-- | An URL anchor without the '#'+newtype Anchor = Anchor Text+ deriving newtype (Eq, Show, Ord)++mkAnchor :: String -> Maybe Anchor+mkAnchor ('#' : name) = pure $ Anchor $ toText name+mkAnchor _ = Nothing++anchorSuffix :: Maybe Anchor -> Text+anchorSuffix = maybe "" (\(Anchor a) -> "#" <> a)++dropUrlAnchor :: Text -> (Text, Maybe Anchor)+dropUrlAnchor = second (mkAnchor . toString) . T.breakOn "#"++-- | Given a Pandoc Link node, apparaise what kind of link it is.+--+-- * Nothing, if the link is an absolute URL+-- * Just (Left wl), if a wiki-link+-- * Just (Right fp), if a relative path (not a wiki-link)+delineateLink :: [(Text, Text)] -> Text -> Maybe (Either (WikiLinkType, WikiLink) FilePath, Maybe Anchor)+delineateLink (Map.fromList -> attrs) url = do+ -- Must be relative+ guard $ not $ "://" `T.isInfixOf` url+ wikiLink <|> internalLink+ where+ wikiLink = do+ wlType :: WikiLinkType <- readMaybe . toString <=< Map.lookup htmlAttr $ attrs+ -- Ignore anchors until https://github.com/srid/emanote/discussions/105+ let (s, manc) = dropUrlAnchor url+ wl <- mkWikiLinkFromUrl s+ pure (Left (wlType, wl), manc)+ internalLink = do+ -- Avoid links like "mailto:", "magnet:", etc.+ -- An easy way to parse them is to look for colon character.+ --+ -- This does mean that "Foo: Bar.md" cannot be linked to this way, however+ -- the user can do it using wiki-links.+ guard $ not $ ":" `T.isInfixOf` url+ let (s, manc) = dropUrlAnchor url+ guard $ not $ T.null s -- Same page anchors+ pure (Right $ UE.decode (toString s), manc)++-- ---------------------+-- Converting wiki links+-- ---------------------++-- | [[Foo/Bar]] -> "Foo/Bar"+wikilinkUrl :: WikiLink -> Text+wikilinkUrl =+ T.intercalate "/" . fmap Slug.unSlug . toList . unWikiLink++wikilinkInline :: WikiLinkType -> WikiLink -> B.Inlines -> B.Inlines+wikilinkInline typ wl = B.linkWith attrs (wikilinkUrl wl) ""+ where+ attrs = ("", [], [(htmlAttr, show typ)])++wikiLinkInlineRendered :: B.Inline -> Maybe Text+wikiLinkInlineRendered x = do+ (wl, inl) <- mkWikiLinkFromInline x+ pure $ case nonEmpty inl of+ Nothing -> show wl+ Just _ ->+ let inlStr = plainify inl+ in if inlStr == wikilinkUrl wl+ then show wl+ else "[[" <> wikilinkUrl wl <> "|" <> plainify inl <> "]]"++-- | Return the various ways to link to a route (ignoring ext)+--+-- Foo/Bar/Qux.md -> [[Qux]], [[Bar/Qux]], [[Foo/Bar/Qux]]+--+-- All possible combinations of Wikilink type use is automatically included.+allowedWikiLinks :: HasCallStack => R @Ext.SourceExt ext -> NonEmpty (WikiLinkType, WikiLink)+allowedWikiLinks r =+ let wls = fmap WikiLink $ tailsNE $ unRoute r+ typs :: NonEmpty WikiLinkType = NE.fromList universe+ in liftM2 (,) typs wls+ where+ tailsNE =+ NE.fromList . mapMaybe nonEmpty . tails . toList++-------------------------+-- Parser+--------------------------++-- | A # prefix or suffix allows semantically distinct wikilinks+--+-- Typically called branching link or a tag link, when used with #.+data WikiLinkType+ = -- | [[Foo]]+ WikiLinkNormal+ | -- | [[Foo]]#+ WikiLinkBranch+ | -- | #[[Foo]]+ WikiLinkTag+ | -- | ![[Foo]]+ WikiLinkEmbed+ deriving stock (Eq, Show, Ord, Typeable, Data, Enum, Bounded, Generic)+ deriving anyclass (ToJSON)++instance Read WikiLinkType where+ readsPrec _ s+ | s == show WikiLinkNormal = [(WikiLinkNormal, "")]+ | s == show WikiLinkBranch = [(WikiLinkBranch, "")]+ | s == show WikiLinkTag = [(WikiLinkTag, "")]+ | s == show WikiLinkEmbed = [(WikiLinkEmbed, "")]+ | otherwise = []++-- | The HTML 'data attribute' storing the wiki-link type.+htmlAttr :: Text+htmlAttr = "data-wikilink-type"++class HasWikiLink il where+ wikilink :: WikiLinkType -> WikiLink -> Maybe il -> il++instance HasWikiLink (CP.Cm b B.Inlines) where+ wikilink typ wl il =+ CP.Cm $ wikilinkInline typ wl $ maybe mempty CP.unCm il++-- | Like `Commonmark.Extensions.Wikilinks.wikilinkSpec` but Zettelkasten-friendly.+--+-- Compared with the official extension, this has two differences:+--+-- - Supports flipped inner text, eg: `[[Foo | some inner text]]`+-- - Supports neuron folgezettel, i.e.: #[[Foo]] or [[Foo]]#+wikilinkSpec ::+ (Monad m, CM.IsInline il, HasWikiLink il) =>+ CM.SyntaxSpec m il bl+wikilinkSpec =+ mempty+ { CM.syntaxInlineParsers =+ [ P.try $+ P.choice+ [ P.try (CT.symbol '#' *> pWikilink WikiLinkTag),+ P.try (CT.symbol '!' *> pWikilink WikiLinkEmbed),+ P.try (pWikilink WikiLinkBranch <* CT.symbol '#'),+ P.try (pWikilink WikiLinkNormal)+ ]+ ]+ }+ where+ pWikilink typ = do+ replicateM_ 2 $ CT.symbol '['+ P.notFollowedBy (CT.symbol '[')+ url <-+ CM.untokenize <$> many (satisfyNoneOf [isPipe, isAnchor, isClose])+ wl <- mkWikiLinkFromUrl url+ -- We ignore the anchor until https://github.com/srid/emanote/discussions/105+ _anchor <-+ M.optional $+ CM.untokenize+ <$> ( CT.symbol '#'+ *> many (satisfyNoneOf [isPipe, isClose])+ )+ title <-+ M.optional $+ -- TODO: Should parse as inline so link text can be formatted?+ CM.untokenize+ <$> ( CT.symbol '|'+ *> many (satisfyNoneOf [isClose])+ )+ replicateM_ 2 $ CT.symbol ']'+ return $ wikilink typ wl (fmap CM.str title)+ satisfyNoneOf toks =+ CT.satisfyTok $ \t -> not $ or $ toks <&> \tok -> tok t+ isAnchor =+ isSymbol '#'+ isPipe =+ isSymbol '|'+ isClose =+ isSymbol ']'+ isSymbol c =+ CT.hasType (CM.Symbol c)++-- | Convert Pandoc AST inlines to raw text.+--+-- TODO: extend on top of plainify from heist-extra+plainify :: [B.Inline] -> Text+plainify = W.query $ \case+ B.Str x -> x+ B.Code _attr x -> x+ B.Space -> " "+ B.SoftBreak -> " "+ B.LineBreak -> " "+ -- TODO: if fmt is html, we should strip the html tags+ B.RawInline _fmt s -> s+ -- Ignore "wrapper" inlines like span.+ B.Span _ _ -> ""+ -- TODO: How to wrap math stuff here?+ B.Math _mathTyp s -> s+ -- Wiki-links must be displayed using its show instance (which returns its+ -- human-readable representation)+ (mkWikiLinkFromInline -> Just (wl, customText)) ->+ if null customText+ then show wl -- We will display raw wikilink; ideally, though, we want to display the title.+ else plainify customText+ -- Ignore the rest of AST nodes, as they are recursively defined in terms of+ -- `Inline` which `W.query` will traverse again.+ _ -> ""
@@ -0,0 +1,77 @@+{-# LANGUAGE RecordWildCards #-}++-- | Types for custom render extensions to Pandoc AST nodes.+--+-- Note that unlike Pandoc *filters* (which operate on entire document), these+-- are modeled based on Text.Pandoc.Walk, ie. fine-grained on individual inline+-- and block processing. We do this only so as to render a specific node during+-- recursion (cf. `rpBlock` and `rpInline` in Render.hs).+--+-- So we expect the extensions to be in Haskell, however external script may be+-- supported using a traditional whole-AST extension API.+module Emanote.Pandoc.Renderer+ ( PandocRenderers (PandocRenderers),+ PandocInlineRenderer,+ PandocBlockRenderer,+ mkRenderCtxWithPandocRenderers,+ EmanotePandocRenderers (..),+ )+where++import Heist (HeistT)+import Heist.Extra.Splices.Pandoc qualified as Splices+import Heist.Extra.Splices.Pandoc.Ctx qualified as Splices+import Heist.Interpreted qualified as HI+import Relude+import Text.Pandoc.Definition qualified as B++-- | Custom Heist renderer function for specific Pandoc AST nodes+type PandocRenderF model route astNode =+ model ->+ PandocRenderers model route ->+ Splices.RenderCtx ->+ route ->+ astNode ->+ Maybe (HI.Splice Identity)++type PandocInlineRenderer model route = PandocRenderF model route B.Inline++type PandocBlockRenderer model route = PandocRenderF model route B.Block++data PandocRenderers model route = PandocRenderers+ { pandocInlineRenderers :: [PandocInlineRenderer model route],+ pandocBlockRenderers :: [PandocBlockRenderer model route]+ }++mkRenderCtxWithPandocRenderers ::+ forall model route m.+ (Monad m) =>+ PandocRenderers model route ->+ Map Text Text ->+ model ->+ route ->+ HeistT Identity m Splices.RenderCtx+mkRenderCtxWithPandocRenderers nr@PandocRenderers {..} classRules model x =+ Splices.mkRenderCtx+ classRules+ ( \ctx blk ->+ asum $+ pandocBlockRenderers <&> \f ->+ f model nr ctx x blk+ )+ ( \ctx blk ->+ asum $+ pandocInlineRenderers <&> \f ->+ f model nr ctx x blk+ )++data EmanotePandocRenderers a r = EmanotePandocRenderers+ { blockRenderers :: PandocRenderers a r,+ -- | Like `blockRenderers` but for use in inline contexts.+ --+ -- Backlinks and titles constitute an example of inline context, where we don't+ -- care about block elements.+ inlineRenderers :: PandocRenderers a r,+ -- | Like `inlineRenderers` but suitable for use inside links (<a> tags).+ linkInlineRenderers :: PandocRenderers a r+ }
@@ -0,0 +1,110 @@+module Emanote.Pandoc.Renderer.Embed where++import Data.Map.Syntax ((##))+import Data.Text qualified as T+import Emanote.Model (Model)+import Emanote.Model.Link.Rel qualified as Rel+import Emanote.Model.Link.Resolve qualified as Resolve+import Emanote.Model.Note qualified as MN+import Emanote.Model.StaticFile qualified as SF+import Emanote.Model.Title qualified as Tit+import Emanote.Pandoc.BuiltinFilters (prepareNoteDoc, preparePandoc)+import Emanote.Pandoc.Link qualified as Link+import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Emanote.Pandoc.Renderer (PandocBlockRenderer, PandocInlineRenderer)+import Emanote.Pandoc.Renderer.Url qualified as RenderedUrl+import Emanote.Route.ModelRoute qualified as R+import Emanote.Route.R qualified as R+import Emanote.Route.SiteRoute qualified as SF+import Emanote.Route.SiteRoute qualified as SR+import Heist qualified as H+import Heist.Extra qualified as HE+import Heist.Extra.Splices.Pandoc (pandocSplice)+import Heist.Extra.Splices.Pandoc qualified as HP+import Heist.Interpreted qualified as HI+import Optics.Operators ((^.))+import Relude+import Text.Pandoc.Definition qualified as B++embedBlockWikiLinkResolvingSplice :: PandocBlockRenderer Model R.LMLRoute+embedBlockWikiLinkResolvingSplice model _nf ctx noteRoute node = do+ B.Para [inl] <- pure node+ (inlRef, (_, _, otherAttrs), is, (url, tit)) <- Link.parseInlineRef inl+ guard $ inlRef == Link.InlineLink+ let parentR = R.withLmlRoute R.routeParent noteRoute+ -- TODO: Use anchor to embed a section?+ (Rel.URTWikiLink (WL.WikiLinkEmbed, wl), _mAnchor) <-+ Rel.parseUnresolvedRelTarget parentR (otherAttrs <> one ("title", tit)) url+ let rRel = Resolve.resolveWikiLinkMustExist model wl+ RenderedUrl.renderSomeInlineRefWith Resolve.resourceSiteRoute (is, (url, tit)) rRel model ctx inl $+ either (embedResourceRoute model ctx) (const Nothing)++embedBlockRegularLinkResolvingSplice :: PandocBlockRenderer Model R.LMLRoute+embedBlockRegularLinkResolvingSplice model _nf ctx noteRoute node = do+ B.Para [inl] <- pure node+ (inlRef, (_, _, otherAttrs), is, (url, tit)) <- Link.parseInlineRef inl+ guard $ inlRef == Link.InlineImage+ let parentR = R.withLmlRoute R.routeParent noteRoute+ (Rel.URTResource mr, _mAnchor) <-+ Rel.parseUnresolvedRelTarget parentR (otherAttrs <> one ("title", tit)) url+ let rRel = Resolve.resolveModelRoute model mr+ RenderedUrl.renderSomeInlineRefWith Resolve.resourceSiteRoute (is, (url, tit)) rRel model ctx inl $+ either (const Nothing) (embedStaticFileRoute model $ WL.plainify is)++embedInlineWikiLinkResolvingSplice :: PandocInlineRenderer Model R.LMLRoute+embedInlineWikiLinkResolvingSplice model _nf ctx noteRoute inl = do+ (inlRef, (_, _, otherAttrs), is, (url, tit)) <- Link.parseInlineRef inl+ guard $ inlRef == Link.InlineLink+ let parentR = R.withLmlRoute R.routeParent noteRoute+ (Rel.URTWikiLink (WL.WikiLinkEmbed, wl), _mAnchor) <- Rel.parseUnresolvedRelTarget parentR (otherAttrs <> one ("title", tit)) url+ let rRel = Resolve.resolveWikiLinkMustExist model wl+ RenderedUrl.renderSomeInlineRefWith Resolve.resourceSiteRoute (is, (url, tit)) rRel model ctx inl $+ either (const Nothing) (embedStaticFileRoute model $ show wl)++runEmbedTemplate :: ByteString -> H.Splices (HI.Splice Identity) -> HI.Splice Identity+runEmbedTemplate name splices = do+ tpl <- HE.lookupHtmlTemplateMust $ "/templates/filters/embed-" <> name+ HE.runCustomTemplate tpl splices++embedResourceRoute :: Model -> HP.RenderCtx -> MN.Note -> Maybe (HI.Splice Identity)+embedResourceRoute model ctx note = do+ pure . runEmbedTemplate "note" $ do+ "ema:note:title" ## Tit.titleSplice ctx preparePandoc (MN._noteTitle note)+ "ema:note:url" ## HI.textSplice (SR.siteRouteUrl model $ SR.lmlSiteRoute $ note ^. MN.noteRoute)+ "ema:note:pandoc" ##+ pandocSplice ctx (prepareNoteDoc note)++embedStaticFileRoute :: Model -> Text -> SF.StaticFile -> Maybe (HI.Splice Identity)+embedStaticFileRoute model altText staticFile = do+ let fp = staticFile ^. SF.staticFilePath+ url = SF.siteRouteUrl model $ SF.staticFileSiteRoute staticFile+ if+ | any (`T.isSuffixOf` toText fp) imageExts ->+ pure . runEmbedTemplate "image" $ do+ "ema:url" ## HI.textSplice url+ "ema:alt" ## HI.textSplice altText+ | any (`T.isSuffixOf` toText fp) videoExts -> do+ pure . runEmbedTemplate "video" $ do+ "ema:url" ## HI.textSplice url+ | ".pdf" `T.isSuffixOf` toText fp -> do+ pure . runEmbedTemplate "pdf" $ do+ "ema:url" ## HI.textSplice url+ | otherwise -> Nothing++imageExts :: [Text]+imageExts =+ [ ".jpg",+ ".jpeg",+ ".png",+ ".svg",+ ".gif",+ ".bmp",+ ".webp"+ ]++videoExts :: [Text]+videoExts =+ [ ".mp4",+ ".webm",+ ".ogv"+ ]
@@ -0,0 +1,53 @@+module Emanote.Pandoc.Renderer.Query+ ( queryResolvingSplice,+ noteSpliceMap,+ )+where++import Data.List qualified as List+import Data.Map.Syntax ((##))+import Data.Text qualified as T+import Emanote.Model (Model)+import Emanote.Model.Note qualified as MN+import Emanote.Model.Query qualified as Q+import Emanote.Model.Title qualified as Tit+import Emanote.Pandoc.BuiltinFilters (preparePandoc)+import Emanote.Pandoc.Renderer (PandocBlockRenderer)+import Emanote.Route (LMLRoute)+import Emanote.Route.SiteRoute qualified as SR+import Heist qualified as H+import Heist.Extra qualified as HE+import Heist.Extra.Splices.Pandoc (RenderCtx)+import Heist.Interpreted qualified as HI+import Heist.Splices.Json qualified as HJ+import Optics.Operators ((^.))+import Relude+import Text.Pandoc.Definition qualified as B++queryResolvingSplice :: PandocBlockRenderer Model LMLRoute+queryResolvingSplice model _nr ctx noteRoute blk = do+ B.CodeBlock+ (_id', classes, _attrs)+ (Q.parseQuery -> Just q) <-+ pure blk+ guard $ List.elem "query" classes+ let mOtherCls = nonEmpty (List.delete "query" classes) <&> T.intercalate " " . toList+ queryTpl = encodeUtf8 $ "/templates/filters/query-" <> fromMaybe "default" mOtherCls+ pure $ do+ tpl <- HE.lookupHtmlTemplateMust queryTpl+ HE.runCustomTemplate tpl $ do+ "query" ##+ HI.textSplice (show q)+ "result" ##+ (HI.runChildrenWith . noteSpliceMap ($ ctx) model) `foldMapM` Q.runQuery noteRoute model q++-- TODO: Reuse this elsewhere+noteSpliceMap ::+ ((RenderCtx -> HI.Splice Identity) -> HI.Splice Identity) ->+ Model ->+ MN.Note ->+ H.Splices (HI.Splice Identity)+noteSpliceMap withCtx model note = do+ "ema:note:title" ## withCtx $ \ctx -> Tit.titleSplice ctx preparePandoc (MN._noteTitle note)+ "ema:note:url" ## HI.textSplice (SR.siteRouteUrl model $ SR.lmlSiteRoute $ note ^. MN.noteRoute)+ "ema:note:metadata" ## HJ.bindJson (note ^. MN.noteMeta)
@@ -0,0 +1,166 @@+module Emanote.Pandoc.Renderer.Url+ ( urlResolvingSplice,+ plainifyWikiLinkSplice,+ renderSomeInlineRefWith,+ )+where++import Data.Text qualified as T+import Emanote.Model (Model)+import Emanote.Model qualified as M+import Emanote.Model.Link.Rel qualified as Rel+import Emanote.Model.Link.Resolve qualified as Resolve+import Emanote.Model.Note qualified as MN+import Emanote.Model.Title qualified as Tit+import Emanote.Pandoc.Link qualified as Link+import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Emanote.Pandoc.Renderer (PandocInlineRenderer)+import Emanote.Route qualified as R+import Emanote.Route.SiteRoute qualified as SR+import Heist.Extra.Splices.Pandoc qualified as HP+import Heist.Extra.Splices.Pandoc qualified as Splices+import Heist.Extra.Splices.Pandoc.Ctx (ctxSansCustomSplicing)+import Heist.Interpreted qualified as HI+import Optics.Core (review)+import Relude+import Text.Pandoc.Definition qualified as B+import Text.Pandoc.Walk qualified as W++-- | Resolve all URLs in inlines (<a> and <img>)+urlResolvingSplice :: PandocInlineRenderer Model R.LMLRoute+urlResolvingSplice model _nf (ctxSansCustomSplicing -> ctx) noteRoute inl = do+ (inlRef, attr@(id', cls, otherAttrs), is, (url, tit)) <- Link.parseInlineRef inl+ let parentR = R.withLmlRoute R.routeParent noteRoute+ (uRel, mAnchor) <- Rel.parseUnresolvedRelTarget parentR (otherAttrs <> one ("title", tit)) url+ let rRel = Resolve.resolveUnresolvedRelTarget model uRel+ renderSomeInlineRefWith id (is, (url, tit)) rRel model ctx inl $ \sr ->+ case inlRef of+ Link.InlineLink -> do+ -- TODO: If uRel is `Rel.URTWikiLink (WL.WikiLinkEmbed, _)`, *and* it appears+ -- in B.Para (so do this in block-level custom splice), then embed it.+ -- We don't do this here, as this inline splice can't embed block elements.+ let (newIs, (newUrl', isNotEmaLink)) = replaceLinkNodeWithRoute model sr (is, url)+ newOtherAttrs = otherAttrs <> [openInNewTabAttr | M.inLiveServer model && isNotEmaLink]+ newAttr = (id', cls, newOtherAttrs)+ newUrl = newUrl' <> WL.anchorSuffix mAnchor+ pure $ HP.rpInline ctx $ B.Link newAttr newIs (newUrl, tit)+ Link.InlineImage -> do+ let (newIs, (newUrl, _)) =+ replaceLinkNodeWithRoute model sr (toList $ nonEmptyInlines url is, url)+ pure $ HP.rpInline ctx $ B.Image attr newIs (newUrl, tit)++openInNewTabAttr :: (Text, Text)+openInNewTabAttr =+ ("target", "_blank")++renderSomeInlineRefWith ::+ (a -> SR.SiteRoute) ->+ -- | AST Node attributes of @InlineRef@+ ([B.Inline], (Text, Text)) ->+ Rel.ResolvedRelTarget a ->+ Model ->+ Splices.RenderCtx ->+ B.Inline ->+ (a -> Maybe (HI.Splice Identity)) ->+ Maybe (HI.Splice Identity)+renderSomeInlineRefWith getSr (is, (url, tit)) rRel model (ctxSansCustomSplicing -> ctx) origInl f = do+ case rRel of+ Rel.RRTMissing -> do+ pure $ do+ raw <-+ HP.rpInline+ ctx+ ( tooltip+ "Link is broken"+ [ B.Strikeout $ one $ B.Str $ Link.unParseLink origInl,+ B.Str "❌"+ ]+ )+ details <-+ HP.rpInline ctx $+ -- FIXME: This aside is meaningless for non-wikilink links (regular+ -- Markdown links)+ B.Span ("", ["emanote:error:aside"], []) $+ one $+ tooltip "Find notes containing this broken link" $+ one $+ B.Link B.nullAttr (one $ B.Emph $ one $ B.Str "backlinks") (url, "")+ if M.inLiveServer model+ then pure $ raw <> details+ else pure raw+ Rel.RRTAmbiguous srs -> do+ pure $ do+ raw <- HP.rpInline ctx (tooltip "Link is ambiguous" [B.Strikeout $ one $ B.Str $ Link.unParseLink origInl, B.Str "❗"])+ candidates <-+ fmap mconcat . sequence $+ toList srs+ <&> \(getSr -> sr) -> do+ let (rp, _) = M.withoutRoutePrism model+ srRoute = toText $ review rp sr+ (_newIs, (newUrl, isNotEmaLink)) = replaceLinkNodeWithRoute model sr (is, srRoute)+ linkAttr = [openInNewTabAttr | M.inLiveServer model && isNotEmaLink]+ newIs = one $ B.Str $ show sr+ HP.rpInline ctx $+ B.Span ("", ["emanote:error:aside"], []) $+ one $+ tooltip (show sr <> " -> " <> srRoute) $+ one $+ B.Link ("", mempty, linkAttr) newIs (newUrl, tit)+ if M.inLiveServer model+ then pure $ raw <> candidates+ else pure raw+ Rel.RRTFound sr -> do+ f sr+ where+ tooltip :: Text -> [B.Inline] -> B.Inline+ tooltip s = B.Span ("", [], one ("title", s))++plainifyWikiLinkSplice :: PandocInlineRenderer Model R.LMLRoute+plainifyWikiLinkSplice _model _nf (ctxSansCustomSplicing -> ctx) _ inl = do+ s <- WL.wikiLinkInlineRendered inl+ pure $ HP.rpInline ctx $ B.Str s++inlinesWithWikiLinksPlainified :: [B.Inline] -> [B.Inline]+inlinesWithWikiLinksPlainified = W.walk $ \case+ (WL.wikiLinkInlineRendered -> Just s) ->+ B.Str s+ x -> x++replaceLinkNodeWithRoute ::+ HasCallStack =>+ Model ->+ SR.SiteRoute ->+ ([B.Inline], Text) ->+ ([B.Inline], (Text, Bool))+replaceLinkNodeWithRoute model r (inner, url) =+ ( inlinesWithWikiLinksPlainified $ nonEmptyLinkInlines model url (Just r) inner,+ let linkUrl = SR.siteRouteUrl model r+ in (linkUrl, "?" `T.isInfixOf` linkUrl)+ )+ where+ nonEmptyLinkInlines :: Model -> Text -> Maybe SR.SiteRoute -> [B.Inline] -> [B.Inline]+ nonEmptyLinkInlines model' url' mr = \case+ [] ->+ toList $+ nonEmptyInlines url $+ fromMaybe [] $+ siteRouteDefaultInnerText model' url' =<< mr+ x -> x++-- | Ensure that inlines list is non-empty, using the provided singleton value if necessary.+nonEmptyInlines :: Text -> [B.Inline] -> NonEmpty B.Inline+nonEmptyInlines x =+ fromMaybe (one $ B.Str x) . nonEmpty++siteRouteDefaultInnerText :: Model -> Text -> SR.SiteRoute -> Maybe [B.Inline]+siteRouteDefaultInnerText model url = \case+ SR.SiteRoute_MissingR _ -> Nothing+ SR.SiteRoute_AmbiguousR _ _ -> Nothing+ SR.SiteRoute_VirtualRoute _ -> Nothing+ SR.SiteRoute_ResourceRoute resR ->+ case resR of+ SR.ResourceRoute_LML lmlR ->+ Tit.toInlines . MN._noteTitle <$> M.modelLookupNoteByRoute lmlR model+ SR.ResourceRoute_StaticFile _ _ ->+ -- Just append a file: prefix, to existing wiki-link.+ pure $ B.Str "File:" : [B.Str url]
@@ -0,0 +1,40 @@+{-# LANGUAGE DeriveAnyClass #-}++-- | We use `relude` as our prelude; any extra Prelude-like functionality is put+-- here.+module Emanote.Prelude where++import Control.Monad.Logger (MonadLogger, logDebugNS, logErrorNS, logInfoNS, logWarnNS)+import Relude++-- | Monadic version of `chain`+chainM :: Monad m => (b -> m (a -> a)) -> [b] -> m (a -> a)+chainM f =+ fmap chain . mapM f+ where+ -- Apply the list of actions in the given order to an initial argument.+ --+ -- chain [f1, f2, ...] a = ... (f2 (f1 x))+ chain :: [a -> a] -> a -> a+ chain = flip $ flipfoldl' ($)++-- | User-provided input is malformed.+newtype BadInput = BadInput Text+ deriving stock (Show)+ deriving anyclass (Exception)++-------------+-- Logging+--------------++log :: MonadLogger m => Text -> m ()+log = logInfoNS "emanote"++logD :: MonadLogger m => Text -> m ()+logD = logDebugNS "emanote"++logE :: MonadLogger m => Text -> m ()+logE = logErrorNS "emanote"++logW :: MonadLogger m => Text -> m ()+logW = logWarnNS "emanote"
@@ -0,0 +1,5 @@+module Emanote.Route (module X) where++import Emanote.Route.Ext as X+import Emanote.Route.ModelRoute as X+import Emanote.Route.R as X
@@ -0,0 +1,92 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DeriveAnyClass #-}++module Emanote.Route.Ext where++import Data.Aeson (ToJSON)+import Data.Data (Data)+import Relude hiding (show)+import System.FilePath qualified as FP++data SourceExt = SourceExt+ deriving stock (Eq, Ord, Show, Read, Data, Generic)+ deriving anyclass (ToJSON)++data FileType a where+ LMLType :: LML -> FileType SourceExt+ Yaml :: FileType SourceExt+ HeistTpl :: FileType SourceExt+ -- | `AnyExt` has no *known* (at compile time) extension. It is used as a+ -- "catch all" type to capture files using an arbitrary.+ AnyExt :: FileType SourceExt+ Html :: FileType ()+ Folder :: FileType ()+ deriving stock (Typeable)++deriving stock instance Eq a => Eq (FileType a)++deriving stock instance Ord a => Ord (FileType a)++-- | A lightweight markup language+--+-- https://en.wikipedia.org/wiki/Lightweight_markup_language+--+-- This type exists simply because we may support more formats (eg: org-mode) in+-- the future.+data LML = Md | Org+ deriving stock (Generic, Eq, Ord, Typeable, Data, Enum, Bounded)+ deriving anyclass (ToJSON)++-- | The `HasExt` class's responsibility is to allow dealing with basepath sans+-- extension (and vice-versa).+class HasExt (ext :: FileType a) where+ fileType :: FileType a++ -- | Return the filepath with the known extension.+ withExt :: FilePath -> FilePath++ -- | Return the filepath without the known extension.+ withoutKnownExt :: FilePath -> Maybe FilePath++instance HasExt ('LMLType 'Md) where+ fileType = LMLType Md+ withExt = flip FP.addExtension ".md"+ withoutKnownExt = fpWithoutExt ".md"++instance HasExt ('LMLType 'Org) where+ fileType = LMLType Org+ withExt = flip FP.addExtension ".org"+ withoutKnownExt = fpWithoutExt ".org"++instance HasExt 'Yaml where+ fileType = Yaml+ withExt = flip FP.addExtension ".yaml"+ withoutKnownExt = fpWithoutExt ".yaml"++instance HasExt 'Html where+ fileType = Html+ withExt = flip FP.addExtension ".html"+ withoutKnownExt = fpWithoutExt ".html"++instance HasExt 'HeistTpl where+ fileType = HeistTpl+ withExt = flip FP.addExtension ".tpl"+ withoutKnownExt = fpWithoutExt ".tpl"++instance HasExt 'Folder where+ fileType = Folder+ withExt = id+ withoutKnownExt = pure++-- | The AnyExt instance ignores explicitly dealing with extensions, expecting+-- the user to explicitly encode the extension in their value tpye.+instance HasExt 'AnyExt where+ fileType = AnyExt+ withExt = id+ withoutKnownExt = pure++fpWithoutExt :: (Monad m, Alternative m) => String -> FilePath -> m FilePath+fpWithoutExt e fp = do+ let (base, ext) = FP.splitExtension fp+ guard $ ext == e+ pure base
@@ -0,0 +1,95 @@+{-# LANGUAGE DeriveAnyClass #-}++-- | Route types representing the resources in our `Model`.+--+-- See also: `Emanote.Route.SiteRoute`.+module Emanote.Route.ModelRoute+ ( -- Some route in a generated site+ ModelRoute (..),+ modelRouteCase,+ mkModelRouteFromFilePath,+ -- Only LML routes+ LMLRoute (..),+ defaultLmlRoute,+ possibleLmlRoutes,+ lmlRouteCase,+ withLmlRoute,+ mkLMLRouteFromFilePath,+ mkLMLRouteFromKnownFilePath,+ isMdRoute,+ -- Static file routes+ StaticFileRoute,+ )+where++import Data.Aeson.Types (ToJSON)+import Emanote.Route.Ext (FileType (AnyExt, LMLType), HasExt, LML (Md, Org))+import Emanote.Route.R (R)+import Emanote.Route.R qualified as R+import Relude++type StaticFileRoute = R 'AnyExt++-- | A R to anywhere in `Model`+data ModelRoute+ = ModelRoute_StaticFile StaticFileRoute+ | ModelRoute_LML LMLRoute+ deriving stock (Eq, Show, Ord, Generic)+ deriving anyclass (ToJSON)++-- | R to a note file in LML (lightweight markup language) format+data LMLRoute+ = LMLRoute_Md (R ('LMLType 'Md))+ | LMLRoute_Org (R ('LMLType 'Org))+ deriving stock (Eq, Show, Ord, Generic)+ deriving anyclass (ToJSON)++defaultLmlRoute :: R (ext :: FileType a) -> LMLRoute+defaultLmlRoute =+ LMLRoute_Md . coerce++possibleLmlRoutes :: R (ext :: FileType a) -> [LMLRoute]+possibleLmlRoutes r =+ [ LMLRoute_Md (coerce r),+ LMLRoute_Org (coerce r)+ ]++lmlRouteCase ::+ LMLRoute ->+ Either (R ('LMLType 'Md)) (R ('LMLType 'Org))+lmlRouteCase = \case+ LMLRoute_Md r -> Left r+ LMLRoute_Org r -> Right r++isMdRoute :: LMLRoute -> Bool+isMdRoute = \case+ LMLRoute_Md _ -> True+ _ -> False++withLmlRoute :: (forall lmlType. HasExt ('LMLType lmlType) => R ('LMLType lmlType) -> r) -> LMLRoute -> r+withLmlRoute f = either f f . lmlRouteCase++modelRouteCase ::+ ModelRoute ->+ Either LMLRoute StaticFileRoute+modelRouteCase = \case+ ModelRoute_LML r -> Left r+ ModelRoute_StaticFile r -> Right r++mkModelRouteFromFilePath :: FilePath -> Maybe ModelRoute+mkModelRouteFromFilePath fp =+ fmap ModelRoute_LML (mkLMLRouteFromFilePath fp)+ <|> fmap ModelRoute_StaticFile (R.mkRouteFromFilePath fp)++mkLMLRouteFromFilePath :: FilePath -> Maybe LMLRoute+mkLMLRouteFromFilePath fp =+ mkLMLRouteFromKnownFilePath Md fp+ <|> mkLMLRouteFromKnownFilePath Org fp++-- | Like `mkLMLRouteFromFilePath`, but when the file extension is known ahead+-- to be of `lmlType`.+mkLMLRouteFromKnownFilePath :: LML -> FilePath -> Maybe LMLRoute+mkLMLRouteFromKnownFilePath lmlType fp =+ case lmlType of+ Md -> fmap LMLRoute_Md (R.mkRouteFromFilePath fp)+ Org -> fmap LMLRoute_Org (R.mkRouteFromFilePath fp)
@@ -0,0 +1,109 @@+module Emanote.Route.R where++import Data.Aeson (ToJSON (toJSON))+import Data.Data (Data)+import Data.List.NonEmpty qualified as NE+import Data.Text qualified as T+import Emanote.Route.Ext (FileType (..), HasExt (..))+import Network.URI.Slug (Slug)+import Network.URI.Slug qualified as Slug+import Relude+import System.FilePath (splitPath)+import Text.Show qualified (Show (show))++-- | Represents the relative path to some file (or its isomporphic URL+-- represetation).+newtype R (ext :: FileType a) = R {unRoute :: NonEmpty Slug}+ deriving stock (Eq, Ord, Typeable, Data)++instance HasExt ext => ToJSON (R ext) where+ toJSON = toJSON . encodeRoute++instance HasExt ext => Show (R ext) where+ show r =+ toString $+ "R[/" <> encodeRoute r <> "]"++-- | Convert foo/bar.<ext> to a @R@+mkRouteFromFilePath :: forall a (ext :: FileType a). HasExt ext => FilePath -> Maybe (R ext)+mkRouteFromFilePath fp = do+ base <- withoutKnownExt @_ @ext fp+ let slugs = fromString . toString . T.dropWhileEnd (== '/') . toText <$> splitPath base+ viaNonEmpty R slugs++mkRouteFromSlugs :: NonEmpty Slug -> R ext+mkRouteFromSlugs =+ R++-- | If the route is a single-slug URL, return the only slug.+routeSlug :: R ext -> Maybe Slug+routeSlug r = do+ x :| [] <- pure $ unRoute r+ pure x++-- | Like `routeSlug` but skips the given prefixes, returning the (only) pending slug.+routeSlugWithPrefix :: NonEmpty Slug -> R ext -> Maybe Slug+routeSlugWithPrefix prefix r = do+ lastSlug :| (nonEmpty -> Just prevSlugs) <- pure $ NE.reverse $ unRoute r+ guard $ NE.reverse prevSlugs == prefix+ pure lastSlug++-- | The base name of the route without its parent path.+routeBaseName :: R ext -> Text+routeBaseName =+ Slug.unSlug . head . NE.reverse . unRoute++routeParent :: R ext -> Maybe (R 'Folder)+routeParent =+ viaNonEmpty R . init . unRoute++-- | For use in breadcrumbs+routeInits :: R ext -> NonEmpty (R ext)+routeInits = \case+ (R ("index" :| [])) ->+ one indexRoute+ (R (slug :| rest')) ->+ indexRoute :| case nonEmpty rest' of+ Nothing ->+ one $ R (one slug)+ Just rest ->+ R (one slug) : go (one slug) rest+ where+ go :: NonEmpty Slug -> NonEmpty Slug -> [R ext]+ go x (y :| ys') =+ let this = R (x <> one y)+ in case nonEmpty ys' of+ Nothing ->+ one this+ Just ys ->+ this : go (unRoute this) ys++indexRoute :: R ext+indexRoute = R $ "index" :| []++-- | Convert a route to filepath+encodeRoute :: forall a (ft :: FileType a). HasExt ft => R ft -> FilePath+encodeRoute (R slugs) =+ let parts = Slug.unSlug <$> slugs+ in withExt @a @ft $ toString $ T.intercalate "/" (toList parts)++-- | Parse our route from html file path+decodeHtmlRoute :: FilePath -> R 'Html+decodeHtmlRoute fp = do+ let base = fromMaybe (toText fp) $ T.stripSuffix ".html" (toText fp)+ R $ case splitOnNE "/" base of+ Nothing ->+ one "index"+ Just parts ->+ fmap Slug.decodeSlug parts+ where+ -- Like `T.splitOn` but returns a NonEmpty list with sensible semantics+ splitOnNE k s =+ case T.splitOn k s of+ [] -> Nothing+ [""] -> Nothing+ x : xs -> Just $ x :| xs++decodeAnyRoute :: FilePath -> Maybe (R 'AnyExt)+decodeAnyRoute =+ mkRouteFromFilePath @_ @'AnyExt
@@ -0,0 +1,15 @@+module Emanote.Route.SiteRoute+ ( SiteRoute (..),+ VirtualRoute (..),+ ResourceRoute (..),+ decodeVirtualRoute,+ noteFileSiteRoute,+ staticFileSiteRoute,+ lmlSiteRoute,+ siteRouteUrl,+ siteRouteUrlStatic,+ )+where++import Emanote.Route.SiteRoute.Class+import Emanote.Route.SiteRoute.Type
@@ -0,0 +1,202 @@+module Emanote.Route.SiteRoute.Class+ ( decodeVirtualRoute,+ noteFileSiteRoute,+ staticFileSiteRoute,+ lmlSiteRoute,+ indexRoute,+ tagIndexRoute,+ taskIndexRoute,+ siteRouteUrl,+ siteRouteUrlStatic,+ urlStrategySuffix,++ -- * Ema stuff+ emanoteRouteEncoder,+ emanoteGeneratableRoutes,+ )+where++import Data.IxSet.Typed qualified as Ix+import Data.List.NonEmpty qualified as NE+import Data.Set qualified as Set+import Data.Time.Format (defaultTimeLocale, formatTime)+import Ema (UrlStrategy (..), routeUrlWith)+import Emanote.Model qualified as M+import Emanote.Model.Link.Rel qualified as Rel+import Emanote.Model.Meta qualified as Model+import Emanote.Model.Note qualified as N+import Emanote.Model.StaticFile qualified as SF+import Emanote.Model.Type (Model, ModelEma, ModelT)+import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as HT+import Emanote.Route qualified as R+import Emanote.Route.ModelRoute (LMLRoute, StaticFileRoute)+import Emanote.Route.SiteRoute.Type+import Emanote.View.LiveServerFiles qualified as LiveServerFile+import Optics.Core (Prism', prism')+import Optics.Operators ((^.))+import Relude++emanoteGeneratableRoutes :: ModelEma -> [SiteRoute]+emanoteGeneratableRoutes model =+ let htmlRoutes =+ model ^. M.modelNotes+ & Ix.toList+ <&> noteFileSiteRoute+ staticRoutes =+ let includeFile f =+ not (LiveServerFile.isLiveServerFile f)+ || (f == LiveServerFile.tailwindFullCssPath && not (model ^. M.modelCompileTailwind))+ in model ^. M.modelStaticFiles+ & Ix.toList+ & filter (includeFile . R.encodeRoute . SF._staticFileRoute)+ <&> staticFileSiteRoute+ virtualRoutes :: [VirtualRoute] =+ let tags = fst <$> M.modelTags model+ tagPaths =+ Set.fromList $+ ([] :) $ -- [] Triggers generation of main tag index.+ concat $+ tags <&> \(HT.deconstructTag -> tagPath) ->+ NE.filter (not . null) $ NE.inits tagPath+ in VirtualRoute_Index+ : VirtualRoute_Export+ : VirtualRoute_StorkIndex+ : VirtualRoute_TaskIndex+ : (VirtualRoute_TagIndex <$> toList tagPaths)+ in htmlRoutes+ <> staticRoutes+ <> fmap SiteRoute_VirtualRoute virtualRoutes++emanoteRouteEncoder :: HasCallStack => ModelEma -> Prism' FilePath SiteRoute+emanoteRouteEncoder model =+ prism' enc dec+ where+ enc = \case+ SiteRoute_MissingR s ->+ -- error $ toText $ "emanote: attempt to encode a 404 route: " <> s+ -- Unfortunately, since ema:multisite does isomorphism check of+ -- encoder, we can't just error out here.+ s+ SiteRoute_AmbiguousR fp _ ->+ -- FIXME: See note above.+ error $ "emanote: attempt to encode an ambiguous route: " <> toText fp+ SiteRoute_ResourceRoute r ->+ encodeResourceRoute model r+ SiteRoute_VirtualRoute r ->+ encodeVirtualRoute r++ dec fp =+ fmap SiteRoute_VirtualRoute (decodeVirtualRoute fp)+ <|> decodeGeneratedRoute model fp+ <|> pure (SiteRoute_MissingR fp)++encodeResourceRoute :: HasCallStack => ModelEma -> ResourceRoute -> FilePath+encodeResourceRoute model = \case+ ResourceRoute_LML r ->+ R.encodeRoute+ $+ -- HACK: This should never fail ... but *if* it does, consult+ -- https://github.com/srid/emanote/issues/148+ maybe+ -- FIXME: See note above.+ (error $ "emanote: attempt to encode missing note: " <> show r)+ N.noteHtmlRoute+ $ M.modelLookupNoteByRoute r model+ ResourceRoute_StaticFile r _fpAbs ->+ R.encodeRoute r++-- | Decode a route that is known to refer to a resource in the model+decodeGeneratedRoute :: ModelEma -> FilePath -> Maybe SiteRoute+decodeGeneratedRoute model fp =+ fmap+ staticFileSiteRoute+ (flip M.modelLookupStaticFileByRoute model =<< R.decodeAnyRoute fp)+ <|> noteHtmlSiteRoute+ (flip M.modelLookupNoteByHtmlRoute model $ R.decodeHtmlRoute fp)+ where+ noteHtmlSiteRoute :: Rel.ResolvedRelTarget N.Note -> Maybe SiteRoute+ noteHtmlSiteRoute = \case+ Rel.RRTMissing ->+ Nothing+ Rel.RRTFound note ->+ Just $ noteFileSiteRoute note+ Rel.RRTAmbiguous notes ->+ Just $ ambiguousNoteURLsRoute notes+ ambiguousNoteURLsRoute :: NonEmpty N.Note -> SiteRoute+ ambiguousNoteURLsRoute ns =+ SiteRoute_AmbiguousR ("/" <> fp) (N._noteRoute <$> ns)++noteFileSiteRoute :: N.Note -> SiteRoute+noteFileSiteRoute =+ lmlSiteRoute . N._noteRoute++lmlSiteRoute :: LMLRoute -> SiteRoute+lmlSiteRoute =+ SiteRoute_ResourceRoute . lmlResourceRoute++lmlResourceRoute :: LMLRoute -> ResourceRoute+lmlResourceRoute = ResourceRoute_LML++staticFileSiteRoute :: SF.StaticFile -> SiteRoute+staticFileSiteRoute =+ (SiteRoute_ResourceRoute . staticResourceRoute) . (SF._staticFileRoute &&& SF._staticFilePath)+ where+ staticResourceRoute :: (StaticFileRoute, FilePath) -> ResourceRoute+ staticResourceRoute = uncurry ResourceRoute_StaticFile++-- | Like `siteRouteUrl` but avoids any dynamism in the URL+siteRouteUrlStatic :: HasCallStack => Model -> SiteRoute -> Text+siteRouteUrlStatic model =+ Ema.routeUrlWith (urlStrategy model) rp+ where+ (rp, _) = M.withoutRoutePrism model++siteRouteUrl :: HasCallStack => Model -> SiteRoute -> Text+siteRouteUrl model sr =+ siteRouteUrlStatic model sr+ <> siteRouteQuery+ where+ siteRouteQuery =+ maybe "" (("?t=" <>) . toText . formatTime defaultTimeLocale "%s") staticFileModifiedTime+ staticFileModifiedTime = do+ -- In live server model, we append a ?t=.. to trigger the browser into+ -- reloading (or invalidating its cache of) this embed static file.+ guard $ M.inLiveServer model+ sfRoute <- staticFileRouteCase sr+ sf <- M.modelLookupStaticFileByRoute sfRoute model+ pure $ sf ^. SF.staticFileTime+ staticFileRouteCase :: SiteRoute -> Maybe StaticFileRoute+ staticFileRouteCase = \case+ SiteRoute_MissingR _fp ->+ Nothing+ SiteRoute_AmbiguousR _ _ ->+ Nothing+ SiteRoute_ResourceRoute rr ->+ case rr of+ ResourceRoute_StaticFile sfR _fp ->+ Just sfR+ ResourceRoute_LML _ ->+ Nothing+ SiteRoute_VirtualRoute _ -> Nothing++urlStrategySuffix :: Model -> Text+urlStrategySuffix model =+ case urlStrategy model of+ Ema.UrlDirect -> ".html"+ Ema.UrlPretty -> ""++urlStrategy :: ModelT f -> UrlStrategy+urlStrategy model =+ Model.lookupRouteMeta Ema.UrlDirect ("template" :| one "urlStrategy") (M.modelIndexRoute model) model++indexRoute :: SiteRoute+indexRoute =+ SiteRoute_VirtualRoute VirtualRoute_Index++tagIndexRoute :: [HT.TagNode] -> SiteRoute+tagIndexRoute =+ SiteRoute_VirtualRoute . VirtualRoute_TagIndex++taskIndexRoute :: SiteRoute+taskIndexRoute =+ SiteRoute_VirtualRoute VirtualRoute_TaskIndex
@@ -0,0 +1,114 @@+{-# LANGUAGE DeriveAnyClass #-}++module Emanote.Route.SiteRoute.Type+ ( SiteRoute (..),+ VirtualRoute (..),+ ResourceRoute (..),+ decodeVirtualRoute,+ encodeVirtualRoute,+ encodeTagIndexR,+ )+where++import Data.Aeson (ToJSON)+import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as HT+import Emanote.Route.Ext qualified as Ext+import Emanote.Route.ModelRoute (LMLRoute, StaticFileRoute, lmlRouteCase)+import Emanote.Route.R qualified as R+import Network.URI.Slug qualified as Slug+import Relude hiding (show)+import Text.Show (show)++-- | A route to a virtual resource (not in `Model`)+data VirtualRoute+ = VirtualRoute_Index+ | VirtualRoute_TagIndex [HT.TagNode]+ | VirtualRoute_Export+ | VirtualRoute_StorkIndex+ | VirtualRoute_TaskIndex+ deriving stock (Eq, Ord, Show, Generic)+ deriving anyclass (ToJSON)++-- | A route to a resource in `Model`+--+-- This is *mostly isomorphic* to `ModelRoute`, except for containing the+-- absolute path to the static file.+data ResourceRoute+ = ResourceRoute_StaticFile StaticFileRoute FilePath+ | ResourceRoute_LML LMLRoute+ deriving stock (Eq, Show, Ord, Generic)+ deriving anyclass (ToJSON)++data SiteRoute+ = SiteRoute_VirtualRoute VirtualRoute+ | SiteRoute_ResourceRoute ResourceRoute+ | SiteRoute_MissingR FilePath+ | SiteRoute_AmbiguousR FilePath (NonEmpty LMLRoute)+ deriving stock (Eq, Ord, Generic)++instance Show SiteRoute where+ show = \case+ SiteRoute_MissingR urlPath ->+ "404: " <> urlPath+ SiteRoute_AmbiguousR urlPath _notes ->+ "Amb: " <> urlPath+ SiteRoute_ResourceRoute rr ->+ case rr of+ ResourceRoute_StaticFile r _fp ->+ show r+ ResourceRoute_LML r ->+ show $ lmlRouteCase r+ SiteRoute_VirtualRoute x ->+ show x++decodeVirtualRoute :: FilePath -> Maybe VirtualRoute+decodeVirtualRoute fp =+ (VirtualRoute_Index <$ decodeIndexR fp)+ <|> (VirtualRoute_TagIndex <$> decodeTagIndexR fp)+ <|> (VirtualRoute_Export <$ decodeExportR fp)+ <|> (VirtualRoute_StorkIndex <$ decodeStorkIndexR fp)+ <|> (VirtualRoute_TaskIndex <$ decodeTaskIndexR fp)++decodeIndexR :: FilePath -> Maybe ()+decodeIndexR fp = do+ "-" :| ["all"] <- pure $ R.unRoute $ R.decodeHtmlRoute fp+ pass++decodeExportR :: FilePath -> Maybe ()+decodeExportR fp = do+ "-" :| ["export.json"] <- R.unRoute <$> R.decodeAnyRoute fp+ pass++decodeStorkIndexR :: FilePath -> Maybe ()+decodeStorkIndexR fp = do+ "-" :| ["stork.st"] <- R.unRoute <$> R.decodeAnyRoute fp+ pass++decodeTagIndexR :: FilePath -> Maybe [HT.TagNode]+decodeTagIndexR fp = do+ "-" :| "tags" : tagPath <- pure $ R.unRoute $ R.decodeHtmlRoute fp+ pure $ fmap (HT.TagNode . Slug.unSlug) tagPath++decodeTaskIndexR :: FilePath -> Maybe ()+decodeTaskIndexR fp = do+ "-" :| ["tasks"] <- pure $ R.unRoute $ R.decodeHtmlRoute fp+ pass++-- NOTE: The sentinel route slugs in this function should match with those of+-- the decoders above.+encodeVirtualRoute :: VirtualRoute -> FilePath+encodeVirtualRoute = \case+ VirtualRoute_TagIndex tagNodes ->+ R.encodeRoute $ encodeTagIndexR tagNodes+ VirtualRoute_Index ->+ R.encodeRoute $ R.R @() @'Ext.Html $ "-" :| ["all"]+ VirtualRoute_Export ->+ R.encodeRoute $ R.R @Ext.SourceExt @'Ext.AnyExt $ "-" :| ["export.json"]+ VirtualRoute_StorkIndex ->+ R.encodeRoute $ R.R @Ext.SourceExt @'Ext.AnyExt $ "-" :| ["stork.st"]+ VirtualRoute_TaskIndex ->+ R.encodeRoute $ R.R @() @'Ext.Html $ "-" :| ["tasks"]++encodeTagIndexR :: [HT.TagNode] -> R.R 'Ext.Html+encodeTagIndexR tagNodes =+ R.R $ "-" :| "tags" : fmap (fromString . toString . HT.unTagNode) tagNodes
@@ -0,0 +1,96 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++module Emanote.Source.Dynamic+ ( emanoteSiteInput,+ EmanoteConfig (..),+ emanoteCompileTailwind,+ emanoteConfigCli,+ emanoteConfigNoteFn,+ emanoteConfigPandocRenderers,+ )+where++import Control.Monad.Logger (MonadLogger, MonadLoggerIO)+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Some (Some)+import Data.UUID.V4 qualified as UUID+import Ema (Dynamic (..))+import Ema.CLI qualified+import Emanote.CLI qualified as CLI+import Emanote.Model.Note (Note)+import Emanote.Model.Stork.Index qualified as Stork+import Emanote.Model.Type qualified as Model+import Emanote.Pandoc.Renderer (EmanotePandocRenderers)+import Emanote.Prelude (chainM)+import Emanote.Route (LMLRoute)+import Emanote.Source.Loc (Loc)+import Emanote.Source.Loc qualified as Loc+import Emanote.Source.Patch qualified as Patch+import Emanote.Source.Pattern qualified as Pattern+import Optics.TH (makeLenses)+import Paths_emanote qualified+import Relude+import System.UnionMount qualified as UM+import UnliftIO (MonadUnliftIO)++-- | Everything that's required to run an Emanote site.+data EmanoteConfig = EmanoteConfig+ { -- | CLI arguments (includes layers)+ _emanoteConfigCli :: CLI.Cli,+ -- | A function to filter the `Note` before it gets added to the model.+ _emanoteConfigNoteFn :: Note -> Note,+ -- | How to render Pandoc to Heist HTML.+ _emanoteConfigPandocRenderers :: EmanotePandocRenderers Model.Model LMLRoute,+ -- | Whether to replace Tailwind2 CDN with a minimized Tailwind3 CSS file.+ _emanoteCompileTailwind :: Bool+ }++-- | Make an Ema `Dynamic` for the Emanote model.+--+-- The bulk of logic for building the Dynamic is in `Patch.hs`.+emanoteSiteInput :: (MonadUnliftIO m, MonadLoggerIO m) => Some Ema.CLI.Action -> EmanoteConfig -> m (Dynamic m Model.ModelEma)+emanoteSiteInput cliAct EmanoteConfig {..} = do+ defaultLayer <- Loc.defaultLayer <$> liftIO Paths_emanote.getDataDir+ instanceId <- liftIO UUID.nextRandom+ storkIndex <- Stork.newIndex+ let layers = Loc.userLayers (CLI.layers _emanoteConfigCli) <> one defaultLayer+ initialModel = Model.emptyModel layers cliAct _emanoteConfigPandocRenderers _emanoteCompileTailwind instanceId storkIndex+ Dynamic+ <$> UM.unionMount+ (layers & Set.map (id &&& Loc.locPath))+ Pattern.filePatterns+ Pattern.ignorePatterns+ initialModel+ (mapFsChanges $ Patch.patchModel layers _emanoteConfigNoteFn storkIndex)++type ChangeHandler tag model m =+ tag ->+ FilePath ->+ UM.FileAction (NonEmpty (Loc, FilePath)) ->+ m (model -> model)++mapFsChanges :: (MonadIO m, MonadLogger m) => ChangeHandler tag model m -> UM.Change Loc tag -> m (model -> model)+mapFsChanges h ch = do+ uncurry (mapFsChangesOnExt h) `chainM` Map.toList ch+ where+ -- Temporarily use block buffering before calling an IO action that is+ -- known ahead to log rapidly, so as to not hamper serial processing speed.+ -- FIXME: This buffers warnings and errors (when parsing .md file) without+ -- dumping them to console. So disabling for now. But we need a proper fix.+ _withBlockBuffering f =+ (hSetBuffering stdout (BlockBuffering Nothing) >> hSetBuffering stderr LineBuffering)+ *> f+ <* (hFlush stdout >> hFlush stderr >> hSetBuffering stdout LineBuffering)++mapFsChangesOnExt ::+ (MonadIO m, MonadLogger m) =>+ ChangeHandler tag model m ->+ tag ->+ Map FilePath (UM.FileAction (NonEmpty (Loc, FilePath))) ->+ m (model -> model)+mapFsChangesOnExt h fpType fps = do+ uncurry (h fpType) `chainM` Map.toList fps++makeLenses ''EmanoteConfig
@@ -0,0 +1,62 @@+-- | Notebook location+module Emanote.Source.Loc+ ( -- * Type+ Loc (..),++ -- * Making a `Loc`+ defaultLayer,+ userLayers,++ -- * Using a `Loc`+ locResolve,+ locPath,++ -- * Dealing with layers of locs+ LocLayers,+ primaryLayer,+ )+where++import Data.Set qualified as Set+import Relude+import System.FilePath ((</>))++-- | Location of the notebook+--+-- The order here matters. Top = higher precedence.+data Loc+ = -- | The Int argument specifies the precedence (lower value = higher precedence)+ LocUser Int FilePath+ | -- | The default location (ie., emanote default layer)+ LocDefault FilePath+ deriving stock (Eq, Ord, Show)++type LocLayers = Set Loc++-- | Return the "primary" `LocUser` layer (that which are not overrides).+--+-- Assumes that the user has put it always by last; i.e, `-L foo;primary/layer`.+primaryLayer :: HasCallStack => LocLayers -> Loc+primaryLayer =+ Set.findMax . Set.filter isUserLayer+ where+ isUserLayer = \case+ LocUser _ _ -> True+ _ -> False++defaultLayer :: FilePath -> Loc+defaultLayer = LocDefault++userLayers :: NonEmpty FilePath -> Set Loc+userLayers paths =+ fromList $+ zip [1 ..] (toList paths) <&> uncurry LocUser++-- | Return the effective path of a file.+locResolve :: (Loc, FilePath) -> FilePath+locResolve (loc, fp) = locPath loc </> fp++locPath :: Loc -> FilePath+locPath = \case+ LocUser _ fp -> fp+ LocDefault fp -> fp
@@ -0,0 +1,184 @@+-- | Patch model state depending on file change event.+module Emanote.Source.Patch+ ( patchModel,+ filePatterns,+ ignorePatterns,+ )+where++import Control.Exception (throwIO)+import Control.Monad.Logger (LoggingT (runLoggingT), MonadLogger, MonadLoggerIO (askLoggerIO))+import Data.ByteString qualified as BS+import Data.List.NonEmpty qualified as NEL+import Data.Time (defaultTimeLocale, formatTime, getCurrentTime)+import Emanote.Model qualified as M+import Emanote.Model.Note qualified as N+import Emanote.Model.SData qualified as SD+import Emanote.Model.Stork.Index qualified as Stork+import Emanote.Model.Type (ModelEma)+import Emanote.Prelude+ ( BadInput (BadInput),+ log,+ logD,+ )+import Emanote.Route qualified as R+import Emanote.Source.Loc (Loc, LocLayers, locPath, locResolve, primaryLayer)+import Emanote.Source.Pattern (filePatterns, ignorePatterns)+import Heist.Extra.TemplateState qualified as T+import Optics.Operators ((%~))+import Relude+import Relude.Extra (traverseToSnd)+import System.UnionMount qualified as UM+import UnliftIO.Concurrent (threadDelay)+import UnliftIO.Directory (doesDirectoryExist)++-- | Map a filesystem change to the corresponding model change.+patchModel ::+ (MonadIO m, MonadLogger m, MonadLoggerIO m) =>+ LocLayers ->+ (N.Note -> N.Note) ->+ Stork.IndexVar ->+ -- | Type of the file being changed+ R.FileType R.SourceExt ->+ -- | Path to the file being changed+ FilePath ->+ -- | Specific change to the file, along with its paths from other "layers"+ UM.FileAction (NonEmpty (Loc, FilePath)) ->+ m (ModelEma -> ModelEma)+patchModel layers noteF storkIndexTVar fpType fp action = do+ logger <- askLoggerIO+ now <- liftIO getCurrentTime+ -- Prefix all patch logging with timestamp.+ let newLogger loc src lvl s =+ logger loc src lvl $ fromString (formatTime defaultTimeLocale "[%H:%M:%S] " now) <> s+ runLoggingT (patchModel' layers noteF storkIndexTVar fpType fp action) newLogger++-- | Map a filesystem change to the corresponding model change.+patchModel' ::+ (MonadIO m, MonadLogger m) =>+ LocLayers ->+ (N.Note -> N.Note) ->+ Stork.IndexVar ->+ -- | Type of the file being changed+ R.FileType R.SourceExt ->+ -- | Path to the file being changed+ FilePath ->+ -- | Specific change to the file, along with its paths from other "layers"+ UM.FileAction (NonEmpty (Loc, FilePath)) ->+ m (ModelEma -> ModelEma)+patchModel' layers noteF storkIndexTVar fpType fp action = do+ case fpType of+ R.LMLType lmlType -> do+ case R.mkLMLRouteFromKnownFilePath lmlType fp of+ Nothing ->+ pure id -- Impossible+ Just r -> do+ -- Stork doesn't support incremental building of index, so we must+ -- clear it to pave way for a rebuild later when requested.+ --+ -- From https://github.com/jameslittle230/stork/discussions/112#discussioncomment-252861+ --+ -- > Stork also doesn't support incremental index updates today --+ -- you'd have to re-index everything when users added a new document,+ -- which might be prohibitively long.+ Stork.clearStorkIndex storkIndexTVar++ case action of+ UM.Refresh refreshAction overlays -> do+ let fpAbs = locResolve $ head overlays+ -- TODO: This should automatically be computed, instead of being passed.+ -- We need access to the model though! With dependency management to boot.+ -- Until this, `layers` is threaded through as a hack.+ currentLayerPath = locPath $ primaryLayer layers+ s <- readRefreshedFile refreshAction fpAbs+ note <- N.parseNote currentLayerPath r fpAbs (decodeUtf8 s)+ pure $ M.modelInsertNote $ noteF note+ UM.Delete -> do+ log $ "Removing note: " <> toText fp+ pure $ M.modelDeleteNote r+ R.Yaml ->+ case R.mkRouteFromFilePath fp of+ Nothing ->+ pure id+ Just r -> case action of+ UM.Refresh refreshAction overlays -> do+ yamlContents <- forM (NEL.reverse overlays) $ \overlay -> do+ let fpAbs = locResolve overlay+ traverseToSnd (readRefreshedFile refreshAction) fpAbs+ sData <-+ liftIO $+ either (throwIO . BadInput) pure $+ SD.parseSDataCascading r yamlContents+ pure $ M.modelInsertData sData+ UM.Delete -> do+ log $ "Removing data: " <> toText fp+ pure $ M.modelDeleteData r+ R.HeistTpl ->+ case action of+ UM.Refresh refreshAction overlays -> do+ let fpAbs = locResolve $ head overlays+ -- Once we start loading HTML templates, mark the model as "ready"+ -- so Ema will begin rendering content in place of "Loading..."+ -- indicator+ readyOnTemplates = bool id M.modelReadyForView (refreshAction == UM.Existing)+ act <- do+ s <- readRefreshedFile refreshAction fpAbs+ logD $ "Read " <> show (BS.length s) <> " bytes of template"+ pure $ M.modelHeistTemplate %~ T.addTemplateFile fpAbs fp s+ pure $ readyOnTemplates >>> act+ UM.Delete -> do+ log $ "Removing template: " <> toText fp+ pure $ M.modelHeistTemplate %~ T.removeTemplateFile fp+ R.AnyExt -> do+ case R.mkRouteFromFilePath fp of+ Nothing ->+ pure id+ Just r -> case action of+ UM.Refresh refreshAction overlays -> do+ let fpAbs = locResolve $ head overlays+ doesDirectoryExist fpAbs >>= \case+ True ->+ -- A directory got added; this is not a static 'file'+ pure id+ False -> do+ let logF = case refreshAction of+ UM.Existing -> logD . ("Registering" <>)+ _ -> log . ("Re-registering" <>)+ logF $ " file: " <> toText fpAbs <> " " <> show r+ t <- liftIO getCurrentTime+ pure $ M.modelInsertStaticFile t r fpAbs+ UM.Delete -> do+ pure $ M.modelDeleteStaticFile r++readRefreshedFile :: (MonadLogger m, MonadIO m) => UM.RefreshAction -> FilePath -> m ByteString+readRefreshedFile refreshAction fp =+ case refreshAction of+ UM.Existing -> do+ logD $ "Loading file: " <> toText fp+ readFileBS fp+ _ ->+ readFileFollowingFsnotify fp++-- | Like `readFileBS` but accounts for file truncation due to us responding+-- *immediately* to a fsnotify modify event (which is triggered even before the+-- writer *finishes* writing the new contents). We solve this "glitch" by+-- delaying the read retry, expecting (hoping really) that *this time* the new+-- non-empty contents will come through. 'tis a bit of a HACK though.+readFileFollowingFsnotify :: (MonadIO m, MonadLogger m) => FilePath -> m ByteString+readFileFollowingFsnotify fp = do+ log $ "Reading file: " <> toText fp+ readFileBS fp >>= \case+ "" ->+ reReadFileBS 100 fp >>= \case+ "" ->+ -- Sometimes 100ms is not enough (eg: on WSL), so wait a bit more and+ -- give it another try.+ reReadFileBS 300 fp+ s -> pure s+ s -> pure s+ where+ -- Wait before reading, logging the given delay.+ reReadFileBS ms filePath = do+ threadDelay $ 1000 * ms+ log $ "Re-reading (" <> show ms <> "ms" <> ") file: " <> toText filePath+ readFileBS filePath
@@ -0,0 +1,42 @@+module Emanote.Source.Pattern where++import Emanote.Route qualified as R+import Relude+import System.FilePattern (FilePattern)++filePattern :: HasCallStack => R.FileType R.SourceExt -> FilePath+filePattern = \case+ R.LMLType R.Md ->+ R.withExt @_ @('R.LMLType 'R.Md) $+ "**/*"+ R.LMLType R.Org ->+ R.withExt @_ @('R.LMLType 'R.Org) $+ "**/*"+ R.Yaml ->+ R.withExt @_ @'R.Yaml $+ "**/*"+ R.HeistTpl ->+ R.withExt @_ @'R.HeistTpl $+ "**/*"+ R.AnyExt ->+ "**"++filePatterns :: [(R.FileType R.SourceExt, FilePattern)]+filePatterns =+ (id &&& filePattern)+ <$> [ R.LMLType R.Md,+ R.LMLType R.Org,+ R.Yaml,+ R.HeistTpl,+ R.AnyExt+ ]++ignorePatterns :: [FilePattern]+ignorePatterns =+ [ -- Ignore all dotfile directories (eg: .git, .vscode)+ "**/.*/**",+ -- Ignore vi/vim/neovim writebackup files (see ":help writebackup")+ "**/*~",+ -- /Top-level ./-/ directory is reserved by Emanote+ "-/**"+ ]
@@ -0,0 +1,3 @@+module Emanote.View (render) where++import Emanote.View.Template (render)
@@ -0,0 +1,226 @@+{-# LANGUAGE RecordWildCards #-}++module Emanote.View.Common+ ( commonSplices,+ renderModelTemplate,+ routeBreadcrumbs,+ generatedCssFile,++ -- * Render context+ TemplateRenderCtx (..),+ mkTemplateRenderCtx,+ defaultRouteMeta,+ )+where++import Data.Aeson.Types qualified as Aeson+import Data.Map.Syntax ((##))+import Data.Text qualified as T+import Data.Version (showVersion)+import Ema qualified+import Emanote.Model.Meta qualified as Meta+import Emanote.Model.SData qualified as SData+import Emanote.Model.Title qualified as Tit+import Emanote.Model.Type (Model)+import Emanote.Model.Type qualified as M+import Emanote.Pandoc.BuiltinFilters (preparePandoc)+import Emanote.Pandoc.Renderer (EmanotePandocRenderers (..), PandocRenderers (..))+import Emanote.Pandoc.Renderer qualified as Renderer+import Emanote.Route (LMLRoute)+import Emanote.Route qualified as R+import Emanote.Route.SiteRoute.Class qualified as SR+import Emanote.View.LiveServerFiles qualified as LiveServerFiles+import Heist qualified as H+import Heist.Extra.Splices.List qualified as Splices+import Heist.Extra.Splices.Pandoc.Ctx (RenderCtx)+import Heist.Extra.TemplateState qualified as Tmpl+import Heist.Interpreted qualified as HI+import Heist.Splices.Apply qualified as HA+import Heist.Splices.Bind qualified as HB+import Heist.Splices.Json qualified as HJ+import Optics.Operators ((^.))+import Paths_emanote qualified+import Relude+import System.FilePath ((</>))+import Text.Blaze.Html ((!))+import Text.Blaze.Html5 qualified as H+import Text.Blaze.Html5.Attributes qualified as A+import Text.Blaze.Renderer.XmlHtml qualified as RX++data TemplateRenderCtx n = TemplateRenderCtx+ { withInlineCtx :: (RenderCtx -> HI.Splice Identity) -> HI.Splice Identity,+ withBlockCtx :: (RenderCtx -> HI.Splice Identity) -> HI.Splice Identity,+ withLinkInlineCtx :: (RenderCtx -> HI.Splice Identity) -> HI.Splice Identity,+ titleSplice :: Tit.Title -> HI.Splice Identity+ }++-- | Create the context in which Heist templates (notably `pandoc.tpl`) will be+-- rendered.+mkTemplateRenderCtx ::+ -- | Current model.+ Model ->+ -- | Current route.+ R.LMLRoute ->+ -- | Associated metadata.+ Aeson.Value ->+ TemplateRenderCtx Identity+mkTemplateRenderCtx model r meta =+ let EmanotePandocRenderers {..} = M._modelPandocRenderers model+ withInlineCtx =+ withRenderCtx inlineRenderers+ withLinkInlineCtx =+ withRenderCtx linkInlineRenderers+ withBlockCtx =+ withRenderCtx blockRenderers+ -- TODO: We should be using withInlineCtx, so as to make the wikilink+ -- render in note title.+ titleSplice titleDoc = withLinkInlineCtx $ \ctx ->+ Tit.titleSplice ctx preparePandoc titleDoc+ in TemplateRenderCtx {..}+ where+ withRenderCtx ::+ (Monad m) =>+ PandocRenderers Model LMLRoute ->+ (RenderCtx -> H.HeistT Identity m x) ->+ H.HeistT Identity m x+ withRenderCtx pandocRenderers f =+ f+ =<< Renderer.mkRenderCtxWithPandocRenderers+ pandocRenderers+ classRules+ model+ r+ classRules :: Map Text Text+ classRules =+ SData.lookupAeson mempty ("pandoc" :| ["rewriteClass"]) meta++defaultRouteMeta :: Model -> (LMLRoute, Aeson.Value)+defaultRouteMeta model =+ let r = M.modelIndexRoute model+ meta = Meta.getEffectiveRouteMeta r model+ in (r, meta)++generatedCssFile :: FilePath+generatedCssFile = "tailwind.css"++commonSplices ::+ HasCallStack =>+ ((RenderCtx -> HI.Splice Identity) -> HI.Splice Identity) ->+ Model ->+ Aeson.Value ->+ Tit.Title ->+ H.Splices (HI.Splice Identity)+commonSplices withCtx model meta routeTitle = do+ let siteTitle = fromString . toString $ SData.lookupAeson @Text "Emanote Site" ("page" :| ["siteTitle"]) meta+ routeTitleFull =+ if routeTitle == siteTitle+ then siteTitle+ else routeTitle <> " – " <> siteTitle+ -- Heist helpers+ "bind" ## HB.bindImpl+ "apply" ## HA.applyImpl+ -- Add tailwind css shim+ "tailwindCssShim" ##+ do+ pure . RX.renderHtmlNodes $+ if M.inLiveServer model || not (model ^. M.modelCompileTailwind)+ then do+ -- Twind shim doesn't reliably work in dev server mode. Let's just use the+ -- tailwind CDN.+ cachedTailwindCdn+ else do+ H.link+ -- TODO: Use ?md5 to prevent stale browser caching of CSS.+ -- TODO: This should go through Ema route encoder!+ ! A.href (H.toValue $ cannotBeCached generatedCssFile)+ ! A.rel "stylesheet"+ ! A.type_ "text/css"+ "ema:version" ##+ HI.textSplice (toText $ showVersion Paths_emanote.version)+ "ema:metadata" ##+ HJ.bindJson meta+ "ema:title" ## withCtx $ \ctx ->+ Tit.titleSplice ctx preparePandoc routeTitle+ -- <head>'s <title> cannot contain HTML+ "ema:titleFull" ##+ Tit.titleSpliceNoHtml routeTitleFull+ -- `ema:homeUrl` is normally `""`; but if Emanote is being served from an URL+ -- prefix, it would be "/foo/" (with a slash at the end). This allows you to+ -- just concatanate homeUrl with a relative URL path (no slash in between), to+ -- get the full URL. The reason there is no slash in between is to account for+ -- the usual case of homeUrl being an empty string.+ "ema:homeUrl" ##+ ( let homeR = SR.lmlSiteRoute (M.modelIndexRoute model)+ homeUrl' = SR.siteRouteUrl model homeR+ homeUrl = if homeUrl' /= "" then homeUrl' <> "/" else homeUrl'+ in HI.textSplice homeUrl+ )+ "ema:indexUrl" ##+ HI.textSplice (SR.siteRouteUrl model SR.indexRoute)+ "ema:tagIndexUrl" ##+ HI.textSplice (SR.siteRouteUrl model $ SR.tagIndexRoute [])+ "ema:taskIndexUrl" ##+ HI.textSplice (SR.siteRouteUrl model SR.taskIndexRoute)+ "ema:emanoteStaticLayerUrl" ##+ HI.textSplice+ ( -- HACK+ -- Also: more-head.tpl is the one place where this is hardcoded.+ let staticFolder = "_emanote-static"+ itUrl =+ SR.siteRouteUrl model $+ SR.staticFileSiteRoute $+ fromMaybe (error "no _emanote-static?") $+ M.modelLookupStaticFile (staticFolder </> "inverted-tree.css") model+ staticFolderUrl = fst $ T.breakOn "/inverted-tree.css" itUrl+ -- Deal with a silly Firefox bug https://github.com/EmaApps/emanote/issues/340+ --+ -- Firefox deduces an incorrect <base> after doing morphdom+ -- patching, unless the <base> is absolute (i.e., starts with a '/').+ patchForFirefoxBug folder url =+ if M.inLiveServer model && url == toText folder+ then "/" <> url+ else url+ in patchForFirefoxBug staticFolder staticFolderUrl+ )+ -- For those cases the user really wants to hardcode the URL+ "ema:urlStrategySuffix" ##+ HI.textSplice (SR.urlStrategySuffix model)+ where+ -- A hack to force the browser not to cache the CSS, because we are not md5+ -- hashing the CSS yet (because the CSS is generated *after* the HTML files+ -- are generated.)+ -- For a proper way to do this, see: https://github.com/srid/ema/issues/20+ cannotBeCached url = url <> "?instanceId=" <> show (model ^. M.modelInstanceID)+ cachedTailwindCdn = do+ let localCdnUrl =+ SR.siteRouteUrl model $+ SR.staticFileSiteRoute $+ LiveServerFiles.tailwindCssFile model+ H.link+ ! A.href (H.toValue localCdnUrl)+ ! A.rel "stylesheet"+ ! A.type_ "text/css"++renderModelTemplate :: Model -> Tmpl.TemplateName -> H.Splices (HI.Splice Identity) -> LByteString+renderModelTemplate model templateName =+ let handleErr =+ if M.inLiveServer model+ then Ema.emaErrorHtmlResponse+ else -- When staticaly generating, we must fail asap on template errors.+ error+ in -- Until Ema's error handling improves ...+ either handleErr id+ . flip (Tmpl.renderHeistTemplate templateName) (model ^. M.modelHeistTemplate)++routeBreadcrumbs :: TemplateRenderCtx n -> Model -> LMLRoute -> HI.Splice Identity+routeBreadcrumbs TemplateRenderCtx {..} model r = do+ let breadcrumbs =+ r+ & R.lmlRouteCase+ -- Hardcode to 'Md, and resolve using resolveLmlRoute latter.+ & either R.routeInits (R.routeInits . coerce)+ & init+ & fmap (M.resolveLmlRoute model)+ Splices.listSplice breadcrumbs "each-crumb" $ \crumbR -> do+ "crumb:url" ## HI.textSplice (SR.siteRouteUrl model $ SR.lmlSiteRoute crumbR)+ "crumb:title" ## titleSplice (M.modelLookupTitle crumbR model)
@@ -0,0 +1,96 @@+{-# LANGUAGE DeriveAnyClass #-}++module Emanote.View.Export+ ( renderGraphExport,+ Link (..),+ modelRels,+ )+where++import Data.Aeson (ToJSON)+import Data.Aeson qualified as Aeson+import Data.Map.Strict qualified as Map+import Emanote.Model (Model)+import Emanote.Model qualified as M+import Emanote.Model.Graph qualified as G+import Emanote.Model.Link.Rel qualified as Rel+import Emanote.Model.Link.Resolve qualified as Resolve+import Emanote.Model.Title qualified as Tit+import Emanote.Route (LMLRoute)+import Emanote.Route qualified as R+import Emanote.Route.SiteRoute qualified as SR+import Emanote.Route.SiteRoute.Class (lmlSiteRoute)+import Optics.Operators ((^.))+import Relude++data Export = Export+ { version :: Word,+ files :: Map Text SourceFile+ }+ deriving stock (Generic, Show)+ deriving anyclass (ToJSON)++currentVersion :: Word+currentVersion = 1++-- | A source file in `Model`+data SourceFile = SourceFile+ { title :: Text,+ filePath :: Text,+ parentNote :: Maybe Text,+ url :: Text,+ meta :: Aeson.Value,+ links :: [Link]+ }+ deriving stock (Generic, Show)+ deriving anyclass (ToJSON)++data Link = Link+ { unresolvedRelTarget :: Rel.UnresolvedRelTarget,+ resolvedRelTarget :: Rel.ResolvedRelTarget Text+ }+ deriving stock (Generic, Eq, Ord, Show)+ deriving anyclass (ToJSON)++renderGraphExport :: Model -> LByteString+renderGraphExport model =+ let notes_ =+ M.modelNoteMetas model+ & Map.mapKeys lmlRouteKey+ & Map.map+ ( \(tit, r, meta_) ->+ let k = lmlRouteKey r+ in SourceFile+ (Tit.toPlain tit)+ k+ (toText . lmlSourcePath <$> G.parentLmlRoute model r)+ (SR.siteRouteUrl model $ lmlSiteRoute r)+ meta_+ (fromMaybe [] $ Map.lookup k rels)+ )+ rels = modelRels model & Map.mapKeys lmlRouteKey+ export = Export currentVersion notes_+ in Aeson.encode export++modelRels :: Model -> Map LMLRoute [Link]+modelRels model =+ Map.fromListWith (<>) $+ M.modelNoteRels model <&> \rel ->+ let from_ = rel ^. Rel.relFrom+ to_ = rel ^. Rel.relTo+ toTarget =+ Resolve.resolveUnresolvedRelTarget model to_+ <&> SR.siteRouteUrlStatic model+ in (from_, one $ Link to_ toTarget)++-- An unique key to represent this LMLRoute in the exported JSON+--+-- We use the source path consistently.+lmlRouteKey :: LMLRoute -> Text+lmlRouteKey =+ toText . R.withLmlRoute R.encodeRoute++-- Path of the LML note+lmlSourcePath :: LMLRoute -> FilePath+lmlSourcePath =+ R.withLmlRoute R.encodeRoute
@@ -0,0 +1,27 @@+module Emanote.View.LiveServerFiles+ ( isLiveServerFile,+ tailwindFullCssPath,+ tailwindCssFile,+ )+where++import Data.Text qualified as T+import Emanote.Model.StaticFile (StaticFile)+import Emanote.Model.Type qualified as M+import Relude++-- TODO: Check this compile-time using TH?++baseDir :: FilePath+baseDir = "_emanote-live-server"++tailwindFullCssPath :: FilePath+tailwindFullCssPath = baseDir <> "/tailwind/2.2.2/tailwind.min.css"++isLiveServerFile :: FilePath -> Bool+isLiveServerFile (toText -> fp) =+ toText baseDir `T.isPrefixOf` fp++tailwindCssFile :: M.Model -> StaticFile+tailwindCssFile model =+ fromMaybe (error "model not ready?") $ M.modelLookupStaticFile tailwindFullCssPath model
@@ -0,0 +1,111 @@+module Emanote.View.TagIndex (renderTagIndex) where++import Data.List qualified as List+import Data.List.NonEmpty qualified as NE+import Data.Map.Strict qualified as Map+import Data.Map.Syntax ((##))+import Data.Tree (Forest, Tree)+import Data.Tree qualified as Tree+import Emanote.Model (Model)+import Emanote.Model qualified as M+import Emanote.Model.Note qualified as MN+import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as HT+import Emanote.Pandoc.Renderer.Query qualified as PF+import Emanote.Route.SiteRoute.Class qualified as SR+import Emanote.View.Common+ ( TemplateRenderCtx (withInlineCtx),+ commonSplices,+ defaultRouteMeta,+ mkTemplateRenderCtx,+ renderModelTemplate,+ )+import Heist.Extra.Splices.List qualified as Splices+import Heist.Extra.Splices.Pandoc.Ctx (emptyRenderCtx)+import Heist.Interpreted qualified as HI+import Relude++-- An index view into the notebook indexed by the given tag path.+data TagIndex = TagIndex+ { -- | The tag path under which this index is creatd+ tagIndexPath :: [HT.TagNode],+ -- | User descriptive title of this index+ tagIndexTitle :: Text,+ -- | All notes tagged precisely with this tag path+ tagIndexNotes :: [MN.Note],+ -- | Tags immediately under this tag path.+ --+ -- If the tag path being index is "foo/bar", this will contain "foo/bar/qux".+ tagIndexChildren :: [(NonEmpty HT.TagNode, [MN.Note])]+ }+ deriving stock (Eq)++mkTagIndex :: Model -> [HT.TagNode] -> TagIndex+mkTagIndex model tagPath' =+ let mTagPath = nonEmpty tagPath'+ tagMap = Map.fromList $ M.modelTags model+ tagForest = HT.tagTree tagMap+ childNodes =+ maybe+ (fst . Tree.rootLabel <$> tagForest)+ (fmap (fst . Tree.rootLabel) . Tree.subForest . flip lookupForestMust tagForest)+ mTagPath+ childTags =+ childNodes <&> \childNode ->+ let t = NE.reverse $ childNode :| reverse tagPath'+ in (t, maybeToMonoid $ Map.lookup (HT.constructTag t) tagMap)+ in case mTagPath of+ Nothing ->+ -- The root index displays all top-level tags (no notes)+ TagIndex [] "Tag Index" [] childTags+ Just tagPath ->+ let notes =+ snd . Tree.rootLabel $ lookupForestMust tagPath tagForest+ viewTitle = "#" <> tagNodesText tagPath <> " - Tag Index"+ in TagIndex (toList tagPath) viewTitle notes childTags+ where+ lookupForestMust :: (Show k, Eq k) => NonEmpty k -> Forest (k, a) -> Tree (k, a)+ lookupForestMust path =+ fromMaybe (error $ "Tag not found in forest: " <> show path)+ . lookupForest path+ lookupForest :: Eq k => NonEmpty k -> Forest (k, a) -> Maybe (Tree (k, a))+ lookupForest (k :| ks') trees =+ case nonEmpty ks' of+ Nothing ->+ List.find (\(Tree.Node lbl _) -> fst lbl == k) trees+ Just ks -> do+ subForest <- Tree.subForest <$> List.find (\(Tree.Node lbl _) -> fst lbl == k) trees+ lookupForest ks subForest++renderTagIndex :: Model -> [HT.TagNode] -> LByteString+renderTagIndex model tagPath = do+ let (r, meta) = defaultRouteMeta model+ tCtx = mkTemplateRenderCtx model r meta+ tagIdx = mkTagIndex model tagPath+ renderModelTemplate model "templates/special/tagindex" $ do+ commonSplices ($ emptyRenderCtx) model meta $ fromString . toString $ tagIndexTitle tagIdx+ "ema:tag:title" ## HI.textSplice (maybe "/" (HT.unTagNode . last) $ nonEmpty tagPath)+ "ema:tag:url" ## HI.textSplice (SR.siteRouteUrl model $ SR.tagIndexRoute tagPath)+ let parents = maybe [] (inits . init) $ nonEmpty (tagIndexPath tagIdx)+ "ema:tagcrumbs" ##+ Splices.listSplice parents "ema:each-crumb" $+ \crumb -> do+ let crumbTitle = maybe "/" (HT.unTagNode . last) . nonEmpty $ crumb+ crumbUrl = SR.siteRouteUrl model $ SR.tagIndexRoute crumb+ "ema:tagcrumb:title" ## HI.textSplice crumbTitle+ "ema:tagcrumb:url" ## HI.textSplice crumbUrl+ "ema:childTags" ##+ Splices.listSplice (tagIndexChildren tagIdx) "ema:each-childTag" $+ \childTag -> do+ let childIndex = mkTagIndex model (toList . fst $ childTag)+ "ema:childTag:title" ## HI.textSplice (tagNodesText $ fst childTag)+ "ema:childTag:url" ## HI.textSplice (SR.siteRouteUrl model $ SR.tagIndexRoute (toList $ fst childTag))+ "ema:childTag:count-note" ## HI.textSplice (show (length $ snd childTag))+ "ema:childTag:count-tag" ## HI.textSplice (show (length $ tagIndexChildren childIndex))+ "ema:notes" ##+ Splices.listSplice (tagIndexNotes tagIdx) "ema:each-note" $+ \note ->+ PF.noteSpliceMap (withInlineCtx tCtx) model note++tagNodesText :: NonEmpty HT.TagNode -> Text+tagNodesText =+ HT.unTag . HT.constructTag
@@ -0,0 +1,55 @@+module Emanote.View.TaskIndex (renderTasks) where++import Data.IxSet.Typed qualified as Ix+import Data.List.NonEmpty qualified as NE+import Data.Map.Strict qualified as Map+import Data.Map.Syntax ((##))+import Emanote.Model (Model)+import Emanote.Model.Task (Task)+import Emanote.Model.Task qualified as Task+import Emanote.Model.Type qualified as M+import Emanote.Route qualified as R+import Emanote.Route.SiteRoute qualified as SR+import Emanote.View.Common qualified as Common+import Heist.Extra.Splices.List qualified as Splices+import Heist.Extra.Splices.Pandoc qualified as Splices+import Heist.Extra.Splices.Pandoc.Ctx (emptyRenderCtx)+import Heist.Interpreted qualified as HI+import Optics.Operators ((^.))+import Relude+import Text.Pandoc.Definition qualified as B++newtype TaskIndex = TaskIndex {unTaskIndex :: Map R.LMLRoute (NonEmpty Task)}++mkTaskIndex :: Model -> TaskIndex+mkTaskIndex model =+ TaskIndex . Map.map NE.sort $+ Map.fromListWith (<>) $+ filter (not . Task._taskChecked) (Ix.toList $ model ^. M.modelTasks) <&> \task ->+ (task ^. Task.taskRoute, one task)++renderTasks :: Model -> LByteString+renderTasks model = do+ let (defR, meta) = Common.defaultRouteMeta model+ tCtx = Common.mkTemplateRenderCtx model defR meta+ taskIndex = mkTaskIndex model+ taskGroupSplice r tasks = do+ "t:note:url" ## HI.textSplice (SR.siteRouteUrl model $ SR.lmlSiteRoute r)+ "t:note:title" ## Common.titleSplice tCtx (M.modelLookupTitle r model)+ "t:note:breadcrumbs" ##+ Common.routeBreadcrumbs tCtx model r+ "t:tasks" ## Splices.listSplice (toList tasks) "task" taskSplice+ taskSplice task = do+ let r = task ^. Task.taskRoute+ -- TODO: reuse note splice+ "task:description" ## Common.withInlineCtx tCtx $ \ctx ->+ Splices.pandocSplice ctx $ B.Pandoc mempty $ one $ B.Plain $ task ^. Task.taskDescription+ "note:title" ## Common.titleSplice tCtx (M.modelLookupTitle r model)+ "note:url" ## HI.textSplice (SR.siteRouteUrl model $ SR.lmlSiteRoute r)++ Common.renderModelTemplate model "templates/special/tasks" $ do+ Common.commonSplices ($ emptyRenderCtx) model meta "Task Index"+ let groups =+ Map.toList (unTaskIndex taskIndex)+ & sortWith fst+ "ema:taskGroups" ## Splices.listSplice groups "taskGroup" (uncurry taskGroupSplice)
@@ -0,0 +1,233 @@+module Emanote.View.Template (emanoteSiteOutput, render) where++import Control.Monad.Logger (MonadLoggerIO)+import Data.Aeson.Types qualified as Aeson+import Data.List (partition)+import Data.List.NonEmpty qualified as NE+import Data.Map.Syntax ((##))+import Data.Text qualified as T+import Data.Tree.Path qualified as PathTree+import Ema qualified+import Emanote.Model (Model, ModelEma)+import Emanote.Model qualified as M+import Emanote.Model.Calendar qualified as Calendar+import Emanote.Model.Graph qualified as G+import Emanote.Model.Meta qualified as Meta+import Emanote.Model.Note qualified as MN+import Emanote.Model.SData qualified as SData+import Emanote.Model.Stork (renderStorkIndex)+import Emanote.Pandoc.BuiltinFilters (prepareNoteDoc, preparePandoc)+import Emanote.Route qualified as R+import Emanote.Route.SiteRoute (SiteRoute)+import Emanote.Route.SiteRoute qualified as SR+import Emanote.Route.SiteRoute.Class (indexRoute)+import Emanote.View.Common qualified as C+import Emanote.View.Export (renderGraphExport)+import Emanote.View.TagIndex qualified as TagIndex+import Emanote.View.TaskIndex qualified as TaskIndex+import Heist qualified as H+import Heist.Extra.Splices.List qualified as Splices+import Heist.Extra.Splices.Pandoc qualified as Splices+import Heist.Extra.Splices.Pandoc.Ctx (emptyRenderCtx)+import Heist.Extra.Splices.Tree qualified as Splices+import Heist.Interpreted qualified as HI+import Heist.Splices qualified as Heist+import Optics.Core (Prism', review)+import Optics.Operators ((.~), (^.))+import Relude+import Text.Pandoc.Builder qualified as B+import Text.Pandoc.Definition (Pandoc (..))++emanoteSiteOutput :: (MonadIO m, MonadLoggerIO m) => Prism' FilePath SiteRoute -> ModelEma -> SR.SiteRoute -> m (Ema.Asset LByteString)+emanoteSiteOutput rp model' r = do+ let model = M.withRoutePrism rp model'+ render model r <&> fmap fixStaticUrl+ where+ -- See the FIXME in more-head.tpl.+ fixStaticUrl :: LByteString -> LByteString+ fixStaticUrl s =+ case findPrefix of+ Nothing -> s+ Just prefix ->+ -- Patch the URL in CSS's "src" attribute.+ encodeUtf8+ . T.replace "src: url(_emanote-static/" ("src: url(" <> prefix <> "_emanote-static/")+ . decodeUtf8+ $ s+ where+ -- Find the "prefix" in PrefixedRoute if Emanote is used as a library.+ findPrefix :: Maybe Text+ findPrefix = do+ let indexR = toText $ review rp indexRoute+ prefix <- T.stripSuffix "-/all.html" indexR+ guard $ not $ T.null prefix+ pure prefix++render :: (MonadIO m, MonadLoggerIO m) => Model -> SR.SiteRoute -> m (Ema.Asset LByteString)+render m sr =+ let setErrorPageMeta =+ MN.noteMeta .~ SData.mergeAesons (withTemplateName "/templates/error" :| [withSiteTitle "Emanote Error"])+ in case sr of+ SR.SiteRoute_MissingR urlPath -> do+ let hereRoute = R.decodeHtmlRoute urlPath+ note404 =+ MN.missingNote hereRoute (toText urlPath)+ & setErrorPageMeta+ & MN.noteTitle .~ "! Missing link"+ pure $ Ema.AssetGenerated Ema.Html $ renderLmlHtml m note404+ SR.SiteRoute_AmbiguousR urlPath notes -> do+ let noteAmb =+ MN.ambiguousNoteURL urlPath notes+ & setErrorPageMeta+ & MN.noteTitle .~ "! Ambiguous link"+ pure $ Ema.AssetGenerated Ema.Html $ renderLmlHtml m noteAmb+ SR.SiteRoute_ResourceRoute r -> pure $ renderResourceRoute m r+ SR.SiteRoute_VirtualRoute r -> renderVirtualRoute m r++renderResourceRoute :: Model -> SR.ResourceRoute -> Ema.Asset LByteString+renderResourceRoute m = \case+ SR.ResourceRoute_LML r -> do+ case M.modelLookupNoteByRoute r m of+ Just note ->+ Ema.AssetGenerated Ema.Html $ renderLmlHtml m note+ Nothing ->+ -- This should never be reached because decodeRoute looks up the model.+ error $ "Bad route: " <> show r+ SR.ResourceRoute_StaticFile _ fpAbs ->+ Ema.AssetStatic fpAbs++renderVirtualRoute :: (MonadIO m, MonadLoggerIO m) => Model -> SR.VirtualRoute -> m (Ema.Asset LByteString)+renderVirtualRoute m = \case+ SR.VirtualRoute_TagIndex mtag ->+ pure $ Ema.AssetGenerated Ema.Html $ TagIndex.renderTagIndex m mtag+ SR.VirtualRoute_Index ->+ pure $ Ema.AssetGenerated Ema.Html $ renderSRIndex m+ SR.VirtualRoute_Export ->+ pure $ Ema.AssetGenerated Ema.Other $ renderGraphExport m+ SR.VirtualRoute_StorkIndex ->+ Ema.AssetGenerated Ema.Other <$> renderStorkIndex m+ SR.VirtualRoute_TaskIndex ->+ pure $ Ema.AssetGenerated Ema.Html $ TaskIndex.renderTasks m++renderSRIndex :: Model -> LByteString+renderSRIndex model = do+ let (r, meta) = C.defaultRouteMeta model+ tCtx = C.mkTemplateRenderCtx model r meta+ C.renderModelTemplate model "templates/special/index" $ do+ C.commonSplices ($ emptyRenderCtx) model meta "Index"+ routeTreeSplice tCtx Nothing model++loaderHead :: LByteString+loaderHead =+ "<em style='font-size: 400%; border-bottom: 1px solid; margin-bottom: 4em; '>Union mounting notebook layers; please wait ...</em>"++renderLmlHtml :: Model -> MN.Note -> LByteString+renderLmlHtml model note = do+ let r = note ^. MN.noteRoute+ meta = Meta.getEffectiveRouteMetaWith (note ^. MN.noteMeta) r model+ ctx = C.mkTemplateRenderCtx model r meta+ templateName = lookupTemplateName meta+ -- Force a doctype into the generated HTML as a workaround for Heist+ -- discarding it. See: https://github.com/srid/emanote/issues/216+ withDoctype = ("<!DOCTYPE html>\n" <>)+ withLoadingMessage =+ if M.inLiveServer model && model ^. M.modelStatus == M.Status_Loading+ then (loaderHead <>)+ else id+ withDoctype . withLoadingMessage . C.renderModelTemplate model templateName $ do+ C.commonSplices (C.withLinkInlineCtx ctx) model meta (note ^. MN.noteTitle)+ let backlinksSplice (bs :: [(R.LMLRoute, NonEmpty [B.Block])]) =+ Splices.listSplice bs "backlink" $+ \(source, contexts) -> do+ let bnote = fromMaybe (error "backlink note missing - impossible") $ M.modelLookupNoteByRoute source model+ bmeta = Meta.getEffectiveRouteMetaWith (bnote ^. MN.noteMeta) source model+ bctx = C.mkTemplateRenderCtx model source bmeta+ -- TODO: reuse note splice+ "backlink:note:title" ## C.titleSplice bctx (M.modelLookupTitle source model)+ "backlink:note:url" ## HI.textSplice (SR.siteRouteUrl model $ SR.lmlSiteRoute source)+ "backlink:note:contexts" ## Splices.listSplice (toList contexts) "context" $ \backlinkCtx -> do+ let ctxDoc :: Pandoc = preparePandoc $ Pandoc mempty $ one $ B.Div B.nullAttr backlinkCtx+ "context:body" ## C.withInlineCtx bctx $ \ctx' ->+ Splices.pandocSplice ctx' ctxDoc+ -- Sidebar navigation+ routeTreeSplice ctx (Just r) model+ "ema:breadcrumbs" ##+ C.routeBreadcrumbs ctx model r+ -- Note stuff+ "ema:note:title" ##+ C.titleSplice ctx (note ^. MN.noteTitle)+ let modelRoute = R.ModelRoute_LML r+ "ema:note:source-path" ##+ HI.textSplice (toText . R.withLmlRoute R.encodeRoute $ r)+ "ema:note:url" ##+ HI.textSplice (SR.siteRouteUrl model . SR.lmlSiteRoute $ r)+ "ema:note:backlinks" ##+ backlinksSplice (G.modelLookupBacklinks modelRoute model)+ let (backlinksDaily, backlinksNoDaily) = partition (Calendar.isDailyNote . fst) $ G.modelLookupBacklinks modelRoute model+ "ema:note:backlinks:daily" ##+ backlinksSplice backlinksDaily+ "ema:note:backlinks:nodaily" ##+ backlinksSplice backlinksNoDaily+ let folgeAnc = G.modelFolgezettelAncestorTree modelRoute model+ "ema:note:uptree" ##+ Splices.treeSplice (const ()) folgeAnc $+ \(last -> nodeRoute) children -> do+ "node:text" ## C.titleSplice ctx $ M.modelLookupTitle nodeRoute model+ "node:url" ## HI.textSplice $ SR.siteRouteUrl model $ SR.lmlSiteRoute nodeRoute+ "tree:open" ## Heist.ifElseISplice (not . null $ children)+ "ema:note:uptree:nonempty" ## Heist.ifElseISplice (not . null $ folgeAnc)+ "ema:note:pandoc" ##+ C.withBlockCtx ctx $+ \ctx' ->+ Splices.pandocSplice ctx' (prepareNoteDoc note)++-- | If there is no 'current route', all sub-trees are marked as active/open.+routeTreeSplice ::+ Monad n =>+ C.TemplateRenderCtx n ->+ Maybe R.LMLRoute ->+ Model ->+ H.Splices (HI.Splice Identity)+routeTreeSplice tCtx mr model = do+ "ema:route-tree" ##+ ( let tree = PathTree.treeDeleteChild "index" $ model ^. M.modelNav+ getOrder tr =+ ( Meta.lookupRouteMeta @Int 0 (one "order") tr model,+ tr+ )+ getCollapsed tr =+ Meta.lookupRouteMeta @Bool True ("template" :| ["sidebar", "collapsed"]) tr model+ mkLmlRoute =+ M.resolveLmlRoute model . R.mkRouteFromSlugs+ lmlRouteSlugs = R.withLmlRoute R.unRoute+ in Splices.treeSplice (getOrder . mkLmlRoute) tree $ \(mkLmlRoute -> nodeRoute) children -> do+ "node:text" ## C.titleSplice tCtx $ M.modelLookupTitle nodeRoute model+ "node:url" ## HI.textSplice $ SR.siteRouteUrl model $ SR.lmlSiteRoute nodeRoute+ let isActiveNode = Just nodeRoute == mr+ isActiveTree =+ -- Active tree checking is applicable only when there is an+ -- active route (i.e., mr is a Just)+ flip (maybe True) mr $ \r ->+ toList (lmlRouteSlugs nodeRoute) `NE.isPrefixOf` lmlRouteSlugs r+ openTree =+ isActiveTree -- Active tree is always open+ || not (getCollapsed nodeRoute)+ "node:active" ## Heist.ifElseISplice isActiveNode+ "node:terminal" ## Heist.ifElseISplice (null children)+ "tree:childrenCount" ## HI.textSplice (show $ length children)+ "tree:open" ## Heist.ifElseISplice openTree+ )++lookupTemplateName :: ConvertUtf8 Text b => Aeson.Value -> b+lookupTemplateName meta =+ encodeUtf8 $ SData.lookupAeson @Text defaultTemplate ("template" :| ["name"]) meta+ where+ defaultTemplate = "templates/layouts/book"++withTemplateName :: Text -> Aeson.Value+withTemplateName =+ SData.oneAesonText (toList $ "template" :| ["name"])++withSiteTitle :: Text -> Aeson.Value+withSiteTitle =+ SData.oneAesonText (toList $ "page" :| ["siteTitle"])
@@ -0,0 +1,28 @@+module Main where++import Emanote (defaultEmanoteConfig, run)+import Emanote.CLI qualified as CLI+import Emanote.Source.Dynamic (emanoteCompileTailwind)+import Main.Utf8 (withUtf8)+import Optics.Core ((.~))+import Relude+import Spec qualified+import System.Environment qualified as Env++main :: IO ()+main =+ withUtf8 $ do+ cli <- CLI.parseCli+ -- Why is test part of executable?+ -- Simply so that ghcid can reload both library and test at the same.+ -- Once Multiple Home Units for GHC is implemented, we can go back to+ -- using separate test stanza.+ if CLI.test cli+ then test+ else run (defaultEmanoteConfig cli & emanoteCompileTailwind .~ True)++test :: IO ()+test = do+ Env.withArgs+ mempty -- Discard emanote's CLI arguments+ Spec.main
@@ -0,0 +1,12 @@+module Spec (main) where++import Emanote.Model.Link.RelSpec qualified as RelSpec+import Emanote.Model.QuerySpec qualified as QuerySpec+import Relude+import Test.Hspec (hspec)++main :: IO ()+main = do+ hspec $ do+ QuerySpec.spec+ RelSpec.spec