emanote 0.8.0.0 → 1.0.0.0
raw patch · 35 files changed
+409/−544 lines, 35 filesdep +commonmark-wikilinkdep +emanotedep +heistdep −heist-emanotedep ~commonmark-extensionsdep ~emadep ~heist-extra
Dependencies added: commonmark-wikilink, emanote, heist
Dependencies removed: heist-emanote
Dependency ranges changed: commonmark-extensions, ema, heist-extra, relude
Files
- CHANGELOG.md +21/−0
- README.md +18/−10
- default/_emanote-static/emanote-logo.svg +19/−56
- default/favicon.svg +19/−56
- default/index.md +0/−7
- default/index.yaml +8/−3
- default/templates/base.tpl +44/−1
- default/templates/components/pandoc.tpl +0/−0
- default/templates/filters/query-timeline.tpl +2/−2
- default/templates/special/base.tpl +0/−3
- emanote.cabal +44/−17
- exe/Main.hs +15/−0
- src/Emanote/CLI.hs +0/−2
- src/Emanote/Model/Graph.hs +1/−1
- src/Emanote/Model/Link/Rel.hs +3/−2
- src/Emanote/Model/Link/RelSpec.hs +0/−32
- src/Emanote/Model/Link/Resolve.hs +1/−1
- src/Emanote/Model/Note.hs +3/−3
- src/Emanote/Model/QuerySpec.hs +0/−19
- src/Emanote/Model/StaticFile.hs +2/−1
- src/Emanote/Model/Title.hs +1/−1
- src/Emanote/Model/Type.hs +40/−7
- src/Emanote/Pandoc/BuiltinFilters.hs +3/−1
- src/Emanote/Pandoc/ExternalLink.hs +53/−0
- src/Emanote/Pandoc/Link.hs +1/−1
- src/Emanote/Pandoc/Markdown/Parser.hs +1/−1
- src/Emanote/Pandoc/Markdown/Syntax/WikiLink.hs +0/−275
- src/Emanote/Pandoc/Renderer/Embed.hs +1/−1
- src/Emanote/Pandoc/Renderer/Url.hs +1/−1
- src/Main.hs +0/−28
- src/Spec.hs +0/−12
- test/Emanote/Model/Link/RelSpec.hs +32/−0
- test/Emanote/Model/QuerySpec.hs +19/−0
- test/Emanote/Pandoc/ExternalLinkSpec.hs +56/−0
- test/Spec.hs +1/−0
+ CHANGELOG.md view
@@ -0,0 +1,21 @@+# Revision history for emanote++## 1.0.0.0 (2022-12-04)++- UI+ - Index pages are no longer marked as 'experimental'+ - Add external link icon to external links (this behaviour is customizable). [\#189](https://github.com/EmaApps/emanote/pull/189)+ - `js.mermaid` snippet uses the "module" script tag approach, which is added to end of `<body>`. See [here](https://mermaid-js.github.io/mermaid/#/n00b-gettingStarted?id=_3-calling-the-javascript-api). + - Align dates in timeline queries+- Configuration+ - Add `page.bodyHtml` option to inject custom HTML at the end of `<body>` tag.+- Packaging+ - Move tests to their own cabal component (thereby, also, unexpose them from library exposed modules)+ - Wikilink parser is now a separate library: https://github.com/srid/commonmark-wikilink+ - Use `heist-extra` 0.2.0.0 which switches over from `heist-emanote` to using official `heist` package.+- Bug fixes+ - #380 & #386: Better handling of `.org` and `.md` ambiguties. Default layer's `index.md` has been removed.++## 0.8.0.0 (2022-11-03)++Initial release.
README.md view
@@ -4,10 +4,14 @@ [](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).+Emanote emanates[^def] a structured view of your plain-text notes. +[^def]: **emanate**: *(of something abstract but perceptible) issue or spread out from (a source)*+ 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. +Emanote is spiritual successor to [neuron](https://neuron.zettel.page) based on [Ema](https://ema.srid.ca).+ ## Installing and using https://emanote.srid.ca/start/install@@ -18,21 +22,25 @@ ## 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.+Emanote is a Haskell software.[^licenses] Thanks to Nix, this repository is pre-configured to provide a delightful development experience with full IDE support in Visual Studio Code. -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.+See https://srid.ca/haskell-template/start for complete instructions, but the tldr is: Install nix, enable Flakes and run `bin/run`. +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++## Related projects++- https://github.com/squidfunk/mkdocs-material++[^licenses]: Emanote uses software and resources that are licensed differently, viz.:+ - [Logo](https://www.svgrepo.com/svg/267765/paper-plane)+ - [Stork search](https://github.com/jameslittle230/stork/blob/master/license.txt)+ - Various SVG icons are from [Heroicons](https://heroicons.com/)
default/_emanote-static/emanote-logo.svg view
@@ -1,64 +1,27 @@ <?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"/> + viewBox="0 0 512.003 512.003" style="enable-background:new 0 0 512.003 512.003;" xml:space="preserve"> +<polygon style="fill:#0068FF;" points="276.547,363.724 156.428,439.952 229.841,320.17 "/> <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"/> + <polygon style="fill:#10BAFC;" points="424.343,501.551 229.841,320.17 499.021,10.446 "/> + <polygon style="fill:#10BAFC;" points="156.428,264.439 12.979,188.915 499.021,10.446 "/> </g> +<polygon style="fill:#0084FF;" points="156.428,264.439 156.428,439.952 229.841,320.17 499.021,10.446 "/> +<path d="M409.804,197.83c-2.509,0-5.026-0.898-7.027-2.72c-4.269-3.883-4.582-10.491-0.699-14.76l0.164-0.181 + c3.883-4.268,10.493-4.582,14.76-0.698c4.27,3.883,4.582,10.491,0.699,14.76l-0.164,0.181 + C415.475,196.679,412.644,197.83,409.804,197.83z"/> +<path d="M318.886,298.027c-2.505,0-5.018-0.895-7.019-2.71c-4.274-3.879-4.594-10.487-0.716-14.759l68.202-75.162 + c3.877-4.274,10.487-4.594,14.759-0.717c4.274,3.878,4.594,10.487,0.717,14.759L326.627,294.6 + C324.565,296.871,321.731,298.027,318.886,298.027z"/> +<path d="M503.407,0.963c-2.579-1.193-5.448-1.251-7.987-0.32c0-0.002,0-0.003-0.001-0.005L9.377,179.106 + c-3.876,1.423-6.551,4.996-6.823,9.116c-0.275,4.12,1.904,8.015,5.558,9.939l137.867,72.584v169.208 + c0,4.679,3.111,8.787,7.612,10.057c0.94,0.265,1.895,0.393,2.837,0.393c1.992,0,3.923-0.589,5.586-1.644 + c0.004,0.006,0.008,0.01,0.014,0.017l113.3-71.9l141.89,132.319c1.966,1.833,4.523,2.807,7.127,2.807c1.15,0,2.31-0.19,3.431-0.58 + c3.66-1.273,6.315-4.467,6.898-8.299l74.678-491.105C510.051,7.42,507.628,2.915,503.407,0.963z M434.512,45.265l-279.03,206.867 + l-116.834-61.51L434.512,45.265z M166.877,269.699l261.434-193.82L221.954,313.315c-0.386,0.444-0.718,0.915-1.016,1.401 + c-0.002-0.003-0.004-0.004-0.007-0.007l-54.055,88.198V269.699H166.877z M188.896,406.972l43.17-70.439l27.465,25.613 + L188.896,406.972z M416.988,480.407L244.345,319.408l238.93-274.914L416.988,480.407z"/> <g> </g> <g>
default/favicon.svg view
@@ -1,64 +1,27 @@ <?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"/> + viewBox="0 0 512.003 512.003" style="enable-background:new 0 0 512.003 512.003;" xml:space="preserve"> +<polygon style="fill:#0068FF;" points="276.547,363.724 156.428,439.952 229.841,320.17 "/> <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"/> + <polygon style="fill:#10BAFC;" points="424.343,501.551 229.841,320.17 499.021,10.446 "/> + <polygon style="fill:#10BAFC;" points="156.428,264.439 12.979,188.915 499.021,10.446 "/> </g> +<polygon style="fill:#0084FF;" points="156.428,264.439 156.428,439.952 229.841,320.17 499.021,10.446 "/> +<path d="M409.804,197.83c-2.509,0-5.026-0.898-7.027-2.72c-4.269-3.883-4.582-10.491-0.699-14.76l0.164-0.181 + c3.883-4.268,10.493-4.582,14.76-0.698c4.27,3.883,4.582,10.491,0.699,14.76l-0.164,0.181 + C415.475,196.679,412.644,197.83,409.804,197.83z"/> +<path d="M318.886,298.027c-2.505,0-5.018-0.895-7.019-2.71c-4.274-3.879-4.594-10.487-0.716-14.759l68.202-75.162 + c3.877-4.274,10.487-4.594,14.759-0.717c4.274,3.878,4.594,10.487,0.717,14.759L326.627,294.6 + C324.565,296.871,321.731,298.027,318.886,298.027z"/> +<path d="M503.407,0.963c-2.579-1.193-5.448-1.251-7.987-0.32c0-0.002,0-0.003-0.001-0.005L9.377,179.106 + c-3.876,1.423-6.551,4.996-6.823,9.116c-0.275,4.12,1.904,8.015,5.558,9.939l137.867,72.584v169.208 + c0,4.679,3.111,8.787,7.612,10.057c0.94,0.265,1.895,0.393,2.837,0.393c1.992,0,3.923-0.589,5.586-1.644 + c0.004,0.006,0.008,0.01,0.014,0.017l113.3-71.9l141.89,132.319c1.966,1.833,4.523,2.807,7.127,2.807c1.15,0,2.31-0.19,3.431-0.58 + c3.66-1.273,6.315-4.467,6.898-8.299l74.678-491.105C510.051,7.42,507.628,2.915,503.407,0.963z M434.512,45.265l-279.03,206.867 + l-116.834-61.51L434.512,45.265z M166.877,269.699l261.434-193.82L221.954,313.315c-0.386,0.444-0.718,0.915-1.016,1.401 + c-0.002-0.003-0.004-0.004-0.007-0.007l-54.055,88.198V269.699H166.877z M188.896,406.972l43.17-70.439l27.465,25.613 + L188.896,406.972z M416.988,480.407L244.345,319.408l238.93-274.914L416.988,480.407z"/> <g> </g> <g>
− default/index.md
@@ -1,7 +0,0 @@-# 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
default/index.yaml view
@@ -81,6 +81,11 @@ headHtml: | <meta name="generator" content="Emanote" /> + # Put anything that should below the <body> of these routes here:+ # You can reference other metadata keys using <snippet var=".." />+ # Or use the JS behaviour library below.+ bodyHtml: |+ # Builtin JS behaviour library. Use them in `page.headHtml` of your .yaml or .md # frontmatter. js:@@ -106,9 +111,9 @@ # 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});+ <script type="module">+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';+ mermaid.initialize({ startOnLoad: false }); mermaid.init(undefined,document.querySelectorAll(".mermaid")); </script> mathjax: |
default/templates/base.tpl view
@@ -23,13 +23,53 @@ </ema:metadata> <tailwindCssShim /> - <!-- Heist error element --> <style>+ /* Heist error element */ strong.error { color: lightcoral; font-size: 90%; font-family: monospace; }++ /* External link icon */+ a[data-linkicon=""]::after {+ content: ""+ }++ a[data-linkicon=none]::after {+ content: ""+ }++ a[data-linkicon="external"]::after {+ content: url('data:image/svg+xml,\+ <svg xmlns="http://www.w3.org/2000/svg" height="0.7em" viewBox="0 0 20 20"> \+ <g style="stroke:gray;stroke-width:1"> \+ <line x1="5" y1="5" x2="5" y2="14" /> \+ <line x1="14" y1="9" x2="14" y2="14" /> \+ <line x1="5" y1="14" x2="14" y2="14" /> \+ <line x1="5" y1="5" x2="9" y2="5" /> \+ <line x1="10" y1="2" x2="17" y2="2" /> \+ <line x1="17" y1="2" x2="17" y2="9" /> \+ <line x1="10" y1="9" x2="17" y2="2" style="stroke-width:1.0" /> \+ </g> \+ </svg>');+ }++ a[data-linkicon="external"][href^="mailto:"]::after {+ content: url('data:image/svg+xml,\+ <svg \+ xmlns="http://www.w3.org/2000/svg" \+ height="0.7em" \+ fill="none" \+ viewBox="0 0 24 24" \+ stroke="gray" \+ stroke-width="2"> \+ <path \+ stroke-linecap="round" \+ stroke-linejoin="round" \+ d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> \+ </svg>');+ } </style> <apply template="/templates/hooks/more-head" /> @@ -46,6 +86,9 @@ <body class="${bodyClass}"> <body-main /> <apply template="components/stork/stork-search" />+ <ema:metadata>+ <snippet var="page.bodyHtml" />+ </ema:metadata> </body> </html>
default/templates/components/pandoc.tpl view
default/templates/filters/query-timeline.tpl view
@@ -7,9 +7,9 @@ <ul> <result> <li>- <div class="flex flex-wrap my-2">+ <div class="flex flex-wrap flex-row my-2"> <ema:note:metadata>- <span data-nosnippet class="mr-2 text-right text-gray-600">+ <span data-nosnippet class="mr-2 text-right font-mono text-gray-600"> <value var="date" /> </span> </ema:note:metadata>
default/templates/special/base.tpl view
@@ -5,9 +5,6 @@ <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>
emanote.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: emanote-version: 0.8.0.0+version: 1.0.0.0 license: AGPL-3.0-only copyright: 2022 Sridhar Ratnakumar maintainer: srid@srid.ca@@ -13,6 +13,7 @@ -- A URL where users can report bugs. bug-reports: https://github.com/EmaApps/emanote/issues extra-source-files:+ CHANGELOG.md LICENSE README.md @@ -27,10 +28,13 @@ _emanote-static/stork/stork.js _emanote-static/stork/stork.wasm favicon.svg- index.md index.yaml templates/**/*.tpl +flag ghcid+ default: False+ manual: True+ common haskell-common ghc-options: -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns@@ -84,7 +88,6 @@ 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@@ -94,20 +97,21 @@ , blaze-html , bytestring , commonmark- , commonmark-extensions+ , commonmark-extensions <0.2.3.3 , commonmark-pandoc , commonmark-simple+ , commonmark-wikilink , containers , data-default , dependent-sum , directory- , ema >=0.8.2+ , ema >=0.9 , filepath , filepattern , fsnotify , hedgehog- , heist-emanote >=1.2.1- , heist-extra+ , heist >=1.1.1.0+ , heist-extra >=0.2.0.0 , hspec , hspec-hedgehog , ixset-typed >=0.5.1.0@@ -149,10 +153,9 @@ , xmlhtml , yaml - hs-source-dirs: src- library import: library-common+ hs-source-dirs: src exposed-modules: Emanote Emanote.CLI@@ -160,13 +163,11 @@ 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@@ -175,11 +176,11 @@ Emanote.Model.Title Emanote.Model.Type Emanote.Pandoc.BuiltinFilters+ Emanote.Pandoc.ExternalLink 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@@ -203,12 +204,38 @@ Emanote.View.TagIndex Emanote.View.TaskIndex Emanote.View.Template- Paths_emanote- Spec + other-modules: Paths_emanote autogen-modules: Paths_emanote +-- To profile: ghc-options: -fexternal-interpreter -prof -fprof-auto -rtsopts executable emanote- import: library-common- main-is: Main.hs- ghc-options: -threaded -rtsopts -with-rtsopts=-N+ import: library-common+ hs-source-dirs: exe+ main-is: Main.hs+ ghc-options: -threaded -rtsopts -with-rtsopts=-N++ if flag(ghcid)+ hs-source-dirs: src++ else+ build-depends: emanote++test-suite test+ import: library-common + type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends:+ , hspec+ , relude++ if flag(ghcid)+ hs-source-dirs: src++ else+ build-depends: emanote+ other-modules:+ Emanote.Model.Link.RelSpec+ Emanote.Model.QuerySpec+ Emanote.Pandoc.ExternalLinkSpec
+ exe/Main.hs view
@@ -0,0 +1,15 @@+module Main where++import Emanote qualified (defaultEmanoteConfig, run)+import Emanote.CLI qualified as CLI+import Emanote.Source.Dynamic (emanoteCompileTailwind)+import Main.Utf8 (withUtf8)+import Optics.Core ((.~))+import Relude++main :: IO ()+main =+ withUtf8 $ do+ cli <- CLI.parseCli+ let config = Emanote.defaultEmanoteConfig cli & emanoteCompileTailwind .~ True+ Emanote.run config
src/Emanote/CLI.hs view
@@ -19,7 +19,6 @@ data Cli = Cli { layers :: NonEmpty FilePath,- test :: Bool, allowBrokenLinks :: Bool, cmd :: Cmd }@@ -31,7 +30,6 @@ 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
src/Emanote/Model/Graph.hs view
@@ -1,5 +1,6 @@ module Emanote.Model.Graph where +import Commonmark.Extensions.WikiLink qualified as WL import Data.IxSet.Typed ((@+), (@=)) import Data.IxSet.Typed qualified as Ix import Data.Map.Strict qualified as Map@@ -11,7 +12,6 @@ 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 ((^.))
src/Emanote/Model/Link/Rel.hs view
@@ -3,6 +3,7 @@ module Emanote.Model.Link.Rel where +import Commonmark.Extensions.WikiLink qualified as WL import Data.Aeson (ToJSON) import Data.IxSet.Typed (Indexable (..), IxSet, ixFun, ixList) import Data.IxSet.Typed qualified as Ix@@ -10,7 +11,6 @@ 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@@ -74,7 +74,8 @@ unresolvedRelsTo :: ModelRoute -> [UnresolvedRelTarget] unresolvedRelsTo r =- let wls = either (R.withLmlRoute WL.allowedWikiLinks) WL.allowedWikiLinks $ R.modelRouteCase r+ let allowedWikiLinks = WL.allowedWikiLinks . R.unRoute+ wls = either (R.withLmlRoute allowedWikiLinks) allowedWikiLinks $ R.modelRouteCase r in (URTWikiLink <$> toList wls) <> [URTResource r]
− src/Emanote/Model/Link/RelSpec.hs
@@ -1,32 +0,0 @@-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
src/Emanote/Model/Link/Resolve.hs view
@@ -1,11 +1,11 @@ module Emanote.Model.Link.Resolve where +import Commonmark.Extensions.WikiLink qualified as WL 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
src/Emanote/Model/Note.hs view
@@ -5,6 +5,8 @@ module Emanote.Model.Note where +import Commonmark.Extensions.WikiLink (plainify)+import Commonmark.Extensions.WikiLink qualified as WL import Control.Monad.Logger (MonadLogger) import Control.Monad.Writer (MonadWriter (tell), WriterT, runWriterT) import Data.Aeson qualified as Aeson@@ -19,8 +21,6 @@ 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)@@ -87,7 +87,7 @@ routeSelfRefs :: R.LMLRoute -> NonEmpty WL.WikiLink routeSelfRefs = fmap snd- . R.withLmlRoute WL.allowedWikiLinks+ . R.withLmlRoute (WL.allowedWikiLinks . R.unRoute) noteAncestors :: Note -> [RAncestor] noteAncestors =
− src/Emanote/Model/QuerySpec.hs
@@ -1,19 +0,0 @@-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)
src/Emanote/Model/StaticFile.hs view
@@ -3,10 +3,10 @@ module Emanote.Model.StaticFile where +import Commonmark.Extensions.WikiLink qualified as WL 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@@ -34,6 +34,7 @@ staticFileSelfRefs = fmap snd . WL.allowedWikiLinks+ . R.unRoute . _staticFileRoute makeLenses ''StaticFile
src/Emanote/Model/Title.hs view
@@ -15,8 +15,8 @@ ) where +import Commonmark.Extensions.WikiLink (plainify) 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
src/Emanote/Model/Type.hs view
@@ -4,6 +4,7 @@ module Emanote.Model.Type where +import Commonmark.Extensions.WikiLink qualified as WL import Data.Aeson qualified as Aeson import Data.Default (Default (def)) import Data.IxSet.Typed ((@=))@@ -32,7 +33,6 @@ 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@@ -109,7 +109,8 @@ modelNotes %~ ( Ix.updateIx r note -- Insert folder placeholder automatically for ancestor paths- >>> flip (foldr injectAncestor) (N.noteAncestors note)+ >>> injectAncestors (N.noteAncestors note)+ >>> dropRedundantAncestor r ) >>> modelRels %~ updateIxMulti r (Rel.noteRels note)@@ -120,6 +121,41 @@ where r = note ^. N.noteRoute +-- | If a placeholder route was added already, but the newly added note is a+-- non-Markdown, removce that markdown placeholder route.+dropRedundantAncestor :: LMLRoute -> IxNote -> IxNote+dropRedundantAncestor recentNoteRoute ns =+ case recentNoteRoute of+ R.LMLRoute_Md _ -> ns+ R.LMLRoute_Org r ->+ case N.lookupNotesByRoute (R.LMLRoute_Md $ coerce r) ns of+ Nothing -> ns+ Just placeholderNote -> Ix.deleteIx (placeholderNote ^. N.noteRoute) ns++injectAncestors :: [N.RAncestor] -> IxNote -> IxNote+injectAncestors ancs' =+ case nonEmpty ancs' of+ Nothing ->+ injectRoot+ Just ancs ->+ flip (foldr injectAncestor) ancs++-- 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+restoreAncestor :: Maybe N.RAncestor -> IxNote -> IxNote+restoreAncestor =+ maybe injectRoot injectAncestor++injectRoot :: IxNote -> IxNote+injectRoot ns =+ case resolveLmlRouteIfExists ns idxR of+ Just _ -> ns+ Nothing ->+ let r = R.defaultLmlRoute idxR+ in Ix.updateIx r (N.ancestorPlaceholderNote $ coerce idxR) ns+ where+ idxR = R.indexRoute+ injectAncestor :: N.RAncestor -> IxNote -> IxNote injectAncestor (N.unRAncestor -> folderR) ns = case resolveLmlRouteIfExists ns folderR of@@ -133,9 +169,7 @@ 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+ >>> restoreAncestor (N.RAncestor <$> mFolderR) ) & modelRels %~ deleteIxMulti k@@ -150,8 +184,6 @@ 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 ::@@ -260,6 +292,7 @@ resolveLmlRoute model r = fromMaybe (R.defaultLmlRoute r) $ resolveLmlRouteIfExists (model ^. modelNotes) r +-- | Lookup a LML route, returning the less popular LML format if there are ambiguities. resolveLmlRouteIfExists :: forall ext. IxNote -> R ext -> Maybe LMLRoute resolveLmlRouteIfExists notes r = do -- TODO: Refactor using `[minBound..maxBound] :: [LML]`
src/Emanote/Pandoc/BuiltinFilters.hs view
@@ -5,6 +5,7 @@ where import Emanote.Model.Note qualified as N+import Emanote.Pandoc.ExternalLink (setExternalLinkIcon) import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as HT import Emanote.Route (encodeRoute) import Emanote.Route.SiteRoute.Type (encodeTagIndexR)@@ -22,6 +23,7 @@ preparePandoc = linkifyInlineTags >>> fixEmojiFontFamily+ >>> setExternalLinkIcon -- HashTag.hs generates a Span for inline tags. -- Here, we must link them to the special tag index page.@@ -40,7 +42,7 @@ tagUrl = toText . encodeRoute . encodeTagIndexR . toList . HT.deconstructTag --- Undo font-familly on emoji spans, so the browser uses an emoji font.+-- Undo font-family 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 =
+ src/Emanote/Pandoc/ExternalLink.hs view
@@ -0,0 +1,53 @@+module Emanote.Pandoc.ExternalLink+ ( setExternalLinkIcon,+ )+where++import Relude+import Text.Pandoc.Definition qualified as B+import Text.Pandoc.Walk qualified as W+import Text.Parsec qualified as P+import Text.Parsec.Char qualified as PC++-- Add a data-linkicon=external attribute to external links that contain some+-- text in their description, provided that they do not already have a+-- data-linkicon attribute.+setExternalLinkIcon :: W.Walkable B.Inline b => b -> b+setExternalLinkIcon =+ W.walk $ \case+ B.Link (id', classes, attrs) inlines (url, title)+ | hasURIScheme url && containsText inlines ->+ let showLinkIconAttr = ("data-linkicon", "external")+ newAttrs = insert attrs showLinkIconAttr+ in B.Link (id', classes, newAttrs) inlines (url, title)+ x -> x+ where+ -- Inserts an element in a key-value list if the element's key is not+ -- already in the list.+ insert :: Eq a => [(a, b)] -> (a, b) -> [(a, b)]+ insert as a+ | fst a `elem` (fst <$> as) = as+ | otherwise = a : as+ -- Checks whether the given text begins with an RFC 3986 compliant URI+ -- scheme.+ hasURIScheme :: Text -> Bool+ hasURIScheme =+ isRight . P.parse schemeP ""+ where+ schemeP = do+ c <- PC.letter+ cs <- P.many $ PC.alphaNum P.<|> P.oneOf ".-+"+ void $ PC.char ':'+ return (c : cs)+ -- Checks whether a list of inlines contains a (perhaps nested) "textual+ -- element", understood as a Pandoc `Str`, `Code` or `Math`.+ containsText :: [B.Inline] -> Bool+ containsText =+ getAny+ . W.query+ ( \case+ B.Str _ -> Any True+ B.Code _ _ -> Any True+ B.Math _ _ -> Any True+ _ -> Any False+ )
src/Emanote/Pandoc/Link.hs view
@@ -1,6 +1,6 @@ module Emanote.Pandoc.Link where -import Emanote.Pandoc.Markdown.Syntax.WikiLink qualified as WL+import Commonmark.Extensions.WikiLink qualified as WL import Relude import Text.Pandoc.Definition qualified as B
src/Emanote/Pandoc/Markdown/Parser.hs view
@@ -5,11 +5,11 @@ import Commonmark qualified as CM import Commonmark.Extensions qualified as CE+import Commonmark.Extensions.WikiLink qualified as WL 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)
− src/Emanote/Pandoc/Markdown/Syntax/WikiLink.hs
@@ -1,275 +0,0 @@-{-# 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.- _ -> ""
src/Emanote/Pandoc/Renderer/Embed.hs view
@@ -1,5 +1,6 @@ module Emanote.Pandoc.Renderer.Embed where +import Commonmark.Extensions.WikiLink qualified as WL import Data.Map.Syntax ((##)) import Data.Text qualified as T import Emanote.Model (Model)@@ -10,7 +11,6 @@ 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
src/Emanote/Pandoc/Renderer/Url.hs view
@@ -5,6 +5,7 @@ ) where +import Commonmark.Extensions.WikiLink qualified as WL import Data.Text qualified as T import Emanote.Model (Model) import Emanote.Model qualified as M@@ -13,7 +14,6 @@ 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
− src/Main.hs
@@ -1,28 +0,0 @@-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
− src/Spec.hs
@@ -1,12 +0,0 @@-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
+ test/Emanote/Model/Link/RelSpec.hs view
@@ -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
+ test/Emanote/Model/QuerySpec.hs view
@@ -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)
+ test/Emanote/Pandoc/ExternalLinkSpec.hs view
@@ -0,0 +1,56 @@+module Emanote.Pandoc.ExternalLinkSpec where++import Emanote.Pandoc.ExternalLink (setExternalLinkIcon)+import Emanote.Pandoc.Markdown.Parser (parseMarkdown)+import Hedgehog+import Relude+import Test.Hspec+import Test.Hspec.Hedgehog (hedgehog)+import Text.Pandoc.Definition (Inline (..))+import Text.Pandoc.Walk qualified as W++spec :: Spec+spec = do+ describe "setExternalLinkIcon" $ do+ it "respects user-specified data-linkicon attribute" . hedgehog $ do+ getDataLinkIconAttrs "[test](https://www.test.com){data-linkicon=abc}" === Right ["abc"]+ getDataLinkIconAttrs "[test](https://www.test.com){data-linkicon=\"\"}" === Right [""]+ getDataLinkIconAttrs "[[foo]]{data-linkicon=external}" === Right ["external"]+ getDataLinkIconAttrs "[abc](x/y/z){data-linkicon=none}" === Right ["none"]+ it "does not add attribute if link is internal" . hedgehog $ do+ getDataLinkIconAttrs "[[foo]]" === Right []+ getDataLinkIconAttrs "[abc](x/y/z)" === Right []+ getDataLinkIconAttrs "![[image.jpg]]" === Right []+ getDataLinkIconAttrs "[](foo/bar/baz)" === Right []+ getDataLinkIconAttrs "[[bar|baz]]" === Right []+ getDataLinkIconAttrs "[`abc`](./test.txt)" === Right []+ getDataLinkIconAttrs "[$$abc$$](/just/test#ing)" === Right []+ getDataLinkIconAttrs "#tag" === Right []+ getDataLinkIconAttrs "[[abc|def]]" === Right []+ it "adds attribute if link is external and its description contains text, code or math" . hedgehog $ do+ getDataLinkIconAttrs "[Emanote](https://github.com/EmaApps/emanote)" === Right ["external"]+ getDataLinkIconAttrs "[`text`](http://somehost:1234/test)" === Right ["external"]+ getDataLinkIconAttrs "[$$E=mc^2$$](ssh://user@host.abc/~/path/)" === Right ["external"]+ getDataLinkIconAttrs "[![[picture.png]] ~~**`code`**~~](scheme://host:port/path?query)" === Right ["external"]+ getDataLinkIconAttrs "[==$$e^{i \\pi} + 1 = 0$$== ![[image.svg]]](git://host.xz/path/to/repo.git/)" === Right ["external"]+ getDataLinkIconAttrs "example@example.com" === Right ["external"]+ getDataLinkIconAttrs "https://www.test.gov" === Right ["external"]+ getDataLinkIconAttrs "[**![[image.jpg]] *qwerty* ![[image.jpg]]**](doi:10.1000/182)" === Right ["external"]+ getDataLinkIconAttrs "[:video_game:](bolo://hostname/)" === Right ["external"]+ it "does not add attribute if link description contains no text, code or math" . hedgehog $ do+ getDataLinkIconAttrs "[](http://nothing.interesting.here)" === Right []+ getDataLinkIconAttrs "[![[image.png]]](https://www.example.com)" === Right []+ getDataLinkIconAttrs "[==*_**~~![[img.png]]~~**_*==](http://something.info)" === Right []++-- | Extract "data-linkicon" attributes present in the given Markdown content.+getDataLinkIconAttrs :: Text -> Either Text [Text]+getDataLinkIconAttrs =+ fmap (W.query $ getLinkAttr "data-linkicon") . parseEmanoteMarkdown+ where+ parseEmanoteMarkdown = fmap (setExternalLinkIcon . snd) . parseMarkdown "<test>"++getLinkAttr :: Text -> Inline -> [Text]+getLinkAttr name (Link (_, _, attrs) _ (_, _)) =+ snd <$> filter ((== name) . fst) attrs+getLinkAttr _ _ =+ []
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}