packages feed

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 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 @@ [![built with nix](https://img.shields.io/badge/Built_With-Nix-5277C3.svg?logo=nixos&labelColor=73C3D5)](https://builtwithnix.org) [![Matrix](https://img.shields.io/matrix/ema:matrix.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 "[![](path/to/image.png)](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 #-}