├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml └── workflows │ ├── ci.yaml │ └── publish.yaml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── README.md └── neuron.svg ├── bin ├── format ├── repl ├── run ├── run-via-tmux └── test ├── ci.nix ├── default.nix ├── dep ├── README.md ├── directory-contents │ ├── default.nix │ ├── github.json │ └── thunk.nix ├── nix-filter │ ├── default.nix │ ├── github.json │ └── thunk.nix ├── nix-thunk │ ├── default.nix │ ├── github.json │ └── thunk.nix ├── pandoc-link-context │ ├── default.nix │ ├── github.json │ └── thunk.nix ├── reflex-dom-pandoc │ ├── default.nix │ ├── github.json │ └── thunk.nix └── reflex-fsnotify │ ├── default.nix │ ├── github.json │ └── thunk.nix ├── doc ├── .nojekyll ├── .vscode │ ├── extensions.json │ └── settings.json ├── Examples.md ├── Guide.md ├── Guide │ ├── Creating and Editing zettels.md │ ├── Creating and Editing zettels │ │ ├── Cerveau.md │ │ └── Editor integration.md │ ├── Plugins │ │ ├── Directory Tree.md │ │ ├── Ignoring files.md │ │ ├── Linking.md │ │ ├── Linking │ │ │ └── Folgezettel Links.md │ │ ├── Tags.md │ │ ├── Tags │ │ │ └── Tag Queries.md │ │ ├── Uplink Tree.md │ │ └── Web feeds.md │ ├── Querying with JSON output.md │ ├── Searching your Zettelkasten.md │ ├── Web interface.md │ ├── Web interface │ │ ├── Automatic Publishing.md │ │ ├── Configuration file.md │ │ ├── Customizing the generated website.md │ │ ├── Customizing the generated website │ │ │ ├── Custom JavaScript and CSS.md │ │ │ └── Custom JavaScript and CSS │ │ │ │ ├── chartjs.md │ │ │ │ ├── graphviz.md │ │ │ │ ├── mermaid.md │ │ │ │ └── tailwind.md │ │ ├── Graph visualization.md │ │ └── Graph visualization │ │ │ ├── Clusters.md │ │ │ ├── Folgezettel Heterarchy.md │ │ │ └── impulse-feature.md │ ├── Zettel ID.md │ ├── Zettel Markdown.md │ ├── Zettel Markdown │ │ ├── Adding images and other files.md │ │ ├── Code Syntax Highlighting.md │ │ ├── Math support.md │ │ ├── Using raw HTML in Markdown.md │ │ └── Zettel metadata.md │ └── extras.md ├── Installing.md ├── Installing │ ├── Declarative Install.md │ ├── Docker workflow.md │ ├── Static binary.md │ └── home-manager systemd service.md ├── Neuron v1.md ├── Philosophy.md ├── Plugins.md ├── Tutorial.md ├── Zettelkasten.md ├── Zettelkasten │ ├── Atomic and autonomous.md │ └── Heterarchy.md ├── index.md ├── neuron.dhall ├── next.md └── static │ ├── cerveau-autocompl.gif │ └── vscode-title-id.gif ├── docker.nix ├── exe └── Main.hs ├── flake.lock ├── flake.nix ├── hie.yaml ├── home-manager-module.nix ├── neuron-search ├── neuron.cabal ├── nixpkgs.nix ├── project.nix ├── shell.nix ├── src ├── Data │ ├── Graph │ │ ├── Labelled.hs │ │ └── Labelled │ │ │ ├── Algorithm.hs │ │ │ ├── Build.hs │ │ │ └── Type.hs │ ├── PathTree.hs │ ├── Structured │ │ ├── Breadcrumb.hs │ │ ├── OpenGraph.hs │ │ └── OpenGraph │ │ │ └── Render.hs │ ├── TagTree.hs │ ├── Time │ │ └── DateMayTime.hs │ └── YAML │ │ └── ToJSON.hs ├── Neuron │ ├── Backend.hs │ ├── CLI │ │ ├── App.hs │ │ ├── Logging.hs │ │ ├── New.hs │ │ ├── Open.hs │ │ ├── Parser.hs │ │ ├── Query.hs │ │ ├── Search.hs │ │ └── Types.hs │ ├── Cache.hs │ ├── Cache │ │ └── Type.hs │ ├── Config.hs │ ├── Config │ │ └── Type.hs │ ├── Frontend │ │ ├── CSS.hs │ │ ├── Common.hs │ │ ├── Impulse.hs │ │ ├── Manifest.hs │ │ ├── Route.hs │ │ ├── Route │ │ │ ├── Data.hs │ │ │ └── Data │ │ │ │ └── Types.hs │ │ ├── Static │ │ │ ├── HeadHtml.hs │ │ │ ├── Html.hs │ │ │ └── StructuredData.hs │ │ ├── Theme.hs │ │ ├── View.hs │ │ ├── Widget.hs │ │ ├── Widget │ │ │ └── InvertedTree.hs │ │ └── Zettel │ │ │ ├── CSS.hs │ │ │ └── View.hs │ ├── LSP.hs │ ├── Markdown.hs │ ├── Plugin.hs │ ├── Plugin │ │ ├── Plugins │ │ │ ├── DirTree.hs │ │ │ ├── Feed.hs │ │ │ ├── Links.hs │ │ │ ├── NeuronIgnore.hs │ │ │ ├── Tags.hs │ │ │ └── UpTree.hs │ │ └── Type.hs │ ├── Reactor.hs │ ├── Reactor │ │ └── Build.hs │ ├── Version.hs │ └── Zettelkasten │ │ ├── Connection.hs │ │ ├── Graph.hs │ │ ├── Graph │ │ ├── Build.hs │ │ └── Type.hs │ │ ├── ID.hs │ │ ├── ID │ │ └── Scheme.hs │ │ ├── Query.hs │ │ ├── Query │ │ └── Graph.hs │ │ ├── Resolver.hs │ │ ├── Zettel.hs │ │ └── Zettel │ │ ├── Error.hs │ │ └── Parser.hs ├── Options │ └── Applicative │ │ └── Extra.hs ├── System │ └── Directory │ │ └── Contents │ │ └── Extra.hs └── Text │ ├── Megaparsec │ └── Simple.hs │ ├── Pandoc │ └── Util.hs │ └── URI │ └── Util.hs ├── static.nix └── test ├── Data ├── Graph │ └── Labelled │ │ └── AlgorithmSpec.hs ├── PathTreeSpec.hs └── TagTreeSpec.hs ├── Neuron ├── VersionSpec.hs └── Zettelkasten │ ├── ID │ └── SchemeSpec.hs │ ├── IDSpec.hs │ └── ZettelSpec.hs └── Spec.hs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [srid] 2 | liberapay: srid 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a bug 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - Browser [e.g. chrome, safari] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Request a feature 4 | url: https://github.com/srid/neuron/discussions/new?category=Ideas 5 | about: Features requests are managed in the Discussions area 6 | - name: Other 7 | url: https://github.com/srid/neuron/discussions/new 8 | about: General discussions are managed in the Discussions area 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest] 11 | env: 12 | MAINLINE: refs/heads/master 13 | DOCKERTAG: latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: cachix/install-nix-action@v16 17 | with: 18 | extra_nix_config: | 19 | experimental-features = nix-command flakes 20 | # This also runs nix-build. 21 | - uses: cachix/cachix-action@v10 22 | with: 23 | name: srid 24 | signingKey: "${{ secrets.CACHIX_SIGNING_KEY }}" 25 | # Only needed for private caches 26 | authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 27 | # This downloads deps from Nix cache, builds neuron, as well as run tests 28 | - name: Build 🔧 29 | run: | 30 | nix-build -j4 --no-out-link ci.nix 31 | # Test default.nix 32 | nix-build -j4 33 | # Test flake.nix 34 | nix build --out-link flake-result 35 | - name: Verify output of default.nix & flake.nix 36 | run: | 37 | [ $(readlink result) = $(readlink flake-result) ] 38 | - name: Retrieve neuron version 39 | run: | 40 | echo "NEURONVER=$(./result/bin/neuron --version)" >> $GITHUB_ENV 41 | - name: Publish Docker image to Docker Hub 42 | if: ${{ github.ref == env.MAINLINE && runner.os == 'Linux' }} 43 | run: | 44 | docker load -i $(nix-build docker.nix --argstr tag "${{ env.DOCKERTAG }}") 45 | docker tag "sridca/neuron:${{ env.DOCKERTAG }}" "sridca/neuron:${{env.NEURONVER}}" 46 | echo ${{ secrets.DOCKER_PASS }} | docker login -u sridca --password-stdin 47 | set -x 48 | docker push "sridca/neuron:${{ env.DOCKERTAG }}" 49 | docker push "sridca/neuron:${{ env.NEURONVER }}" 50 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: "Publish Neuron site" 2 | on: 3 | # Run only when pushing to master branch 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: cachix/install-nix-action@v16 13 | - uses: cachix/cachix-action@v10 14 | with: 15 | name: srid 16 | # This builds neuron, as well as run tests 17 | - name: Install neuron 18 | run: nix-env -if . 19 | - name: Build neuron site 🔧 20 | run: | 21 | neuron --version 22 | neuron -d doc/ gen --pretty-urls 23 | - name: Deploy to GitHub Pages 🚀 24 | uses: JamesIves/github-pages-deploy-action@3.7.1 25 | with: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | BRANCH: gh-pages 28 | FOLDER: doc/.neuron/output 29 | CLEAN: true 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-* 3 | .ghc.environment.* 4 | result 5 | result-2 6 | result-data 7 | .shake 8 | .neuron 9 | neuron.prof 10 | 11 | # CI 12 | tmp 13 | neuron-linux-bundle 14 | 15 | # Others 16 | .ionide -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "haskell.haskell", 6 | "arrterian.nix-env-selector", 7 | "bbenoist.nix", 8 | "b4dm4n.nixpkgs-fmt" 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnType": true, 3 | "editor.formatOnSave": true, 4 | "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix" 5 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to neuron 2 | 3 | This document describes how to develop neuron, as well as some guidelines when it comes to submiting a PR. 4 | 5 | ## Developing locally 6 | 7 | We recommend [VSCode] for developing Neuron, but any text editor supporting [haskell-language-server] will do. When opening the project in VSCode, install the two extensions ([Haskell](https://marketplace.visualstudio.com/items?itemName=haskell.haskell) and [Nix Environment Selector](https://marketplace.visualstudio.com/items?itemName=arrterian.nix-env-selector)) it recommends. 8 | 9 | **Note**: Nix Environment Selector should automatically select the `shell.nix` environment; if it doesn't, you can hit Ctrl+Shift+P in VSCode and run the command "Nix-Env: Select Environment" to select `shell.nix` manually. The extension will ask you to reload VSCode at the end. 10 | 11 | You can expect full IDE support for Haskell in VSCode from this point. 12 | 13 | [VSCode]: https://code.visualstudio.com/ 14 | [haskell-language-server]: https://github.com/haskell/haskell-language-server#editor-integration 15 | 16 | ### Instant reload 17 | 18 | When modifying the source code, use `bin/run` (which uses ghcid) to test your changes in real-time: 19 | 20 | ```bash 21 | bin/run -d ./doc gen -wS 22 | ``` 23 | 24 | This command automatically recompiles and restarts when you change any of the Haskell source files. Furthermore, this command runs site generation on the given Zettelkasten. You can pass the same neuron arguments to `bin/run`. This is essentially equivalent to running a development version of neuron with instant reload. 25 | 26 | ### Running tests 27 | 28 | Unit tests can be run via ghcid as follows: 29 | 30 | ``` 31 | bin/test 32 | ``` 33 | 34 | This too reloads when any of the source files change. 35 | 36 | ### Hacking on Pandoc's HTML layout 37 | 38 | Neuron delegates HTML rendering of the Pandoc AST to [reflex-dom-pandoc](https://github.com/srid/reflex-dom-pandoc). To hack on it, first [install nix-thunk](https://github.com/obsidiansystems/nix-thunk) and then: 39 | 40 | ```sh 41 | # This will clone the git repo of reflex-dom-pandoc at dep/reflex-dom-pandoc 42 | nix-thunk unpack dep/reflex-dom-pandoc 43 | 44 | # Let's work on that repo 45 | cd dep/reflex-dom-pandoc 46 | ``` 47 | 48 | Then you can try your changes with 49 | ``` 50 | # Run ghcid (using neuron's nix config) 51 | nix-shell ../../shell.nix --run ghcid 52 | ``` 53 | 54 | Now as you edit the reflex-dom-pandoc sources, ghcid should give you compiler feedback. Once you are done with your changes, simply re-run neuron's ghcid or bin/run (see further above) and it should reflect your changes. 55 | 56 | When you are done, commit your changes to reflex-dom-pandoc (presumably in a branch) and then `git push` it. Finally, you must "pack" the thunk and commit the changes to the neuron repo: 57 | 58 | ```sh 59 | cd ../.. # Back to neuron 60 | rm -rf dep/reflex-platform/dist-newstyle # cleanup build artifacts before packing 61 | nix-thunk pack dep/reflex-dom-pandoc 62 | git add dep/reflex-dom-pandoc 63 | ``` 64 | 65 | ## Guidelines when submitting a PR 66 | 67 | ### Update ChangeLog and version 68 | 69 | - If your PR is adding a feature or fixing a user-facing bug, add a casual entry to CHANGELOG.md mentioning the PR number. 70 | - Update the `version` field in neuron.cabal. Change `a.b.c.d` to `a.b.(c+1).0`. 71 | 72 | ### Autoformatting 73 | 74 | Run the `bin/format` script to auto-format your Haskell source changes using [ormolu](https://github.com/tweag/ormolu). You don't need to do this when using VSCode which is configured to auto-format on save. 75 | 76 | ### Test your build 77 | 78 | Run `nix-build` with your changes to make sure that everything compiles, and the tests succeed. 79 | 80 | #### Installing from source 81 | 82 | `nix-build` will produce a binary under `./result/bin/neuron`. You can also install directly from source using `nix-env -if .`. 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # neuron 4 | 5 | [![AGPL](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://en.wikipedia.org/wiki/Affero_General_Public_License) 6 | [![built with nix](https://img.shields.io/badge/Built_With-Nix-5277C3.svg?logo=nixos&labelColor=73C3D5)](https://builtwithnix.org) 7 | [![FAIR](https://img.shields.io/badge/FAIR-pledge-blue)](https://www.fairforall.org/about/) 8 | [![Matrix](https://img.shields.io/matrix/neuron:matrix.org)](https://app.element.io/#/room/#neuron:matrix.org "Chat on Matrix") 9 | [![Liberapay](https://img.shields.io/liberapay/patrons/srid.svg?logo=liberapay)](https://liberapay.com/srid/donate "Donate using liberapay") 10 | 11 | neuron is a **future-proof** app for managing your plain-text notes in [Zettelkasten](https://neuron.zettel.page/zettelkasten) style, as well as for **publishing** them on the web. Read its [philosophy](https://neuron.zettel.page/philosophy). 12 | 13 |
14 | Checkout Emanote, neuron's successor! 15 | Neuron is being superceded by Emanote: https://github.com/srid/emanote 16 |
17 | 18 | **Highlights** 19 | 20 | - Work with a directory of markdown files 21 | - Powerful linking syntax and hierarchical tagging 22 | - Auto-generated static web site (see [examples](https://neuron.zettel.page/examples)) 23 | - Simple to use, with optional editor integration ([emacs, vim, vscode, etc.](https://neuron.zettel.page/editor)) 24 | 25 | ## Getting started 26 | 27 | See https://neuron.zettel.page/install to install and use neuron locally. Or start from [neuron-template](https://github.com/srid/neuron-template) if you just want to publish a neuron site using GitHub Pages. 28 | 29 | ## Developing 30 | 31 | Development documentation, including instructions to install from source, is available in [CONTRIBUTING.md](https://github.com/srid/neuron/blob/master/CONTRIBUTING.md). 32 | 33 | ## Discussions 34 | 35 | To discuss and propose ideas, visit the [Discussions tab](https://github.com/srid/neuron/discussions) on GitHub. To chat about the project join [Matrix](https://app.element.io/#/room/#neuron:matrix.org). 36 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | ## Credits 2 | 3 | Logo from https://www.svgrepo.com/ 4 | -------------------------------------------------------------------------------- /assets/neuron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 44 | 45 | 47 | 51 | 55 | 56 | 62 | 68 | 74 | 80 | 86 | 92 | 98 | 104 | 106 | 112 | 118 | 119 | 121 | 122 | 124 | 125 | 127 | 128 | 130 | 131 | 133 | 134 | 136 | 137 | 139 | 140 | 142 | 143 | 145 | 146 | 148 | 149 | 151 | 152 | 154 | 155 | 157 | 158 | 160 | 161 | 163 | 164 | -------------------------------------------------------------------------------- /bin/format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | nix-shell --pure --run "find {src,exe,test} -name \*.hs | xargs ormolu -m inplace" 4 | nix-shell --pure --run "nixpkgs-fmt *.nix" 5 | -------------------------------------------------------------------------------- /bin/repl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | nix-shell --run 'cabal new-repl neuron:lib:neuron' 4 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | nix-shell --pure --run "ghcid --warnings -c 'cabal new-repl exe:neuron --flags=ghcid' -T \":main $*\"" 4 | -------------------------------------------------------------------------------- /bin/run-via-tmux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | PROJECT=$(basename `pwd`) 4 | exec tmux new-session -A -s $PROJECT bin/run -d ./doc gen -ws :9003 5 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | nix-shell --pure --run "ghcid $* -c 'cabal new-repl test:neuron-test --flags=ghcid' -T \":main $*\"" 4 | -------------------------------------------------------------------------------- /ci.nix: -------------------------------------------------------------------------------- 1 | { system ? builtins.currentSystem }: 2 | let 3 | pkgs = import ./nixpkgs.nix { inherit system; }; 4 | in 5 | pkgs.recurseIntoAttrs { 6 | # Build both default.nix and shell.nix such that both derivations are 7 | # pushed to cachix. This allows the development workflow (bin/run, etc.) to 8 | # use cachix to full extent. 9 | neuron = import ./default.nix; 10 | neuronShell = import ./shell.nix; 11 | } 12 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { 12 | src = ./.; 13 | }).defaultNix 14 | -------------------------------------------------------------------------------- /dep/README.md: -------------------------------------------------------------------------------- 1 | These dependencies are to be managed by `nix-thunk`, and not hand-edited. 2 | -------------------------------------------------------------------------------- /dep/directory-contents/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/directory-contents/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "srid", 3 | "repo": "directory-contents", 4 | "branch": "sans-tests--pathaIsSymbolicLinkFix--witherable04", 5 | "private": false, 6 | "rev": "0d3f1d5c86063232a3ccf081d9be143eb2ff1466", 7 | "sha256": "1zmxs2acj1nhdwpn62ksrihmpwyf1dza9iiqhkm41c4m025v6q82" 8 | } 9 | -------------------------------------------------------------------------------- /dep/directory-contents/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /dep/nix-filter/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/nix-filter/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "numtide", 3 | "repo": "nix-filter", 4 | "private": false, 5 | "rev": "3c9e33ed627e009428197b07216613206f06ed80", 6 | "sha256": "19w142crrkywxynmyw4rhz4nglrg64yjawfkw3j91qwkwbfjds84" 7 | } 8 | -------------------------------------------------------------------------------- /dep/nix-filter/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /dep/nix-thunk/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/nix-thunk/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "obsidiansystems", 3 | "repo": "nix-thunk", 4 | "private": false, 5 | "rev": "bab7329163fce579eaa9cfba67a4851ab806b76f", 6 | "sha256": "0wn96xn6prjzcsh4n8p1n40wi8la53ym5h2frlqbfzas7isxwygg" 7 | } 8 | -------------------------------------------------------------------------------- /dep/nix-thunk/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /dep/pandoc-link-context/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/pandoc-link-context/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "srid", 3 | "repo": "pandoc-link-context", 4 | "branch": "master", 5 | "private": false, 6 | "rev": "71e4061789884bc3030a9686add9b7fa58aea14e", 7 | "sha256": "1ww1ccsmdmx8ljrs911ind86wna2fg471flazblg59ag6xm9k5wc" 8 | } 9 | -------------------------------------------------------------------------------- /dep/pandoc-link-context/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /dep/reflex-dom-pandoc/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/reflex-dom-pandoc/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "srid", 3 | "repo": "reflex-dom-pandoc", 4 | "branch": "master", 5 | "private": false, 6 | "rev": "b6a76c2c980a2bba9b4d170f95ef8b526ffea3a4", 7 | "sha256": "1fqpc87mfjcjn5dnh2swzasjy0vkwq3x2yqgff5ww3pdalfk22k9" 8 | } 9 | -------------------------------------------------------------------------------- /dep/reflex-dom-pandoc/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /dep/reflex-fsnotify/default.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | import (import ./thunk.nix) -------------------------------------------------------------------------------- /dep/reflex-fsnotify/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "reflex-frp", 3 | "repo": "reflex-fsnotify", 4 | "private": false, 5 | "rev": "cca674623b797dd423421dec0f1da952a1d1f36d", 6 | "sha256": "1q7mmdba2lrc8pgnqf8fif3zjprk8h5kj8l1g6gnmzqc5566qqq1" 7 | } 8 | -------------------------------------------------------------------------------- /dep/reflex-fsnotify/thunk.nix: -------------------------------------------------------------------------------- 1 | # DO NOT HAND-EDIT THIS FILE 2 | let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }: 3 | if !fetchSubmodules && !private then builtins.fetchTarball { 4 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256; 5 | } else (import {}).fetchFromGitHub { 6 | inherit owner repo rev sha256 fetchSubmodules private; 7 | }; 8 | json = builtins.fromJSON (builtins.readFile ./github.json); 9 | in fetch json -------------------------------------------------------------------------------- /doc/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srid/neuron/aff49d85dc7d9c4b011a1e5a27ffae8f7e4f5537/doc/.nojekyll -------------------------------------------------------------------------------- /doc/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Main markdown support 6 | "yzhang.markdown-all-in-one", 7 | 8 | // Zettelkasten wiki-links, backlinks, etc. 9 | "svsool.markdown-memo", 10 | 11 | // Graph view 12 | // Disabled until https://github.com/tchayen/markdown-links/issues/28 is done 13 | // "tchayen.markdown-links", 14 | 15 | // For expanding title in daily note 16 | "gruntfuggly.auto-snippet", 17 | 18 | // Commands for bold, italic, etc. 19 | "mdickin.markdown-shortcuts", 20 | 21 | // Goodies: 22 | // - Checkboxes 23 | "bierner.markdown-checkbox", 24 | // - Footnote 25 | "houkanshan.vscode-markdown-footnote" 26 | " 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /doc/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Avoid having to explicitly save notes 3 | "files.autoSave": "afterDelay", 4 | 5 | // Minimap is not useful for Markdown notes 6 | "editor.minimap.enabled": false, 7 | 8 | // Generally note files are not opened in duplicate tabs. 9 | // This also enables you to navigate to already open note in other split pane 10 | "workbench.editor.revealIfOpen": true, 11 | 12 | // For those that use daily notes, via vscode-memo extension 13 | "autoSnippet.snippets": [ 14 | { 15 | "pattern": "**/\\d{4}-\\d{2}-\\d{2}.md", 16 | "snippet": "daily" 17 | } 18 | ] 19 | 20 | // If use Git, these might be interesting: 21 | // "git.autofetch": true, 22 | // "git.postCommitCommand": "push" 23 | } 24 | -------------------------------------------------------------------------------- /doc/Examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: examples 3 | --- 4 | 5 | Here are some public Zettelkastens managed by neuron: 6 | 7 | - [neuron.zettel.page](https://neuron.zettel.page/) ([source](https://github.com/srid/neuron/tree/master/doc)): This very site you are viewing. 8 | - [alexsoto.dev](https://alexsoto.dev/) ([source](https://github.com/alex-a-soto/zettelkasten)): demonstrates social share buttons and embedded comments (via [[Custom JavaScript and CSS]]) 9 | - [EyebrowHairs](https://www.eyebrowhairs.com/) ([source](https://github.com/EyebrowHairs/garden)): demonstrates CSS styling (via [[Custom JavaScript and CSS]]) 10 | - [Alien Psychology](https://alien-psychology.zettel.page/) ([source](https://github.com/srid/alien-psychology)) (TV series cataloging) 11 | - [zk.zettel.page](https://zk.zettel.page/) ([source](https://github.com/Kuratoro/zk.zettel.page)): Public Zettelkasten for the r/Zettelkasten subreddit 12 | - [Wiki of Kalle](https://wiki.jillejr.tech/) ([source](https://github.com/jilleJr/wiki)) 13 | - [Le note di azazel](http://azazel.it/): Personal Zettelkasten in Italian. 14 | - [politicalnotes.org](https://politicalnotes.org/): demonstrates use of title IDs 15 | - [brain.jaredweakly.com](https://brain.jaredweakly.com/) 16 | - [inariksit.github.io/cclaw-zettelkasten](https://inariksit.github.io/cclaw-zettelkasten/) ([source](https://github.com/inariksit/cclaw-zettelkasten)): Zettelkasten for the reading list of CCLAW@SMU 17 | - [devonmorris.dev](https://devonmorris.dev) ([source](https://github.com/DevonMorris/zettelkasten); author's [blog post](https://devonmorris.dev/7d324c98.html) on neuron workflow) 18 | - [morg.systems](https://morg.systems) ([source](https://github.com/Morgawr/morg-zettel/)): Personal site with some guides and notes about (mostly) learning Japanese 19 | 20 | :::{.ui .message} 21 | If you are hosting your own Zettelkasten publicly and would like to be included this list, edit this page (using the link below) to open a pull request. 22 | ::: 23 | -------------------------------------------------------------------------------- /doc/Guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: guide 3 | --- 4 | 5 | **Basics**: A neuron notebook is just a directory of [[Zettel Markdown]] files, which are identified by [[Zettel ID]]. Neuron transforms it into a [[Web interface]], that is generated statically (self-contained HTML files). Advanced functionality are added via [[Plugins]]. 6 | 7 | **Managing**: See [[Creating and Editing zettels]], [[Searching your Zettelkasten]] and [[Querying with JSON output]] 8 | 9 | **Publishing**: [[Automatic Publishing]] 10 | 11 | **Extras**: [[extras]] -------------------------------------------------------------------------------- /doc/Guide/Creating and Editing zettels.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: create 3 | tags: 4 | - walkthrough 5 | --- 6 | 7 | # Creating and Editing zettels 8 | 9 | You may use any text editor with Markdown support to edit your zettel files. Neuron provides a command to create new zettel files with the suitable [[Zettel ID]]: 10 | 11 | ```bash 12 | neuron new 13 | ``` 14 | 15 | This command will print the path to the file created. Use `-e` to also open the text editor: 16 | 17 | ```bash 18 | neuron new -e 19 | ``` 20 | 21 | Of course, you can also start with an empty file and begin writing: 22 | 23 | ```bash 24 | echo "Hello world ..." > "My new note.md" 25 | ``` 26 | 27 | Do not forget to link your new zettel to the rest of your Zettelkasten. See [[Linking]]. 28 | 29 | ## Opening a Zettel by title 30 | 31 | See [[Searching your Zettelkasten]]. 32 | 33 | ## Using a text editor 34 | 35 | See [[Editor integration]]# 36 | 37 | ## Web Interface 38 | 39 | [[Cerveau]]# provides a web interface to browse and edit your Neuron v1 notes. 40 | -------------------------------------------------------------------------------- /doc/Guide/Creating and Editing zettels/Cerveau.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: cerveau 3 | --- 4 | 5 | :::{.ui .warning .message} 6 | Cerveau supports only neuron v1, not v2. 7 | ::: 8 | 9 | [**Cerveau**](https://www.cerveau.app/) is a web app for easy viewing and editing of Neuron notes backed by [Git](https://guides.github.com/introduction/git-handbook/) as storage. 10 | 11 | Cerveau is currently in public beta. Everybody can sign-in to try it out, and [Neuron sponsors](https://github.com/sponsors/srid) get full access (unlimited notes) to it, while facilitating the potential open-sourcing of Cerveau. 12 | 13 | :::{.ui .center .aligned .basic .segment} 14 | 15 | ::: 16 | 17 | See the [Cerveau announcement post](https://www.srid.ca/cerveau-announce), as well as the [Cerveau user guide](https://guide.cerveau.app/) for details. 18 | -------------------------------------------------------------------------------- /doc/Guide/Creating and Editing zettels/Editor integration.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: editor 3 | --- 4 | 5 | While you may use any text editor with neuron, the following extensions enable certain neuron-specific features on top of basic text editing. 6 | 7 | ## VSCode 8 | 9 | :::{.ui .message} 10 | VSCode is recommended for new users of neuron. Though you may also use other Zettelkasten apps that supports wiki-links (see [[Linking]]) like [Obsidian](https://obsidian.md/). 11 | ::: 12 | 13 | Use the [vscode-memo](https://github.com/svsool/vscode-memo#memo) extension when editing your Neuron notes in [Visual Studio Code](https://code.visualstudio.com/). For other useful extensions, consult the template repo ([`.vscode/extensions.json`](https://github.com/srid/neuron-template/blob/master/.vscode/extensions.json)) in [[Automatic Publishing]]. 14 | 15 | ![VSCode Gif Demo](./static/vscode-title-id.gif "Demo of editing neuron notes in VSCode"){.ui .centered .large .image} 16 | 17 | ## Vim/Neovim 18 | 19 | 20 | - [neuron-v2.vim](https://github.com/chiefnoah/neuron-v2.vim) 21 | - [neuron.nvim](https://github.com/oberblastmeister/neuron.nvim) (neovim only) 22 | - [nerveux.nvim](https://github.com/pyrho/nerveux.nvim) (neovim only) 23 | 24 | ## Emacs 25 | 26 | You can use one of the following modes to edit neuron notes in Emacs. `neuron-mode` supports more neuron-specific functionality than `markdown-mode`. 27 | 28 | ### Editing with `neuron-mode` 29 | 30 | [neuron-mode](https://github.com/felko/neuron-mode) supports nifty editor features like opening a zettel by title, linking to other zettels by title, as well as displaying the title of the zettel next to the link (see screenshot below). 31 | 32 | ![screenshot](https://user-images.githubusercontent.com/3998/80873287-6fa75e00-8c85-11ea-9cf7-6e03db001d00.png){.ui .centered .large .image} 33 | 34 | ### Editing with `markdown-mode` 35 | 36 | [markdown-mode](https://github.com/jrblevin/markdown-mode) may be desirable to those that want only basic Zettelkasten functionality. In order to be able to open wiki-links (`markdown-follow-thing-at-point`) you must use the following settings: 37 | 38 | ```elisp 39 | (setq markdown-enable-wiki-links t) 40 | (setq markdown-link-space-sub-char " ") 41 | (setq markdown-wiki-link-search-type '(project)) 42 | ``` 43 | 44 | ## Editors known to work with v1 45 | 46 | These editors are known to work with version 1 of neuron. Your mileage may vary with the latest development version (version 2) of neuron. 47 | 48 | ### Vim 49 | 50 | See [this fork of neuron.vim](https://github.com/fiatjaf/neuron.vim). 51 | 52 | ![screenshot](https://github.com/fiatjaf/neuron.vim/raw/master/screenshot.png){.ui .centered .large .image} 53 | 54 | ### Online 55 | 56 | [[Cerveau]] can be used to edit your neuron v1 notes online using a web browser. Here's a small demo of the Cerveau editor in action, demonstrating the link autocomplete feature. 57 | 58 | ![demo](./static/cerveau-autocompl.gif) 59 | -------------------------------------------------------------------------------- /doc/Guide/Plugins/Ignoring files.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: neuronignore 3 | --- 4 | 5 | The `neuronignore` plugin allows you to specify a list of patterns that will be used to ignore specific Markdown files. 6 | 7 | Add a file named `.neuronignore` to your notes directory. It should look like this: 8 | 9 | ```ini 10 | # Ignore top-level dotfiles and dotdirs 11 | .*/** 12 | 13 | # Ignore project specific files 14 | README.md 15 | CHANGELOG.md 16 | LICENSE.md 17 | 18 | # Ignore everything under sub directories 19 | # (if not using dirtree plugin) 20 | */*/** 21 | ``` -------------------------------------------------------------------------------- /doc/Guide/Plugins/Linking.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: linking 3 | --- 4 | 5 | Although you may use regular Markdown links, neuron supports **wiki-links** that are often more convenient to use to link zettels. To link to a zettel using wiki-links, place that zettel's [[Zettel ID]] inside `[[..]]`. For example, 6 | 7 | ```markdown 8 | Place that zettel's [[id]] inside `[[..]]`. 9 | ``` 10 | 11 | In [[Web interface]], neuron will automatically display the title of the 12 | linked zettel. 13 | 14 | Alternate link text can also be given using a pipe, as in `[[link|text to display]]`. 15 | 16 | ## Folgezettel links 17 | 18 | Wiki-links may be labelled as "folgezettel" if you are writing a "structure note" (or hub note, or index note). See [[Folgezettel Links]]. 19 | 20 | ## Advanced linking 21 | 22 | If you are using the [[Tags]] plugin, you can link to multiple zettels matching a tag (see [[Tag Queries]]#). 23 | -------------------------------------------------------------------------------- /doc/Guide/Plugins/Linking/Folgezettel Links.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: folgezettel 3 | --- 4 | 5 | Wiki-links (see [[Linking]]) may be labelled as "folgezettel" if you are writing a "structure note" (or hub note, or index note). Folgezettel links are used to curate a tree-like structure (termed [[Folgezettel Heterarchy]]) for your zettelkasten, which structure is used in both [[Uplink Tree]] and [[impulse-feature]]. 6 | 7 | To mark a wiki-link as "folgezettel", put the `#` letter to either the end or the beginning of the link. The position of `#` indicates the *direction* of the folgezettel relationship. 8 | 9 | ```markdown 10 | # Programming paradigms 11 | 12 | There are different kinds of programming paradigms: 13 | 14 | * [[Functional Programming]]# 15 | * [[Imperative Programming]]# 16 | ``` 17 | 18 | When `#` is placed at the end[^legacy], as illustrated above, the current zettel becomes the (structural) parent of the linked note. Let's look at the opposite example, 19 | 20 | ```markdown 21 | # Haskell 22 | 23 | Haskell is an example of a language with #[[Functional Programming]] paradigm. 24 | ``` 25 | 26 | Here, the `#` is placed at the *beginning* of the link; thus, the linked zettel ("Functional Programming") becomes the parent of the current zettel ("Haskell"). Notice that, conceptually, this is equivalent to tagging; i.e., `#[[Foo]]` is semantically equivalent to `#foo`. Thus, the use of folgezettel links can obviate [[Tags]] for most typical use cases. 27 | 28 | :::{.ui .message} 29 | :::{.header} 30 | Take-away 31 | ::: 32 | If you are just starting out, begin with plain wiki-links. Once your get comfortable, experiment with making some of them folgezettel, while confirming the desired heterarchy in [[impulse-feature]] or [[Uplink Tree]]. 33 | ::: 34 | 35 | [^legacy]: [[Neuron v1]] used `[[[..]]]` to create forward folgezettel links. These are still supported for backwards compatability, but users should use `[[..]]#` going forward. -------------------------------------------------------------------------------- /doc/Guide/Plugins/Tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: tags 3 | --- 4 | 5 | Neuron supports Twitter like tags. Tags are added by prefixing them with `#`, and they can appear anywhere in the note text. For example: 6 | 7 | ```markdown 8 | Gaslighting is enabled by the victim's #cognitive-distortion without which the 9 | victimizer is rendered powerless to influence their victim. 10 | ``` 11 | 12 | In the above example, the note is tagged with `#cognitive-distortion` which also links to the tag page. 13 | 14 | Tags can also be specified in the [[Zettel metadata]] block. The above tag can be specified alternatively as follows: 15 | 16 | ```markdown 17 | --- 18 | tags: 19 | - cognitive-distortion 20 | ``` 21 | 22 | ## Hierarchical tags 23 | 24 | Tags can be nested using a "tag/subtag" syntax, to allow a more fine-grained organization of your Zettelkasten, especially when using advanced queries as shown in [[Tag Queries]]. 25 | 26 | For example, the following zettel is tagged "math/calculus/definition" 27 | 28 | ```markdown 29 | --- 30 | tags: 31 | - math/calculus/definition 32 | --- 33 | 34 | # Derivative 35 | ``` 36 | 37 | It will be included in the following tag queries: 38 | 39 | - `math/**` 40 | - `math/calculus/*` 41 | - `math/calculus/definition` 42 | - `**/definition` 43 | 44 | See [[Tag Queries]] to understand how to link zettels automatically based on tag patterns as above. 45 | -------------------------------------------------------------------------------- /doc/Guide/Plugins/Tags/Tag Queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: tag-queries 3 | --- 4 | 5 | Neuron supports *tag link queries* that can be used to *dynamically* create links based on a tag (see [[Tags]]). 6 | 7 | For example, to link to all zettels with the "science" tag (from the example at [[Zettel metadata]]), you would add the following to your note: 8 | 9 | ``` 10 | [[z:zettels?tag=science&timeline]] 11 | ``` 12 | 13 | When neuron encounters an [URI] with the `z:` protocol such as the above, it treats it as a link query. The `z:zettels` query in particular will link to all zettels tagged with the tag specified in the `tag` query parameter. 14 | 15 | ## Demo 16 | 17 | Here is a list of zettels tagged "walkthrough" on this very Zettelkasten: 18 | 19 | [[z:zettels?tag=walkthrough]] 20 | 21 | The above list was produced by the link query `[[z:zettels?tag=walkthrough]]`. 22 | 23 | ## Folgezettel connections 24 | 25 | Like [[Folgezettel Links]], you can use `#` to specify whether the links should be a folgezettel. 26 | 27 | ## Hierarchical tags 28 | 29 | Queries can also link to zettels whose [[Tags]] match a glob pattern. Two kinds of patterns in particular are noteworthy: 30 | 31 | 1. **Simple globs**: Simple globs (`*`) can be used to query all tags at a particular level in the hierarchy. For instance, `[[z:zettels?tag=science/*]]` will link to all zettels tagged "science/physics" *and* "science/biology", but *not* "science/physics/kinematics". 32 | 33 | 2. **Recursive globs**: Recursive globs (`**`) are like simple globs, but they operate recursively, matching tags at *all* levels in the hierarchy. For instance, `[[z:zettels?tag=science/**]]` will also match "science/physics/kinematics". This will also include zettels that are tagged "science" only, though this behavior can be avoided by querying "science/\*/\*\*" instead. 34 | 35 | :::{.ui .message} 36 | For a real-world example of link queries and hierarchical tags, see [Alien Psychology](https://alien-psychology.zettel.page/) ([source](https://github.com/srid/alien-psychology)). 37 | ::: 38 | 39 | ## Control flags 40 | 41 | Link queries support a few query flags to control the link listing UI: 42 | 43 | * `?grouped`: Group the results by matching tag (use with hierarchical tags) 44 | * `?timeline`: Query and display the results as if they form a timeline. This flag, 45 | * selects only zettels with a `date` set (see [[Zettel metadata]]). 46 | * sorts the results by date. 47 | * display the date besides the link. 48 | * `?showid`: Display the zettel ID alongside the zettel title (link). 49 | 50 | ## Limit the amount of zettels 51 | 52 | You can limit the amount of zettels to be shown in a query. This can be useful for e.g. a feed of posts. 53 | 54 | ``` 55 | [[z:zettels?tag=**&limit=2&timeline]] 56 | ``` 57 | 58 | ## Limitations of tag queries 59 | 60 | Non-ascii tags must be URI encoded when using in tag queries. See [this comment](https://github.com/srid/neuron/issues/446#issuecomment-720001775). Alternatively, if tagging in your case is semantically equivalent to linking, you may use [[Folgezettel Links]]. 61 | 62 | [URI]: https://en.wikipedia.org/wiki/Uniform_Resource_Identifier 63 | -------------------------------------------------------------------------------- /doc/Guide/Plugins/Uplink Tree.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: uplink-tree 3 | --- 4 | 5 | An **uplink** is a special kind of backlink created using [[Folgezettel Links]]. An **uplink tree** (aka. "uptree") of a zettel is the subset of the [[Folgezettel Heterarchy]] which branch off to the zettel. Uplink tree is displayed immediately above the zettel's title; all backlinks of the zettel are displayed immediately below the zettel's content. 6 | -------------------------------------------------------------------------------- /doc/Guide/Plugins/Web feeds.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: feed 3 | --- 4 | 5 | ![](https://upload.wikimedia.org/wikipedia/en/thumb/4/43/Feed-icon.svg/1920px-Feed-icon.svg.png){.ui .right .floated .tiny .image} 6 | 7 | The feed plugin is used to generate a [web feed] on a per-zettel basis. These zettels are generally considered to be hub notes or structure notes, because the feed's contents will be their direct folgezettel children (created using [[Folgezettel Links]]) with a `date` [[Zettel metadata]] property. 8 | 9 | To activate feed generation for a zettel, add the following to its YAML frontmatter: 10 | 11 | ```yaml 12 | --- 13 | feed: 14 | count: 5 15 | --- 16 | ``` 17 | 18 | The generated feed will be of the filename `${slug}.xml` (it is an Atom feed), where "slug" corresponds to the slug of the zettel (see [[Zettel metadata]]). 19 | 20 | Feed items are determined from the graph, by looking for the folgezettel children of the zettel, which may be established in one of the following ways: 21 | 22 | - [[Tag Queries]] with folgezettel label 23 | - Wiki-links using [[Folgezettel Links]] 24 | - Adding to a subfolder, with the [[Directory Tree]] plugin enabled 25 | 26 | A link to the feed will be added to the `` element of the generated HTML. 27 | 28 | [web feed]: https://en.wikipedia.org/wiki/Web_feed 29 | -------------------------------------------------------------------------------- /doc/Guide/Querying with JSON output.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: query 3 | --- 4 | 5 | Use the `query` command to query your Zettelkasten and return the matches in JSON format. 6 | 7 | ## Querying all zettels 8 | 9 | To retrieve the metadata (sans content) of all zettels in your Zettelkasten: 10 | 11 | ```bash 12 | neuron query --zettels 13 | ``` 14 | 15 | You can use [`jq`][jq] to further process the JSON result. For eg., to print a list of zettel titles: 16 | 17 | ```bash 18 | neuron query --zettels | jq ".[].Title" 19 | ``` 20 | 21 | ## Querying a single zettel 22 | 23 | To retrieve the metadata for a zettel by its [[Zettel ID]]: 24 | 25 | ```bash 26 | neuron query --id=index 27 | ``` 28 | 29 | ## Querying entire Zettelkasten graph 30 | 31 | ```bash 32 | neuron query --graph 33 | ``` 34 | 35 | ## Other queries 36 | 37 | - `neuron query --backlinks-of ID` 38 | - `neuron query --uplinks-of ID` 39 | - `neuron query --tags` 40 | - `neuron query --tag=mytag` 41 | 42 | ## Fast querying 43 | 44 | Pass the `--cached` argument if you want the query to run instantly, by reading from the local cache. To make sure that the cache remains up-to-date, you must be running `neuron gen` as a daemon. 45 | 46 | [jq]: https://stedolan.github.io/jq/ -------------------------------------------------------------------------------- /doc/Guide/Searching your Zettelkasten.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: searching 3 | tags: 4 | - walkthrough 5 | --- 6 | 7 | # Searching your Zettelkasten 8 | 9 | ## Interactive search 10 | 11 | Use the `search` command to search for a particular zettel: 12 | 13 | ```bash 14 | neuron search 15 | ``` 16 | 17 | This command[^1] will allow you to search your Zettels by title, and then print the matching zettel's filepath at the end. 18 | 19 | You may pipe the command to your text editor in order to directly edit the matching Zettel, or simply pass the `-e` option which opens the zettel in your $EDITOR: 20 | 21 | ```bash 22 | neuron search -e 23 | ``` 24 | 25 | See asciinema: 26 | 27 | ![asciicast](https://asciinema.org/a/313358.png) 28 | 29 | ### Full-text search 30 | 31 | The `--full-text` (alias: `-a`) option can be used to search by the whole content, not just the title: 32 | 33 | ```bash 34 | neuron search -a 35 | ``` 36 | [^1]: Internally, `neuron search` uses the tool [`bat`](https://github.com/sharkdp/bat) to display the selected Zettel. Note that you might want to configure `bat`, such as to set the color theme (the default `bat` theme is for a dark terminal background, which might not work well on light-themed terminals). 37 | -------------------------------------------------------------------------------- /doc/Guide/Web interface.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: web 3 | --- 4 | 5 | Neuron can generate a fully-functional and self-sufficient web site out of your zettelkasten. It generates the HTML files under your Zettelkasten directory, in `.neuron/output/`, as well as spin up a server that will serve that generated site at [localhost:8080](http://localhost:8080). 6 | 7 | ```bash 8 | neuron gen -wS 9 | ``` 10 | 11 | The `gen` command takes a few options, notably: 12 | 13 | * You can override the output directory path using `-o`. 14 | 15 | * You can override server settings such as the host and port. For example, 16 | 17 | ```bash 18 | neuron gen -ws 127.0.0.1:8081 19 | ``` 20 | 21 | * You can choose pretty URLs (i.e., `/foo` instead of `/foo.html`) using `--pretty-urls`. 22 | 23 | Additional CLI details are available via `--help`. 24 | 25 | ## Local site without server 26 | 27 | The web interface can also be accessed without necessarily running the server. 28 | First run neuron generator in "watch mode" only (no http server): 29 | 30 | ```bash 31 | # Watch only, without serving 32 | neuron gen -w 33 | ``` 34 | 35 | Leave this command running in one terminal, and then use `neuron open` to directly open the locally generated HTML site. 36 | 37 | ## Publishing to the web 38 | 39 | See [[Automatic Publishing]]# 40 | 41 | ## Features 42 | 43 | (See the links below) 44 | -------------------------------------------------------------------------------- /doc/Guide/Web interface/Automatic Publishing.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: publishing 3 | --- 4 | 5 | You can configure your neuron notes to be **automatically published** on the web. The simplest approach to achieve this is to store your notes in GitHub[^gitlab], and use their GitHub Pages + GitHub Actions[^this]. You can give this a try instantly with zero-configuration by using [`neuron-template`](https://github.com/srid/neuron-template). Follow the instructions in that link to get started. 6 | 7 | In order to enable automatic publishing for your *existing* neuron site that is already on GitHub, begin by copying `neuron-template`'s [`.github/workflows`](https://github.com/srid/neuron-template/tree/master/.github/workflows) directory to your repository. 8 | 9 | [^this]: This very site is set up to automatically publish in this manner. 10 | 11 | [^gitlab]: If you prefer to use GitLab (and thus GitLab Pages + GitLab CI) over GitHub, checkout the unofficial template [thematten/neuron-template](https://gitlab.com/thematten/neuron-template) on GitLab. For an existing GitLab repo, you will want to copy over its [`.gitlab-ci.yml`](https://gitlab.com/thematten/neuron-template/-/blob/master/.gitlab-ci.yml). 12 | -------------------------------------------------------------------------------- /doc/Guide/Web interface/Configuration file.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: configuration 3 | --- 4 | 5 | You may configure the parameters of your web interface by adding an optional configuration file named `neuron.dhall` under the notes directory. It should contain: 6 | 7 | ## Supported fields 8 | 9 | | Field name | Description | 10 | |-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 11 | | **`siteTitle`** | Your Neuron site's title | 12 | | **`siteBaseUrl`** | The base URL of your published Neuron site. Setting the base URL will enable [breadcrumbs](https://developers.google.com/search/docs/data-types/breadcrumb) in your site's structured data | 13 | | **`author`** | Author name | 14 | | **`theme`** | Color scheme to use for your site. Value must be [one of the color names](https://semantic-ui.com/usage/theming.html#sitewide-defaults) supported by SemanticUI. | 15 | | **`editUrl`** | The URL (without the zettel filename) to edit zettels. To remove the edit button from the navbar, remove this entry from your configuration. | 16 | | **`plugins`** | See [[Plugins]] | 17 | ## Example 18 | 19 | ```dhall 20 | { siteTitle = 21 | "My Zettelkasten for college" 22 | , siteBaseUrl = 23 | Some "https://somecollege.edu/~john/neuron" 24 | , theme = 25 | "brown" 26 | , editUrl = 27 | Some "https://github.com/john/website/edit/master/notes/" 28 | } 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /doc/Guide/Web interface/Customizing the generated website.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: customize-site 3 | --- 4 | 5 | Although neuron uses fixed HTML & CSS structure, you can customize the final site in one of the few ways. 6 | 7 | ## Favicon 8 | 9 | A custom [favicon](https://en.wikipedia.org/wiki/Favicon) for your site can be specified by copying it to the `static` directory (see [[Adding images and other files]]). Neuron recognizes the following file names: 10 | 11 | * `static/favicon.svg` 12 | * `static/favicon.png` 13 | * `static/favicon.ico` 14 | * `static/favicon.jpg` 15 | * `static/favicon.jpeg` 16 | * `static/apple-touch-icon.png` 17 | 18 | ## Web app manifest 19 | 20 | If a [web app manifest](https://web.dev/add-manifest/) file named `static/manifest.webmanifest` exists, Neuron will automatically use it in generated pages. 21 | 22 | ## Custom JavaScript / CSS 23 | 24 | [[Custom JavaScript and CSS]]# 25 | -------------------------------------------------------------------------------- /doc/Guide/Web interface/Customizing the generated website/Custom JavaScript and CSS.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: custom-head 3 | --- 4 | 5 | You can add custom JavaScript or CSS code to the `` element of the generated pages by adding it to the `head.html` file under the notes directory. 6 | 7 | If this file does not exist, neuron will use its builtin one that provides 8 | 9 | - MathJax JavaScript (for [[Math support]]) 10 | - Prism JavaScript (for [[Code Syntax Highlighting]]) 11 | 12 | :::{.ui .message} 13 | Note that if you are going to specify a custom `head.html`, you must include the above manually. That is, copy-paste the following to your new `head.html`: 14 | 15 | ```html 16 | 17 | 18 | 19 | 20 | 21 | 22 | ``` 23 | ::: 24 | 25 | Here are some of the interesting things you can do with a custom `head.html`: 26 | 27 | [[z:zettels?tag=custom-head-recipe]]# -------------------------------------------------------------------------------- /doc/Guide/Web interface/Customizing the generated website/Custom JavaScript and CSS/graphviz.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [custom-head-recipe] 3 | --- 4 | 5 | # Graphviz 6 | 7 | [Graphviz](https://graphviz.org/) is visualization software for representing structural information like abstract graphs and networks. 8 | 9 | Render Graphviz content in neuron by including the following HTML snippet in your `head.html` file ([[Custom JavaScript and CSS]]) or the zettel file ([[Using raw HTML in Markdown]]): 10 | 11 | ```html 12 | 13 | 14 | 28 | ``` 29 | 30 | Then include your Graphviz syntax in Markdown as follows: 31 | 32 | ~~~markdown 33 | ```{.graphviz} 34 | digraph G { 35 | 36 | subgraph cluster_0 { 37 | style=filled; 38 | color=lightgrey; 39 | node [style=filled,color=white]; 40 | a0 -> a1 -> a2 -> a3; 41 | label = "process #1"; 42 | } 43 | 44 | subgraph cluster_1 { 45 | node [style=filled]; 46 | b0 -> b1 -> b2 -> b3; 47 | label = "process #2"; 48 | color=blue 49 | } 50 | start -> a0; 51 | start -> b0; 52 | a1 -> b3; 53 | b2 -> a3; 54 | a3 -> a0; 55 | a3 -> end; 56 | b3 -> end; 57 | 58 | start [shape=Mdiamond]; 59 | end [shape=Msquare]; 60 | } 61 | ``` 62 | ~~~ 63 | 64 | When you open your generated neuron site, it will render as follows: 65 | 66 | ```{.graphviz} 67 | digraph G { 68 | 69 | subgraph cluster_0 { 70 | style=filled; 71 | color=lightgrey; 72 | node [style=filled,color=white]; 73 | a0 -> a1 -> a2 -> a3; 74 | label = "process #1"; 75 | } 76 | 77 | subgraph cluster_1 { 78 | node [style=filled]; 79 | b0 -> b1 -> b2 -> b3; 80 | label = "process #2"; 81 | color=blue 82 | } 83 | start -> a0; 84 | start -> b0; 85 | a1 -> b3; 86 | b2 -> a3; 87 | a3 -> a0; 88 | a3 -> end; 89 | b3 -> end; 90 | 91 | start [shape=Mdiamond]; 92 | end [shape=Msquare]; 93 | } 94 | ``` 95 | 96 | 97 | 98 | 99 | 113 | 114 | -------------------------------------------------------------------------------- /doc/Guide/Web interface/Customizing the generated website/Custom JavaScript and CSS/mermaid.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [custom-head-recipe] 3 | --- 4 | 5 | # Mermaid 6 | 7 | [Mermaid](https://mermaid-js.github.io/mermaid/) provides markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs. To 8 | 9 | Render mermaid content in neuron by including the following HTML snippet in your `head.html` file ([[Custom JavaScript and CSS]]) or the zettel file ([[Using raw HTML in Markdown]]): 10 | 11 | ```html 12 | 13 | 16 | ``` 17 | 18 | Then include your Mermaid syntax in Markdown as follows: 19 | 20 | ~~~markdown 21 | ```{.mermaid} 22 | sequenceDiagram 23 | Alice->>John: Hello John, how are you? 24 | activate John 25 | John-->>Alice: Great! 26 | deactivate John 27 | ``` 28 | ~~~ 29 | 30 | When you open your generated neuron site, it will render as follows: 31 | 32 | ```{.mermaid} 33 | sequenceDiagram 34 | Alice->>John: Hello John, how are you? 35 | activate John 36 | John-->>Alice: Great! 37 | deactivate John 38 | ``` 39 | 40 | 41 | 42 | 45 | 46 | -------------------------------------------------------------------------------- /doc/Guide/Web interface/Customizing the generated website/Custom JavaScript and CSS/tailwind.md: -------------------------------------------------------------------------------- 1 | --- 2 | tags: [custom-head-recipe] 3 | --- 4 | 5 | # Using Tailwind CSS classes 6 | 7 | Neuron provides [Semantic UI](https://fomantic-ui.com/) already, however you can use other CSS toolkits as well. [Tailwind](https://tailwindcss.com/) in particular is a note-worthy one as it allows you to style elements using pre-defined *semantically defined* classes. 8 | 9 | First, activate Tailwind using [`twind/shim`](https://twind.dev/handbook/the-shim.html), in your `head.html` (see [[Custom JavaScript and CSS]]): 10 | 11 | ```html 12 | 15 | 16 | 30 | 31 | ``` 32 | 33 | That's it; now you can use any of the Tailwind CSS classes in your Markdown files. Here's an example: 34 | 35 | ```markdown 36 | ## Highlights of the day: 37 | 38 | :::{.rounded .shadow-2xl .border-2 .border-solid .border-pink-400 .text-xl .mb-4} 39 | - Drank a [new]{.my-highlight} type of *coffee* 40 | - Hacked a new feature to my pet project 41 | - Enjoyed doing nothing in particular 42 | ::: 43 | 44 | Random bits: 45 | - These are not styled like the above div. 46 | ``` 47 | 48 | ## Live example 49 | 50 | See https://www.srid.ca/now (the pink box) 51 | -------------------------------------------------------------------------------- /doc/Guide/Web interface/Graph visualization.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: graph-visualize 3 | --- 4 | 5 | A zettelkasten is a [directed graph](https://en.wikipedia.org/wiki/Directed_graph). Neuron also has an unique notion of [[Folgezettel Heterarchy]]#, which is a *subset* of this graph established by having zettels "branch off" to other zettels. 6 | 7 | There are two modes of visualization these graph connections: 8 | 9 | ## [[impulse-feature]] 10 | 11 | In an otherwise statically-generated neuron site, [[impulse-feature]]# represents its *dynamic* part. 12 | 13 | ## Uplinks and Backlinks 14 | 15 | A backlink of a zettel is a zettel that links to it. If that link is a folgezettel link, it is called an "uplink". Each zettel has its [[Uplink Tree]]# displayed at the top. 16 | -------------------------------------------------------------------------------- /doc/Guide/Web interface/Graph visualization/Clusters.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: clusters 3 | --- 4 | 5 | Your Zettelkasten may have two or more clusters, not [connected](https://en.wikipedia.org/wiki/Connected_graph) to one another. [[impulse-feature]] will display these clusters, with each cluster's [[Folgezettel Heterarchy]] rendered as a [forest](https://tinyurl.com/wikipedia-forest), whose roots (aka "[mother](https://www.geeksforgeeks.org/find-a-mother-vertex-in-a-graph/) zettels") could be considered as a portal zettel into that sub-Zettelkasten. 6 | 7 | -------------------------------------------------------------------------------- /doc/Guide/Web interface/Graph visualization/Folgezettel Heterarchy.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: folgezettel-heterarchy 3 | --- 4 | 5 | Neuron allows you to organically build a [[Heterarchy]] out of your Zettelkasten 6 | over time. When a zettel links (see [[Linking]]) to another, it "branches 7 | off"[^1] to that zettel ... using [[Folgezettel Links]]#. 8 | 9 | A folgezettel heterarchy differs from a traditional "category tree" in two key 10 | ways: 11 | 12 | * It is built *organically* over time based on the connections formed, instead 13 | of being pre-defined. 14 | * A note can have *multiple* parents. 15 | 16 | The heterarchy is displayed in the following places 17 | 18 | * [[impulse-feature]]: Full folgezettel heterarchy of the zettelkasten. 19 | * [[Uplink Tree]]#: Subset of the above, branching off to the zettel. 20 | 21 | ## See also 22 | 23 | * The [impulse](./impulse) of this site displays its folgezettel heterarchy. 24 | 25 | [^1]: Hence the German compound word "[folge]-[zettel]". Read 26 | 27 | [folge]: https://en.wiktionary.org/wiki/Folge#German 28 | 29 | [zettel]: https://en.wiktionary.org/wiki/Zettel#German -------------------------------------------------------------------------------- /doc/Guide/Web interface/Graph visualization/impulse-feature.md: -------------------------------------------------------------------------------- 1 | # Impulse 2 | 3 | Impulse displays a full index to your generated statically-generated [[Web interface]]. It displays the zettelkasten [[Folgezettel Heterarchy]] for all [[Clusters]]# in the zettelkasten graph. 4 | 5 | View this site's impulse [here](./impulse). -------------------------------------------------------------------------------- /doc/Guide/Zettel ID.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: id 3 | --- 4 | 5 | A Zettel ID is a [[Zettel Markdown]] file's filename[^unicode] without the extension. Zettel IDs must be unique across the Zettelkasten. 6 | 7 | [^unicode]: Neuron will [NFC normalize](https://www.unicode.org/faq/normalization.html) the Zettel ID derived from filename or link so that they work reliably when using non-ascii characters in filename or links (see [[Linking]]). 8 | 9 | By default, `neuron new`[^new] will use random alphanumeric IDs of length 8, called a "random ID". But you may use arbitrary text as ID as well, called a "title ID". 10 | 11 | ## When to use *title IDs* 12 | 13 | A title ID is one that uses arbitrary text, typically denoting the title of the note. For example, in the link `[[Some note title]]` (see [[Linking]]), "Some note title" is the title ID, corresponding to the note filename `Some note title.md`, that is generated in the [[Web interface]] as the HTML file named `some-note-title.html` (unless you override this slug in [[Zettel metadata]]). 14 | 15 | Use title IDs when you want truly future-proof[^futureproof] link IDs that work on any text editor. However, note that this comes at the cost that you are willing to rename them (manually or using a script[^rename]) across your Zettelkasten if the title ID of any of your notes changes. 16 | 17 | Another advantange of using title IDs is that you do not have to specify an explicit title (eg: `# Foo`) in the Markdown file, as neuron will infer it from the filename.[^titleIdEx] 18 | 19 | [^titleIdEx]: See [example](https://github.com/srid/r-ScientificNutrition) 20 | 21 | ## When to prefer *random IDs* 22 | 23 | The advantage to using random IDs (which neuron uses by default) is that you do not have to rename links across your Zettelkasten when changing the title of a note. This makes the links slightly less future-proof, however ... because, for most convenient editing experience you now have to rely on using a text editor (see [[Editor integration]]) that supports expanding them with the title from the note text. 24 | 25 | [^new]: See [[Creating and Editing zettels]] 26 | [^futureproof]: See [[Philosophy]] 27 | [^rename]: Use `sed` or [sd](https://github.com/chmln/sd) in a script to rename title IDs across your Zettelkasten. Some text editors, like VSCode, may have built renaming support; see [[Editor integration]]. 28 | -------------------------------------------------------------------------------- /doc/Guide/Zettel Markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: markdown 3 | --- 4 | 5 | Zettel files are written in Markdown, per [CommonMark](https://commonmark.org/) and [GFM](https://github.github.com/gfm/) spec, along with most of [commonmark extensions](https://github.com/jgm/commonmark-hs/tree/master/commonmark-extensions) enabled.[^tech] 6 | 7 | * [[Linking]]# 8 | * [[Zettel metadata]]# 9 | * [[Adding images and other files]]# 10 | * Styling elements using Semantic UI ([\#176](https://github.com/srid/neuron/issues/176)) 11 | 12 | 13 | Enriching Markdown: 14 | 15 | * [[Using raw HTML in Markdown]]# 16 | * [[Code Syntax Highlighting]]# 17 | * [[Math support]]# 18 | * [Footnotes](https://github.com/jgm/commonmark-hs/blob/master/commonmark-extensions/test/footnotes.md) and [many other extensions](https://github.com/jgm/commonmark-hs/tree/master/commonmark-extensions) are enabled. 19 | * **Highlighting**: Surround text with `== ... ==` to highlighting them (eg: This ==word== is highlighted). 20 | 21 | [^tech]: Neuron uses [commonmark-hs](https://github.com/jgm/commonmark-hs) to parse them into the [Pandoc AST](https://pandoc.org/using-the-pandoc-api.html), as well as provides an extention on top to handle zettel links. 22 | -------------------------------------------------------------------------------- /doc/Guide/Zettel Markdown/Adding images and other files.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: static-files 3 | --- 4 | 5 | Create a folder named "static" in your Zettelkasten directory, and add your 6 | images and files to it. Neuron will automatically copy them as is to the output 7 | folder. You may reference them from the Markdown file using relative links. 8 | 9 | For example, suppose you have copied a file called `rose.png` to your `static` 10 | folder, you can then include it in your Markdown as: 11 | 12 | ```markdown 13 | ![Rose](./static/rose.png) 14 | ``` 15 | 16 | ## Special files 17 | 18 | Neuron treats certain files as special. See [[Customizing the generated website]]# 19 | -------------------------------------------------------------------------------- /doc/Guide/Zettel Markdown/Code Syntax Highlighting.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: syntax-highlighting 3 | --- 4 | 5 | Neuron provides syntax highlighting through [Prism](https://prismjs.com/), a JavaScript library that highlights code blocks in the web browser. To customize Prism's theme, or to provide your own syntax highlighter, you can do so in `head.html` (see [[Custom JavaScript and CSS]]). 6 | 7 | To activate syntax highlighting, you must specify the language in your [fenced code blocks](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#fenced-code-blocks) (see [example here](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting)) or view source of the below. 8 | 9 | ## Demo 10 | 11 | Haskell: 12 | 13 | ```haskell 14 | module Prime where 15 | 16 | primes = filterPrime [2..] 17 | where 18 | filterPrime (p:xs) = 19 | p : filterPrime [x | x <- xs, x `mod` p /= 0] 20 | ``` 21 | 22 | JSON: 23 | 24 | ```json 25 | { "_comment" : "This is JSON" 26 | , "name" : "srid" 27 | , "loc" : "Quebec" 28 | } 29 | ``` 30 | 31 | Nix: 32 | 33 | ```nix 34 | buildPythonPackage rec { 35 | pname = "hello"; 36 | version = "1.0"; 37 | src = fetchPypi { 38 | inherit pname version; 39 | sha256 = "01ba..0"; 40 | }; 41 | } 42 | ``` 43 | 44 | Markdown: 45 | 46 | ```markdown 47 | This is `markdown`, the *format* used by **Neuron** 48 | 49 | # Heading 50 | 51 | - Link to [neuron](https://neuron.zettel.page) 52 | - [[Wiki Link]] is not official Markdown syntax 53 | ``` 54 | 55 | Matlab: 56 | 57 | ```matlab 58 | % This is Matlab code 59 | data = T(:,{'Year','Month','DayofMonth','UniqueCarrier'}); 60 | data.Date = datetime(data.Year,data.Month,data.DayofMonth); 61 | data.UniqueCarrier = categorical(data.UniqueCarrier); 62 | ``` 63 | 64 | This one has no language identifier specified: 65 | 66 | ``` 67 | Just plain text. 68 | 69 | No particular syntax. 70 | ``` -------------------------------------------------------------------------------- /doc/Guide/Zettel Markdown/Math support.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: math 3 | --- 4 | 5 | Neuron's Markdown syntax supports [MathJax](https://www.mathjax.org/). 6 | 7 | ## LaTeX input 8 | 9 | You can type LaTeX code in zettels in three ways: 10 | 11 | * **Inline**: by surrounding your code with ``` $...$ ``` 12 | * **Block**: by surrounding your code with ``` $$...$$ ``` 13 | * **Multi-line block, centered**: 14 | ```markdown 15 | $$ 16 | \begin{split} 17 | x+1 &= 2 \\ 18 | y+2 &= 3 19 | \end{split} 20 | $$ 21 | ``` 22 | 23 | ## Examples 24 | 25 | * Inline LaTeX when written as ``` $a^2+b^2$ ``` looks like this: $a^2+b^2$ 26 | 27 | * Block LaTeX looks like this: $$(A = B) \cong (A \cong B)$$ 28 | 29 | * The multi-line block example shown above will render like this: 30 | $$ 31 | \begin{split} 32 | x+1 &= 2 \\ 33 | y+2 &= 3 34 | \end{split} 35 | $$ 36 | -------------------------------------------------------------------------------- /doc/Guide/Zettel Markdown/Using raw HTML in Markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: raw-html 3 | --- 4 | 5 | You can write raw HTML inline in your Markdown notes. You can also used fenced code blocks to specify them explicitly. 6 | 7 | ### Using fenced code blocks 8 | 9 | The Haskell CommonMark interpreter supports ['raw-attributes' to cause the code in the block to be interpreted as inline](https://github.com/jgm/commonmark-hs/blob/master/commonmark-extensions/test/raw_attribute.md). 10 | 11 | From the docs: 12 | 13 | > If attached to a fenced code block, it causes the block to be interpreted as raw block content with the specified format. 14 | 15 | For HTML, this just requires adding `{=html}` after the opening ` ``` ` code block fence. 16 | 17 | So, the following markdown content: 18 | 19 | ```````````````````````````````` markdown 20 | ## Some markdown interspersed with HTML 21 | 22 | Here is a video: 23 | 24 | ``` {=html} 25 | 26 | ``` 27 | ```````````````````````````````` 28 | 29 | results in the following generated HTML: 30 | 31 | ```````````````````````````````` html 32 |

Some markdown interspersed with HTML

33 |

Here is a video:

34 | 35 | ```````````````````````````````` -------------------------------------------------------------------------------- /doc/Guide/Zettel Markdown/Zettel metadata.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: metadata 3 | --- 4 | 5 | Zettels may contain optional metadata in the YAML frontmatter. 6 | 7 | ## Date 8 | 9 | The date of the zettels can be specified in the "date" metadata field. The time part is optional. 10 | 11 | ```yaml 12 | --- 13 | date: 2020-08-21T13:06 14 | --- 15 | ``` 16 | 17 | Date is used in a number of places in neuron: 18 | 19 | - `neuron new` automatically fills in the date field. 20 | - [[Tag Queries]]'s `timeline` flag displays the date in zettel listing. 21 | - Date is used in [[Web feeds]] 22 | 23 | ## Slug 24 | 25 | The "slug" of a zettel is used in its URL, which in turn is determined by the filename of the generated HTML file. By default neuron will use the lowercase version of [[Zettel ID]], with whitespace replaced with hyphen as the slug, which may be overriden here. 26 | 27 | ```yaml 28 | --- 29 | slug: foo-bar 30 | --- 31 | ``` 32 | 33 | ## Pinning 34 | 35 | Zettels can be pinned in [[impulse-feature]] so that they appear at the top. To pin a zettel, add the "pinned" tag (see [[Tags]]#) to it. 36 | 37 | ```yaml 38 | --- 39 | tags: 40 | - pinned 41 | --- 42 | ``` 43 | 44 | ## Hiding a zettel 45 | 46 | Sometimes you want to "draft" a note, and as such wish to hide it from the rest of Zettelkasten, notably in [[impulse-feature]]. This can be achieved by marking a zettel as "unlisted": 47 | 48 | ```yaml 49 | --- 50 | unlisted: true 51 | --- 52 | ``` 53 | 54 | ## Other metadata 55 | 56 | You can explicitly specify a title using the `title` metadata; otherwise, Neuron will infer it from the Markdown heading or [[Zettel ID]]. 57 | 58 | The metadata key `tags` or `keywords` can be used to specify tags, although neuron supports inline tags as well (see [[Tags]]#). 59 | -------------------------------------------------------------------------------- /doc/Guide/extras.md: -------------------------------------------------------------------------------- 1 | # User-contributed utilities 2 | 3 | Some neuron users have developed scripts and tools to improve on the base functionality of neuron. 4 | 5 | [neuron-extras](https://github.com/b0o/neuron-extras) by Maddison Hellstrom 6 | : auto-generate a Table of Contents / hierarchical index based on hierarchical [[Tags]] 7 | 8 | [neuron-helper](https://github.com/zettelzottel/neuron-helper) by ybaumy 9 | : export ebook and article highlights from Readwise.io to neuron Markdown, etc. 10 | 11 | [zk](https://github.com/mickael-menu/zk) by Mickaël Menu 12 | : a command-line tool to manage a Zettelkasten with advanced filtering and note generation capabilities, [compatible with neuron](https://github.com/mickael-menu/zk/blob/main/docs/neuron.md). 13 | 14 | [neuron-citation](https://github.com/samwalls/neuron-citation) by Sam Walls 15 | : allows pages to specify `reference` data, and for citations to be made in various formats. 16 | 17 | [ox-neuron](https://github.com/vedang/ox-neuron) by Vedang Manerikar 18 | : A Neuron Zettel Markdown back-end for Org Export Engine (see [anouncement](https://github.com/srid/neuron/discussions/644)). 19 | -------------------------------------------------------------------------------- /doc/Installing.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: install 3 | --- 4 | 5 | Neuron can be installed on Windows, Linux or macOS. 6 | 7 | ## Prerequisites 8 | 9 | ### Nix 10 | 11 | Neuron can be installed via the Nix[^nix] package manager. Install [Nix](https://nixos.org/) as follows: 12 | 13 | ```shell 14 | curl -L https://nixos.org/nix/install | sh 15 | ``` 16 | 17 | :::{.ui .floating .message} 18 | :::{.header} 19 | OS-specific notes 20 | ::: 21 | 22 | * If you are on **Windows**, you should begin by [installing Ubuntu on WSL 2](https://docs.microsoft.com/en-us/windows/wsl/install-win10) (not WSL 1), before installing Nix on it.[^winuse] 23 | * If you are on **macOS Catalina or later**, refer to the [macOS Installation](https://nixos.org/manual/nix/stable/#sect-macos-installation) section of the Nix manual on how to install Nix. 24 | 25 | As an alternative to Nix, you may try the [[Docker workflow]]#. 26 | ::: 27 | 28 | [^winuse]: For further tips on using neuron on WSL, see [here](https://note.ema.srid.ca/tips/wsl). 29 | 30 | [staticbin]: https://github.com/srid/neuron/releases/download/1.0.1.0/neuron-1.0.1.0-linux.tar.gz 31 | 32 | ### Enable cache 33 | 34 | Enable the [Nix cache](https://srid.cachix.org/) for neuron. 35 | 36 | ```shell 37 | nix-env -iA cachix -f https://cachix.org/api/v1/install 38 | cachix use srid 39 | ``` 40 | 41 | If you skip this step, your machine will spend some time compiling the neuron source code, as well as take more disk space. 42 | 43 | ## Install neuron 44 | 45 | To install the latest development version (see [[Neuron v1]] if you are looking for an older version) of neuron, run: 46 | 47 | ```shell 48 | nix-env -if https://github.com/srid/neuron/archive/master.tar.gz 49 | ``` 50 | 51 | Note that this command can also *upgrade* your existing install of neuron. 52 | 53 | For alternative mechanisms, see [[Declarative Install]]#. 54 | 55 | ## Test your install 56 | 57 | Make sure that you can execute the `neuron` executable. You should expect the following: 58 | 59 | ``` 60 | Usage: neuron [--version] [-d PATH] [-o PATH] COMMAND 61 | Neuron, future-proof Zettelkasten app 62 | 63 | Available options: 64 | --version Show version 65 | -d PATH Run as if neuron was started in PATH instead of the 66 | current working directory 67 | -o PATH Custom path to generate static site in 68 | -h,--help Show this help text 69 | 70 | Available commands: 71 | gen Generate and serve the static site 72 | new Create a new zettel 73 | open Open the local static site 74 | search Search zettels and print their path 75 | query Query the zettelkasten in JSON 76 | ``` 77 | 78 | ## What's next? 79 | 80 | Proceed to the [[Tutorial]]. 81 | 82 | [^nix]: Nix is a general package manager that you can use to manage other software and services as well. [See here](https://github.com/srid/neuron/issues/193#issuecomment-629557917). 83 | -------------------------------------------------------------------------------- /doc/Installing/Declarative Install.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: install-declarative 3 | --- 4 | 5 | If you use [NixOS](https://nixos.org/), add the following to your `environment.systemPackages` list: 6 | 7 | 8 | ```nix 9 | (let 10 | neuronSrc = builtins.fetchTarball "https://github.com/srid/neuron/archive/master.tar.gz"; 11 | neuronPkg = import neuronSrc; 12 | in neuronPkg.default) 13 | ``` 14 | 15 | If you use [home-manager](https://github.com/rycee/home-manager), add the above to your `home.packages` list. 16 | 17 | ## Pinning versions 18 | 19 | It is generally recommended to pin your imports in Nix. The above expression will fetch the then `master` branch, which is not what you want for reproducibility. Pick a revision from [the commit history](https://github.com/srid/neuron/commits/master), and then use it, for example: 20 | 21 | ```nix 22 | # Use this for neuron 0.5 or above only. 23 | (let neuronRev = "GITREVHERE"; 24 | neuronSrc = builtins.fetchTarball "https://github.com/srid/neuron/archive/${neuronRev}.tar.gz"; 25 | neuronPkg = import neuronSrc; 26 | in neuronPkg.default) 27 | ``` 28 | 29 | In the future if you decide to upgrade neuron, simply change the revision hash to a newer one. 30 | 31 | ## Flakes 32 | 33 | [Flakes](https://nixos.wiki/wiki/Flakes) is supported, and you can use neuron via the URL `github:srid/neuron`. For eg., `nix run github:srid/neuron` will run neuron off the master branch on GitHub. 34 | 35 | ## Systemd service 36 | 37 | If you use [home-manager](https://github.com/rycee/home-manager), you can also 38 | run neuron as a systemd service; see [[home-manager systemd service]]#. 39 | -------------------------------------------------------------------------------- /doc/Installing/Docker workflow.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: docker 3 | --- 4 | 5 | You can use neuron without installing it by trying the Docker image [sridca/neuron](https://hub.docker.com/r/sridca/neuron). The image is built automatically from the development version of neuron. 6 | 7 | You will need to [install Docker](https://docs.docker.com/get-docker/) if you do not already have it installed. 8 | 9 | In order to quickly get started, try: 10 | 11 | ```bash 12 | mkdir ~/zettelkasten 13 | cd ~/zettelkasten 14 | echo "hello world" > hello.md 15 | touch neuron.dhall # Mark this a neuron directory 16 | docker run --rm -t -i -p 8080:8080 -v $(pwd):/notes sridca/neuron neuron gen -ws 0.0.0.0:8080 17 | ``` 18 | 19 | This will run the neuron server on the current directory which can be accessed at . The docker image operates on `/notes` as the current working directory, which is where you are expected to mount your notes directory. 20 | 21 | ## Whalebrew 22 | 23 | macOS users may want to [try the Whalebrew package](https://github.com/srid/neuron/issues/528), but note that it may not be up to date. 24 | -------------------------------------------------------------------------------- /doc/Installing/Static binary.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: install-static 3 | --- 4 | 5 | There is no [static binary](https://github.com/srid/neuron/issues/626) per se for current version of neuron, but a **self-contained Linux executable** is available [here](https://github.com/srid/neuron/releases/tag/1.9.35.0). As this was produced by the experimental [nix bundle](https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-bundle.html) feature, your mileage with it may vary. 6 | 7 | Due to a [limitation](https://github.com/srid/neuron/issues/626#issuecomment-897575923) in nix-bundle you must *always* pass the absolute path to your notebook in the command line. For example, instead of running `neuron gen -wS`, run `neuron -d $(pwd) gen -wS`. 8 | 9 | ## v1.0 10 | 11 | :::{.ui .warning .message} 12 | **IMPORTANT**: These instructions for an *older version* of neuron. Static builds are currently unavailable[^why] for latest versions of neuron. 13 | ::: 14 | 15 | [^why]: See ; specifically, we need someone willing to volunteer maintenance of static builds over time. 16 | 17 | :::{.ui .warning .message} 18 | **Note**: Some users have [reported problems](https://github.com/srid/neuron/issues/430#issuecomment-718597211) with the static binary; if you notice the same, just install using Nix. 19 | ::: 20 | 21 | Linux and Windows (WSL2) users can get the static binary [here][staticbin]. If you choose to use the static binary instead of installing through Nix (see [[Installing]]), note the following: 22 | 23 | - You will have to *manually* install the runtime dependencies such as `fzf`, `bat`, `envsubst`, etc. yourself. 24 | - The static binary corresponds to the last stable release, which generally lags behind the development version (which the Nix install method at [[Installing]] uses). 25 | 26 | [staticbin]: https://github.com/srid/neuron/releases/download/1.0.1.0/neuron-1.0.1.0-linux.tar.gz 27 | -------------------------------------------------------------------------------- /doc/Installing/home-manager systemd service.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: install-systemd 3 | --- 4 | 5 | If you use [home-manager](https://github.com/rycee/home-manager), you can have 6 | `neuron` run in the background automatically. Add the following to your `home.nix`: 7 | 8 | ```nix 9 | systemd.user.services.neuron = let 10 | notesDir = "/path/to/your/zettelkasten"; 11 | # See "Declarative Install" 12 | neuron = ( 13 | let neuronRev = "GITREVHERE"; 14 | neuronSrc = builtins.fetchTarball https://github.com/srid/neuron/archive/${neuronRev}.tar.gz; 15 | in import neuronSrc {}); 16 | in { 17 | Unit.Description = "Neuron zettelkasten service"; 18 | Install.WantedBy = [ "graphical-session.target" ]; 19 | Service = { 20 | ExecStart = "${neuron}/bin/neuron -d ${notesDir} gen -wS"; 21 | }; 22 | }; 23 | ``` 24 | -------------------------------------------------------------------------------- /doc/Neuron v1.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: v1 3 | --- 4 | 5 | Current development version (v2) of neuron breaks compatibility in many cases (such as dropping org-mode support). If you would like these features, consider staying with version 1 of neuron. You can install this version using: 6 | 7 | ```shell 8 | nix-env -if https://github.com/srid/neuron/archive/v1.tar.gz 9 | ``` 10 | 11 | If you would like to contribute org support to v2, see [this issue](https://github.com/srid/neuron/issues/557). -------------------------------------------------------------------------------- /doc/Philosophy.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: philosophy 3 | --- 4 | 5 | Neuron was designed with these criteria in mind: 6 | 7 | * **Future-proof**: store notes locally[^plain] as *plain-text* (Markdown) files 8 | * Not tied[^editor] to a single text editor 9 | * Statically generated web site, for browsing and publishing on the web 10 | * Remain as simple to use as possible, whilst being feature-rich via [[Plugins]] 11 | 12 | [^plain]: Store your notes however you want. We recommend [Git](https://guides.github.com/introduction/git-handbook/), which enables full revision history of your notes for lifetime. 13 | [^editor]: Text editors should ideally be *decoupled*, integrating via something like [LSP](https://github.com/srid/neuron/issues/213). Both Emacs and Vim have extensions for neuron (see [[Editor integration]]). 14 | -------------------------------------------------------------------------------- /doc/Plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: plugins 3 | --- 4 | 5 | Neuron includes several plugins, some of them are enabled by default. 6 | 7 | You can turn on or turn off these plugins in [[Configuration file]]. For example, the following setting in your `neuron.dhall` will enable the three specified plugins, while leaving everything else disabled: 8 | 9 | ```dhall 10 | { plugins = [ "neuronignore", "links", "uptree", "feed" ] 11 | } 12 | ``` 13 | 14 | List of available plugins is displayed below: 15 | 16 | | Plugin Name | Documentation | Enabled by default? | 17 | |----------------|--------------------|---------------------| 18 | | `neuronignore` | [[Ignoring files]] | Yes | 19 | | `links` | [[Linking]] | Yes | 20 | | `tags` | [[Tags]] | Yes | 21 | | `uptree` | [[Uplink Tree]] | Yes | 22 | | `feed` | [[Web feeds]] | Yes | 23 | | `dirtree` | [[Directory Tree]] | No | 24 | -------------------------------------------------------------------------------- /doc/Zettelkasten.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: zettelkasten 3 | --- 4 | 5 | Zettelkasten is a smart note taking system created by a German sociologist, whose productivity increased to epic proportions due to it[^dclear]. 6 | 7 | All notes in a Zettelkasten are linear, [[Atomic and autonomous]]#, with links created between them, enabling a [[Heterarchy]]# to evolve organically, rather than having to preconceive a hierarchy ahead (as is the case with outliners like Workflowy and Dynalist). 8 | 9 | [^dclear]: To learn more, read [Zettelkasten — How One German Scholar Was So Freakishly Productive](https://writingcooperative.com/zettelkasten-how-one-german-scholar-was-so-freakishly-productive-997e4e0ca125) by David Clear. 10 | 11 | -------------------------------------------------------------------------------- /doc/Zettelkasten/Atomic and autonomous.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: atomic 3 | --- 4 | 5 | Zettelkasten notes are atomic and autonomous. 6 | 7 | Per [David Clear](https://writingcooperative.com/zettelkasten-how-one-german-scholar-was-so-freakishly-productive-997e4e0ca125): 8 | 9 | * **The principle of atomicity**: The term [was coined by Christian Tietze](https://zettelkasten.de/posts/create-zettel-from-reading-notes/). It means that each note should contain one idea and one idea only. This makes it possible to link ideas with a laser focus. 10 | * **The principle of autonomy**: Each note should be autonomous, meaning it should be self-contained and comprehensible on its own. [This allows](http://web.archive.org/web/20170407030848/https://omxi.se/2015-06-21-living-with-a-zettelkasten.html) notes to be moved, processed, separated, and concatenated independently of its neighbors. It also ensures that notes remain useful even if the original source of information disappears. 11 | -------------------------------------------------------------------------------- /doc/Zettelkasten/Heterarchy.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: heterarchy 3 | --- 4 | 5 | Traditional note taking methods, including outliners (Workflowy, Dynalist) necessitate creation of *preconceived* hierarchies, built "top-down". In contrast, the **heterarchy** of Zettelkasten is built organically from "bottom-up" via gradual forming of links between the notes. 6 | 7 | Ahrens [writes](https://www.quora.com/What-is-the-best-way-to-take-notes/answer/S%C3%B6nke-Ahrens?ch=10&share=e1efd8f9&srid=uJBsW): 8 | 9 | > \[...\] don’t start with preconceived categories and fill them with notes. Instead, let order emerge bottom-up by making connections between your notes \[...\] 10 | 11 | 12 | Per [David Clear](https://writingcooperative.com/zettelkasten-how-one-german-scholar-was-so-freakishly-productive-997e4e0ca125): 13 | 14 | > **Don’t worry about structure**: Don’t worry about putting notes in neat folders or into unique preconceived categories. [As Schmidt put it](https://sociologica.unibo.it/article/view/8350/8270), in a Zettelkasten “there are no privileged positions” and “there is no top and no bottom.” The organization develops organically. 15 | 16 | Richard Meadows at [lesswrong](https://www.lesswrong.com/posts/NfdHG6oHBJ8Qxc26s/the-zettelkasten-method-1) writes: 17 | 18 | > the benefits of Zettelkasten accrue in a non-linear fashion over time, as the graph becomes more connected. So even if you 'get it' as soon as you start playing around with the cards, you could reasonably expect to reap much greater gains over a timespan of months or years (at least, that's my experience!). 19 | 20 | ## See also 21 | 22 | * [Category-free notes](https://article69.art.blog/2019/12/20/how-and-why-to-create-a-zettelkasten-a-guide-in-the-vein-of-niklas-luhman/) 23 | * [[Folgezettel Heterarchy]]# 24 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | dirtree: 3 | display: False 4 | --- 5 | # Neuron Zettelkasten 6 | 7 | [Neuron](https://github.com/srid/neuron) is a future-proof open-source app for managing your plain-text notes in [[Zettelkasten]]# style, as well as for publishing them as a static site on the web. Read its [[Philosophy]]#. 8 | 9 | :::{.ui .center .aligned .message .grey} 10 | **Note**: Neuron will soon be superceded by a [[next|next-gen successor]]#. 11 | ::: 12 | 13 | ## Getting started 14 | 15 | * [[Installing]]# 16 | * [[Tutorial]]# 17 | * [[Guide]]# 18 | * [[Examples]]# 19 | 20 | ## External links 21 | 22 | :::{.ui .right .floated .basic .segment} 23 | Donate using Liberapay 24 | ::: 25 | 26 | * [Source Code](https://github.com/srid/neuron) 27 | * [Neuron news](https://www.srid.ca/neuron) 28 | * [Discussion Forum](https://github.com/srid/neuron/discussions) 29 | * [Community Chat](https://app.element.io/#/room/#neuron:matrix.org) 30 | * [Sponsor the project](https://github.com/sponsors/srid) 31 | -------------------------------------------------------------------------------- /doc/neuron.dhall: -------------------------------------------------------------------------------- 1 | { siteTitle = "Neuron Zettelkasten" 2 | , siteBaseUrl = Some "https://neuron.zettel.page" 3 | , editUrl = Some "https://github.com/srid/neuron/edit/master/doc/" 4 | , plugins = ["neurondhall", "links", "tags", "dirtree", "uptree"] 5 | } 6 | -------------------------------------------------------------------------------- /doc/next.md: -------------------------------------------------------------------------------- 1 | # Emanote is next-gen Neuron 2 | 3 | Neuron is now in **maintenance-mode**. It will continue to receive bug fixes, but all new feature work now happens on the successor project, called [Emanote](https://github.com/srid/emanote). 4 | 5 | Emanote improves neuron in various ways. Its development is nearly done, and you may start using it today on your neuron notebook. Your notes should work for most part; see [migration notes here](https://emanote.srid.ca/start/neuron) on where differences lie. 6 | -------------------------------------------------------------------------------- /doc/static/cerveau-autocompl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srid/neuron/aff49d85dc7d9c4b011a1e5a27ffae8f7e4f5537/doc/static/cerveau-autocompl.gif -------------------------------------------------------------------------------- /doc/static/vscode-title-id.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srid/neuron/aff49d85dc7d9c4b011a1e5a27ffae8f7e4f5537/doc/static/vscode-title-id.gif -------------------------------------------------------------------------------- /docker.nix: -------------------------------------------------------------------------------- 1 | # Builds a docker image containing the neuron executable 2 | # 3 | # Run as: 4 | # docker load -i $( 5 | # nix-build docker.nix \ 6 | # --argstr name \ 7 | # --argstr tag 8 | # ) 9 | let 10 | pkgs = import ./nixpkgs.nix { }; 11 | neuron = (import ./project.nix { }).neuron; 12 | in 13 | { name ? "sridca/neuron" 14 | , tag ? "dev" 15 | }: pkgs.dockerTools.buildImage { 16 | inherit name tag; 17 | contents = [ 18 | neuron 19 | # These are required for the GitLab CI runner 20 | pkgs.coreutils 21 | pkgs.bash_5 22 | ]; 23 | 24 | config = { 25 | Env = [ 26 | # For i18n to work (with filenames, etc.) 27 | "LANG=en_US.UTF-8" 28 | "LOCALE_ARCHIVE=${pkgs.glibcLocales}/lib/locale/locale-archive" 29 | ]; 30 | WorkingDir = "/notes"; 31 | Volumes = { 32 | "/notes" = { }; 33 | }; 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /exe/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE NoImplicitPrelude #-} 4 | 5 | module Main where 6 | 7 | import GHC.IO.Handle (BufferMode (LineBuffering)) 8 | import Main.Utf8 (withUtf8) 9 | import Neuron.CLI.App (run) 10 | import qualified Neuron.Reactor as Reactor 11 | import Relude 12 | import System.IO (hSetBuffering) 13 | 14 | main :: IO () 15 | main = do 16 | hSetBuffering stdout LineBuffering 17 | hSetBuffering stderr LineBuffering 18 | withUtf8 $ do 19 | run Reactor.generateSite 20 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1606424373, 7 | "narHash": "sha256-oq8d4//CJOrVj+EcOaSXvMebvuTkmBJuT5tzlfewUnQ=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "99f1c2157fba4bfe6211a321fd0ee43199025dbf", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "locked": { 21 | "lastModified": 1617631617, 22 | "narHash": "sha256-PARRCz55qN3gy07VJZIlFeOX420d0nGF0RzGI/9hVlw=", 23 | "owner": "numtide", 24 | "repo": "flake-utils", 25 | "rev": "b2c27d1a81b0dc266270fa8aeecebbd1807fc610", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "numtide", 30 | "repo": "flake-utils", 31 | "type": "github" 32 | } 33 | }, 34 | "nixpkgs": { 35 | "locked": { 36 | "lastModified": 1630140382, 37 | "narHash": "sha256-ntXepAHFlAEtaYIU5EzckRUODeeMgpu1u2Yug+4LFNc=", 38 | "owner": "nixos", 39 | "repo": "nixpkgs", 40 | "rev": "08ef0f28e3a41424b92ba1d203de64257a9fca6a", 41 | "type": "github" 42 | }, 43 | "original": { 44 | "owner": "nixos", 45 | "repo": "nixpkgs", 46 | "rev": "08ef0f28e3a41424b92ba1d203de64257a9fca6a", 47 | "type": "github" 48 | } 49 | }, 50 | "root": { 51 | "inputs": { 52 | "flake-compat": "flake-compat", 53 | "flake-utils": "flake-utils", 54 | "nixpkgs": "nixpkgs" 55 | } 56 | } 57 | }, 58 | "root": "root", 59 | "version": 7 60 | } 61 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Future-proof note-taking and publishing based on Zettelkasten"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/08ef0f28e3a41424b92ba1d203de64257a9fca6a"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | flake-compat = { 8 | url = "github:edolstra/flake-compat"; 9 | flake = false; 10 | }; 11 | }; 12 | 13 | outputs = { self, nixpkgs, flake-utils, ... }: 14 | { 15 | homeManagerModule = import ./home-manager-module.nix; 16 | } // flake-utils.lib.eachDefaultSystem (system: 17 | let 18 | pkgs = import nixpkgs { inherit system; }; 19 | project = import ./project.nix { inherit pkgs; }; 20 | 21 | in 22 | rec { 23 | packages = { neuron = project.neuron; }; 24 | defaultPackage = packages.neuron; 25 | 26 | devShell = project.shell; 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /hie.yaml: -------------------------------------------------------------------------------- 1 | cradle: 2 | cabal: 3 | - path: "./src" 4 | component: "lib:neuron" 5 | - path: "./exe" 6 | component: "exe:neuron" 7 | - path: "./test" 8 | component: "test:neuron-test" 9 | -------------------------------------------------------------------------------- /home-manager-module.nix: -------------------------------------------------------------------------------- 1 | { config, lib, pkgs, ... }: 2 | 3 | let 4 | inherit (lib) 5 | mkEnableOption 6 | mkIf 7 | mkOption 8 | types 9 | ; 10 | 11 | cfg = config.services.neuron; 12 | in 13 | { 14 | options = { 15 | services.neuron = { 16 | enable = mkEnableOption '' 17 | Future-proof note-taking and publishing based on Zettelkasten 18 | ''; 19 | 20 | package = mkOption { 21 | type = types.package; 22 | default = pkgs.neuron-notes; 23 | defaultText = "pkgs.neuron-notes"; 24 | description = "neuron derivation to use"; 25 | }; 26 | 27 | notesDirectory = mkOption { 28 | type = types.nullOr types.path; 29 | default = null; 30 | example = "/home/\${name}/zettelkasten"; 31 | description = "Directory that holds the neuron notes"; 32 | }; 33 | 34 | host = mkOption { 35 | type = types.str; 36 | default = "127.0.0.1"; 37 | description = "The hostname or IP address the HTTP server should listen to"; 38 | }; 39 | 40 | port = mkOption { 41 | type = types.port; 42 | default = 8080; 43 | description = "Port the HTTP server should listen to"; 44 | }; 45 | 46 | prettyUrls = mkOption { 47 | type = types.bool; 48 | default = false; 49 | description = "If set, drops the .html at the end of Zettel URLs."; 50 | }; 51 | 52 | systemdTarget = mkOption { 53 | type = types.str; 54 | default = "graphical-session.target"; 55 | description = "Systemd target to bind to"; 56 | }; 57 | }; 58 | }; 59 | 60 | config = mkIf cfg.enable { 61 | assertions = [ 62 | { 63 | assertion = cfg.notesDirectory != null; 64 | message = "`services.neuron.notesDirectory' must be set."; 65 | } 66 | ]; 67 | 68 | systemd.user.services.neuron = { 69 | Unit = { 70 | Description = "Neuron zettelkasten service"; 71 | PartOf = [ cfg.systemdTarget ]; 72 | After = [ cfg.systemdTarget ]; 73 | }; 74 | 75 | Service = { 76 | Type = "exec"; 77 | ExecStart = '' 78 | ${cfg.package}/bin/neuron \ 79 | -d ${cfg.notesDirectory} \ 80 | gen \ 81 | --watch \ 82 | --serve ${cfg.host}:${toString cfg.port} \ 83 | ${lib.optionalString cfg.prettyUrls "--pretty-urls"} 84 | ''; 85 | }; 86 | 87 | Install = { 88 | WantedBy = [ cfg.systemdTarget ]; 89 | }; 90 | }; 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /neuron-search: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | NOTESDIR=${1} 6 | FILTERBY=${2} 7 | SEARCHFROMFIELD=${3} 8 | EXTENSIONS=${4} 9 | OPENCMD=$(envsubst -no-unset -no-empty <<<${5}) 10 | EXACT="" 11 | 12 | : ${NEURON_SEARCH_CONTEXT_ABOVE:=10} 13 | 14 | # Use --exact for full text search 15 | if [ -z "${FILTERBY}" ]; then 16 | EXACT="--exact" 17 | fi 18 | 19 | cd ${NOTESDIR} 20 | 21 | # The LC_ALL here is fix the "illegal byte sequence" error 22 | # See https://github.com/srid/neuron/pull/445#discussion_r513066111 23 | { (rg --no-heading --line-number --with-filename --sort path "${FILTERBY}" -g "${EXTENSIONS}" || echo) \ 24 | && rg --no-heading --no-line-number --with-filename --sort path "${FILTERBY}" -g "${EXTENSIONS}" \ 25 | --files-without-match | awk '{ printf "%s:0:# %s\n", $0, $0}'; } \ 26 | | ( [ ${FILTERBY} ] && LC_ALL=C sort -t: -k1,3 -r || cat ) \ 27 | | ( [ ${FILTERBY} ] && LC_ALL=C sort -t: -k1,1 -u || cat )\ 28 | | fzf ${EXACT} --tac --no-sort -d ':' -n ${SEARCHFROMFIELD}.. \ 29 | --preview 'bat --style=plain --color=always --highlight-line={2} {1}' \ 30 | --preview-window ':+{2}-'"$NEURON_SEARCH_CONTEXT_ABOVE" \ 31 | | awk -F: "{printf \"${NOTESDIR}/%s\", \$1}" \ 32 | | xargs -I % ${OPENCMD} % 33 | -------------------------------------------------------------------------------- /nixpkgs.nix: -------------------------------------------------------------------------------- 1 | let 2 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 3 | nixpkgs = lock.nodes.nixpkgs.locked; 4 | 5 | in 6 | import (fetchTarball { 7 | url = "https://github.com/nixos/nixpkgs/archive/${nixpkgs.rev}.tar.gz"; 8 | sha256 = nixpkgs.narHash; 9 | }) 10 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | in 6 | fetchTarball { 7 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 8 | sha256 = lock.nodes.flake-compat.locked.narHash; 9 | } 10 | ) 11 | { 12 | src = ./.; 13 | }).shellNix 14 | -------------------------------------------------------------------------------- /src/Data/Graph/Labelled.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | 3 | module Data.Graph.Labelled 4 | ( -- * Graph type 5 | LabelledGraph, 6 | Vertex (..), 7 | 8 | -- * Graph construction 9 | mkGraphFrom, 10 | 11 | -- * Querying 12 | getGraph, 13 | findVertex, 14 | getVertices, 15 | hasEdge, 16 | edgeLabel, 17 | 18 | -- * Algorithms 19 | postSet, 20 | preSet, 21 | preSetWithEdgeLabel, 22 | preSetWithEdgeLabelMany, 23 | topSort, 24 | clusters, 25 | dfsForest, 26 | dfsForestFrom, 27 | dfsForestBackwards, 28 | bfsForestFrom, 29 | bfsForestBackwards, 30 | obviateRootUnlessForest, 31 | induceOnEdge, 32 | induceOnEdgeReplacing, 33 | induce, 34 | emap, 35 | vmap, 36 | ) 37 | where 38 | 39 | import Data.Graph.Labelled.Algorithm 40 | import Data.Graph.Labelled.Build 41 | import Data.Graph.Labelled.Type 42 | -------------------------------------------------------------------------------- /src/Data/Graph/Labelled/Build.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE RankNTypes #-} 3 | {-# LANGUAGE NoImplicitPrelude #-} 4 | 5 | module Data.Graph.Labelled.Build where 6 | 7 | import qualified Algebra.Graph.Labelled.AdjacencyMap as LAM 8 | import Data.Graph.Labelled.Type (LabelledGraph (LabelledGraph), Vertex (..)) 9 | import qualified Data.Map.Strict as Map 10 | import Relude 11 | 12 | -- Build a graph from a list objects that contains information about the 13 | -- corresponding vertex as well as the outgoing edges. 14 | mkGraphFrom :: 15 | forall e v. 16 | (Eq e, Monoid e, Ord (VertexID v), Vertex v) => 17 | -- | List of known vertices in the graph. 18 | [v] -> 19 | [(e, v, v)] -> 20 | LabelledGraph v e 21 | mkGraphFrom xs es = 22 | let vertexList = vertexID <$> xs 23 | vertexMap = Map.fromList $ fmap (vertexID &&& id) xs 24 | edges = flip fmap es $ \(e, v1, v2) -> (e, vertexID v1, vertexID v2) 25 | graph = 26 | LAM.overlay 27 | (LAM.vertices vertexList) 28 | (LAM.edges edges) 29 | in LabelledGraph graph vertexMap 30 | -------------------------------------------------------------------------------- /src/Data/Graph/Labelled/Type.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE FlexibleContexts #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE StandaloneDeriving #-} 7 | {-# LANGUAGE TypeFamilies #-} 8 | {-# LANGUAGE UndecidableInstances #-} 9 | {-# LANGUAGE NoImplicitPrelude #-} 10 | 11 | module Data.Graph.Labelled.Type where 12 | 13 | import qualified Algebra.Graph.Labelled.AdjacencyMap as LAM 14 | import Data.Aeson 15 | import qualified Data.Map.Strict as Map 16 | import Relude 17 | 18 | -- | Instances of this class can be used as a vertex in a graph. 19 | class Vertex v where 20 | type VertexID v :: Type 21 | 22 | -- | Get the vertex ID associated with this vertex. 23 | -- 24 | -- This relation is expected to be bijective. 25 | vertexID :: v -> VertexID v 26 | 27 | -- | An edge and vertex labelled graph 28 | -- 29 | -- The `v` must be an instance of `Vertex`; as such `v` is considered the vertex 30 | -- label, with the actual vertex (bijectively) derived from it. 31 | data LabelledGraph v e = LabelledGraph 32 | { graph :: LAM.AdjacencyMap e (VertexID v), 33 | vertices :: Map (VertexID v) v 34 | } 35 | deriving (Generic) 36 | 37 | deriving instance (Eq e, Eq v, Eq (VertexID v)) => Eq (LabelledGraph v e) 38 | 39 | deriving instance 40 | ( Ord e, 41 | Show e, 42 | Show v, 43 | Ord (VertexID v), 44 | Show (VertexID v) 45 | ) => 46 | Show (LabelledGraph v e) 47 | 48 | instance (ToJSONKey (VertexID v), ToJSON v, ToJSON e) => ToJSON (LabelledGraph v e) where 49 | toJSON (LabelledGraph g vs) = 50 | toJSON $ 51 | object 52 | [ "adjacencyMap" .= LAM.adjacencyMap g, 53 | "vertices" .= vs 54 | ] 55 | 56 | instance (FromJSONKey (VertexID v), Ord (VertexID v), Eq e, Monoid e, FromJSON v, FromJSON e) => FromJSON (LabelledGraph v e) where 57 | parseJSON = 58 | withObject "LabelledGraph" $ \lg -> 59 | LabelledGraph 60 | <$> (fmap mkGraph $ lg .: "adjacencyMap") 61 | <*> lg .: "vertices" 62 | where 63 | mkGraph = LAM.fromAdjacencyMaps . Map.toList 64 | -------------------------------------------------------------------------------- /src/Data/PathTree.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | 3 | module Data.PathTree 4 | ( mkTreeFromPaths, 5 | annotatePathsWith, 6 | foldSingleParentsWith, 7 | ) 8 | where 9 | 10 | import qualified Data.List.NonEmpty as NE 11 | import qualified Data.Map as Map 12 | import Data.Tree (Forest, Tree (Node)) 13 | import Relude 14 | import Relude.Extra.Group (groupBy) 15 | 16 | mkTreeFromPaths :: Ord a => [[a]] -> Forest a 17 | mkTreeFromPaths paths = uncurry mkNode <$> Map.assocs groups 18 | where 19 | groups = fmap tail <$> groupBy head (mapMaybe nonEmpty paths) 20 | mkNode label children = 21 | Node label $ mkTreeFromPaths $ toList children 22 | 23 | annotatePathsWith :: (NonEmpty a -> ann) -> Tree a -> Tree (a, ann) 24 | annotatePathsWith f = go [] 25 | where 26 | go ancestors (Node rel children) = 27 | let path = rel :| ancestors 28 | in Node (rel, f $ NE.reverse path) $ fmap (go $ toList path) children 29 | 30 | -- | Fold nodes with one child using the given function 31 | -- 32 | -- The function is called with the parent and the only child. If a Just value is 33 | -- returned, folding happens with that value, otherwise there is no effect. 34 | foldSingleParentsWith :: (a -> a -> Maybe a) -> Tree a -> Tree a 35 | foldSingleParentsWith f = go 36 | where 37 | go (Node parent children) = 38 | case fmap go children of 39 | [Node child grandChildren] 40 | | Just new <- f parent child -> Node new grandChildren 41 | xs -> Node parent xs 42 | -------------------------------------------------------------------------------- /src/Data/Structured/Breadcrumb.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE ViewPatterns #-} 7 | {-# LANGUAGE NoImplicitPrelude #-} 8 | 9 | -- | JSON for Google's structured data breadcrumbs 10 | -- 11 | -- https://developers.google.com/search/docs/data-types/breadcrumb 12 | module Data.Structured.Breadcrumb where 13 | 14 | import Data.Aeson 15 | ( KeyValue ((.=)), 16 | ToJSON (toJSON), 17 | encode, 18 | object, 19 | ) 20 | import Data.Tree (Forest, foldTree) 21 | import Reflex.Dom.Core hiding (mapMaybe) 22 | import Relude 23 | import Text.URI (URI) 24 | import qualified Text.URI as URI 25 | 26 | data Item = Item 27 | { name :: Text, 28 | url :: Maybe URI 29 | } 30 | deriving (Eq, Show, Generic) 31 | 32 | newtype Breadcrumb = Breadcrumb {unBreadcrumb :: NonEmpty Item} 33 | deriving (Eq, Show, Generic) 34 | 35 | renderBreadcrumbs :: DomBuilder t m => [Breadcrumb] -> m () 36 | renderBreadcrumbs bs = 37 | elAttr "script" ("type" =: "application/ld+json") $ text $ decodeUtf8 $ encode bs 38 | 39 | instance ToJSON Breadcrumb where 40 | toJSON (Breadcrumb (toList -> crumbs)) = 41 | toJSON $ 42 | object 43 | [ "@context" .= toJSON context, 44 | "@type" .= ("BreadcrumbList" :: Text), 45 | "itemListElement" .= toJSON (uncurry itemJson <$> zip [1 :: Int ..] crumbs) 46 | ] 47 | where 48 | context = "https://schema.org" :: Text 49 | itemJson pos Item {..} = 50 | object 51 | [ "@type" .= toJSON ("ListItem" :: Text), 52 | "position" .= toJSON pos, 53 | "name" .= toJSON name, 54 | "item" .= toJSON (fmap URI.render url) 55 | ] 56 | 57 | fromForest :: Forest Item -> [Breadcrumb] 58 | fromForest = 59 | fmap Breadcrumb . mapMaybe (nonEmpty . reverse) . concatMap (foldTree f) 60 | where 61 | f :: Item -> [[[Item]]] -> [[Item]] 62 | f parent = \case 63 | [] -> [[parent]] 64 | childPaths -> 65 | fmap (parent :) `concatMap` childPaths 66 | -------------------------------------------------------------------------------- /src/Data/Structured/OpenGraph.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GADTs #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | 6 | -- | Meta tags for The Open Graph protocol: https://ogp.me/ 7 | module Data.Structured.OpenGraph 8 | ( OpenGraph (..), 9 | OGType (..), 10 | Article (..), 11 | ) 12 | where 13 | 14 | import Data.Time (UTCTime) 15 | import Relude 16 | import qualified Text.URI as URI 17 | 18 | -- The OpenGraph metadata 19 | -- 20 | -- This type can be directly rendered to HTML using `toHTML`. 21 | data OpenGraph = OpenGraph 22 | { _openGraph_title :: Text, 23 | _openGraph_url :: Maybe URI.URI, 24 | _openGraph_author :: Maybe Text, 25 | _openGraph_description :: Maybe Text, 26 | _openGraph_siteName :: Text, 27 | _openGraph_type :: Maybe OGType, 28 | _openGraph_image :: Maybe URI.URI 29 | } 30 | deriving (Eq, Show) 31 | 32 | -- TODO: Remaining ADT values & sub-fields 33 | data OGType 34 | = OGType_Article Article 35 | | OGType_Website 36 | deriving (Eq, Show) 37 | 38 | -- TODO: _article_profile :: [Profile] 39 | data Article = Article 40 | { _article_section :: Maybe Text, 41 | _article_modifiedTime :: Maybe UTCTime, 42 | _article_publishedTime :: Maybe UTCTime, 43 | _article_expirationTime :: Maybe UTCTime, 44 | _article_tag :: [Text] 45 | } 46 | deriving (Eq, Show) 47 | -------------------------------------------------------------------------------- /src/Data/Structured/OpenGraph/Render.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | {-# LANGUAGE ScopedTypeVariables #-} 5 | {-# LANGUAGE NoImplicitPrelude #-} 6 | 7 | module Data.Structured.OpenGraph.Render where 8 | 9 | import Data.Structured.OpenGraph 10 | ( Article (..), 11 | OGType (..), 12 | OpenGraph (..), 13 | ) 14 | import Data.Time.ISO8601 (formatISO8601) 15 | import Reflex.Dom.Core 16 | import Relude 17 | import qualified Text.URI as URI 18 | 19 | renderOpenGraph :: forall t m. DomBuilder t m => OpenGraph -> m () 20 | renderOpenGraph OpenGraph {..} = do 21 | meta' "author" `mapM_` _openGraph_author 22 | meta' "description" `mapM_` _openGraph_description 23 | requireAbsolute "OGP URL" (\ourl -> elAttr "link" ("rel" =: "canonical" <> "href" =: ourl) blank) `mapM_` _openGraph_url 24 | metaOg "title" _openGraph_title 25 | metaOg "site_name" _openGraph_siteName 26 | whenJust _openGraph_type $ \case 27 | OGType_Article (Article {..}) -> do 28 | metaOg "type" "article" 29 | metaOg "article:section" `mapM_` _article_section 30 | metaOgTime "article:modified_time" `mapM_` _article_modifiedTime 31 | metaOgTime "article:published_time" `mapM_` _article_publishedTime 32 | metaOgTime "article:expiration_time" `mapM_` _article_expirationTime 33 | metaOg "article:tag" `mapM_` _article_tag 34 | OGType_Website -> do 35 | metaOg "type" "website" 36 | requireAbsolute "OGP image URL" (metaOg "image") `mapM_` _openGraph_image 37 | where 38 | meta' k v = 39 | elAttr "meta" ("name" =: k <> "content" =: v) blank 40 | metaOg k v = 41 | elAttr "meta" ("property" =: ("og:" <> k) <> "content" =: v) blank 42 | metaOgTime k t = 43 | metaOg k $ toText $ formatISO8601 t 44 | requireAbsolute :: Text -> (Text -> m ()) -> URI.URI -> m () 45 | requireAbsolute description f uri' = 46 | if isJust (URI.uriScheme uri') 47 | then f $ URI.render uri' 48 | else error $ description <> " must be absolute. this URI is not: " <> URI.render uri' 49 | -------------------------------------------------------------------------------- /src/Data/Time/DateMayTime.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 4 | {-# LANGUAGE LambdaCase #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE RecordWildCards #-} 7 | {-# LANGUAGE TupleSections #-} 8 | {-# LANGUAGE TypeApplications #-} 9 | {-# LANGUAGE ViewPatterns #-} 10 | {-# LANGUAGE NoImplicitPrelude #-} 11 | {-# OPTIONS_GHC -fno-warn-orphans #-} 12 | 13 | module Data.Time.DateMayTime 14 | ( -- Date type 15 | DateMayTime, 16 | mkDateMayTime, 17 | getDay, 18 | -- Date formatting 19 | dateTimeFormat, 20 | formatDay, 21 | formatLocalTime, 22 | formatDateMayTime, 23 | -- Date parsing 24 | parseDateMayTime, 25 | ) 26 | where 27 | 28 | import Data.Aeson 29 | import Data.Time 30 | ( Day, 31 | FormatTime, 32 | LocalTime (..), 33 | TimeOfDay, 34 | defaultTimeLocale, 35 | formatTime, 36 | parseTimeM, 37 | ) 38 | import Relude 39 | 40 | -- | Like `Day` but with optional time. 41 | newtype DateMayTime = DateMayTime {unDateMayTime :: (Day, Maybe TimeOfDay)} 42 | deriving (Eq, Show, Generic, Ord) 43 | 44 | instance ToJSON DateMayTime where 45 | toJSON = toJSON . formatDateMayTime 46 | 47 | instance FromJSON DateMayTime where 48 | parseJSON = parseDateMayTime <=< parseJSON 49 | 50 | mkDateMayTime :: Either Day LocalTime -> DateMayTime 51 | mkDateMayTime = 52 | DateMayTime . \case 53 | Left day -> 54 | (day, Nothing) 55 | Right datetime -> 56 | localDay &&& Just . localTimeOfDay $ datetime 57 | 58 | getDay :: DateMayTime -> Day 59 | getDay = fst . unDateMayTime 60 | 61 | formatDateMayTime :: DateMayTime -> Text 62 | formatDateMayTime (DateMayTime (day, mtime)) = 63 | maybe (formatDay day) (formatLocalTime . LocalTime day) mtime 64 | 65 | formatDay :: Day -> Text 66 | formatDay = formatTime' dateFormat 67 | 68 | formatLocalTime :: LocalTime -> Text 69 | formatLocalTime = formatTime' dateTimeFormat 70 | 71 | parseDateMayTime :: (MonadFail m, Alternative m) => Text -> m DateMayTime 72 | parseDateMayTime (toString -> s) = do 73 | fmap mkDateMayTime $ 74 | fmap Left (parseTimeM False defaultTimeLocale dateFormat s) 75 | <|> fmap Right (parseTimeM False defaultTimeLocale dateTimeFormat s) 76 | 77 | dateFormat :: String 78 | dateFormat = "%Y-%m-%d" 79 | 80 | dateTimeFormat :: String 81 | dateTimeFormat = "%Y-%m-%dT%H:%M" 82 | 83 | -- | Like `formatTime` but with default time locale and returning Text 84 | formatTime' :: FormatTime t => String -> t -> Text 85 | formatTime' s = toText . formatTime defaultTimeLocale s 86 | -------------------------------------------------------------------------------- /src/Data/YAML/ToJSON.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE NoImplicitPrelude #-} 7 | {-# OPTIONS_GHC -fno-warn-orphans #-} 8 | 9 | -- | Import this module if you want `toJSON` on YAML AST. 10 | module Data.YAML.ToJSON where 11 | 12 | import Data.Aeson 13 | import qualified Data.Aeson as Aeson 14 | import qualified Data.Aeson.Encoding as AesonEncoding 15 | import qualified Data.YAML as Y 16 | import Relude 17 | 18 | instance Aeson.ToJSONKey (Y.Node Y.Pos) where 19 | toJSONKey = ToJSONKeyText f (AesonEncoding.text . f) 20 | where 21 | f = \case 22 | Y.Scalar _ x -> scalarToText x 23 | Y.Mapping _ _ x -> show x 24 | Y.Sequence _ _ x -> show x 25 | Y.Anchor _ _ x -> f x 26 | scalarToText = \case 27 | Y.SNull -> "null" 28 | Y.SBool x -> show x 29 | Y.SFloat x -> show x 30 | Y.SInt x -> show x 31 | Y.SStr x -> x 32 | Y.SUnknown _tag x -> show x 33 | 34 | instance Aeson.ToJSON (Y.Node Y.Pos) where 35 | toJSON = \case 36 | Y.Scalar _ x -> toJSON x 37 | Y.Mapping _ _ x -> toJSON x 38 | Y.Sequence _ _ x -> toJSON x 39 | -- Not sure what to do here, but we don't expect to get this in neuron. 40 | Y.Anchor _ _ _ -> toJSON ("unsupported" :: Text) 41 | 42 | instance ToJSON Y.Scalar where 43 | toJSON = \case 44 | Y.SNull -> Aeson.Null 45 | Y.SBool x -> toJSON x 46 | Y.SFloat x -> toJSON x 47 | Y.SInt x -> toJSON x 48 | Y.SStr x -> toJSON x 49 | Y.SUnknown _tag x -> toJSON x 50 | -------------------------------------------------------------------------------- /src/Neuron/Backend.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE ViewPatterns #-} 7 | {-# LANGUAGE NoImplicitPrelude #-} 8 | 9 | module Neuron.Backend where 10 | 11 | import Colog (WithLog, log) 12 | import qualified Data.Text as T 13 | import Network.Wai.Application.Static (defaultFileServerSettings, staticApp) 14 | import qualified Network.Wai.Handler.Warp as Warp 15 | import Network.Wai.Middleware.Rewrite (rewriteWithQueries) 16 | import Neuron.CLI.Logging 17 | import Relude 18 | import System.Directory (doesPathExist) 19 | import System.FilePath (()) 20 | 21 | -- | Run a HTTP server to serve a directory of static files 22 | -- 23 | -- Binds the server to host 127.0.0.1. 24 | serve :: 25 | (MonadIO m, WithLog env Message m) => 26 | -- | Host 27 | Text -> 28 | -- | Port number to bind to 29 | Int -> 30 | -- | Directory to serve. 31 | FilePath -> 32 | m () 33 | serve host port path = do 34 | log (I' WWW) $ toText $ "Serving " <> path <> " at http://" <> toString host <> ":" <> show port 35 | liftIO $ Warp.runSettings settings app 36 | where 37 | app = 38 | allowPrettyUrls $ 39 | staticApp $ 40 | defaultFileServerSettings path 41 | settings = 42 | Warp.setHost (fromString $ toString host) $ 43 | Warp.setPort 44 | port 45 | Warp.defaultSettings 46 | allowPrettyUrls = 47 | rewriteWithQueries $ \pq _reqH -> do 48 | -- Serve /foo.html when asked for /foo, but only if foo.html exists on 49 | -- disk. 50 | case pq of 51 | ([oldPath@(T.stripSuffix ".html" -> Nothing)], []) -> do 52 | let newPath = toString $ oldPath <> ".html" 53 | doesPathExist (path newPath) >>= \case 54 | True -> pure ([toText newPath], []) 55 | False -> pure pq 56 | _ -> 57 | pure pq 58 | -------------------------------------------------------------------------------- /src/Neuron/CLI/App.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GADTs #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RankNTypes #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE NoImplicitPrelude #-} 8 | 9 | module Neuron.CLI.App 10 | ( run, 11 | ) 12 | where 13 | 14 | import Control.Concurrent.Async (race_) 15 | import Data.Tagged 16 | import Data.Time 17 | ( getCurrentTime, 18 | getCurrentTimeZone, 19 | utcToLocalTime, 20 | ) 21 | import qualified Neuron.Backend as Backend 22 | import qualified Neuron.CLI.Logging as Logging 23 | import Neuron.CLI.New (newZettelFile) 24 | import Neuron.CLI.Open (openLocallyGeneratedFile) 25 | import Neuron.CLI.Parser (commandParser) 26 | import Neuron.CLI.Query (runQuery) 27 | import Neuron.CLI.Search (interactiveSearch) 28 | import Neuron.CLI.Types 29 | import qualified Neuron.Version as Version 30 | import Options.Applicative 31 | import Relude 32 | import System.Console.ANSI (hSupportsANSI) 33 | import System.Directory (getCurrentDirectory) 34 | import System.IO (hIsTerminalDevice) 35 | 36 | run :: (GenCommand -> App ()) -> IO () 37 | run act = do 38 | defaultNotesDir <- getCurrentDirectory 39 | cliParser <- commandParser defaultNotesDir <$> now 40 | app <- 41 | execParser $ 42 | info 43 | (versionOption <*> cliParser <**> helper) 44 | (fullDesc <> progDesc "Neuron, future-proof Zettelkasten app ") 45 | useColors <- liftA2 (&&) (hIsTerminalDevice stderr) (hSupportsANSI stderr) 46 | let logAction = Logging.mkLogAction useColors 47 | runApp (Env app logAction) $ runAppCommand act 48 | where 49 | versionOption = 50 | infoOption 51 | (toString $ untag Version.neuronVersion) 52 | (long "version" <> help "Show version") 53 | now = do 54 | tz <- getCurrentTimeZone 55 | utcToLocalTime tz <$> liftIO getCurrentTime 56 | 57 | runAppCommand :: (GenCommand -> App ()) -> App () 58 | runAppCommand genAct = do 59 | getCommand >>= \case 60 | LSP -> do 61 | -- LSP.lspServer 62 | putStrLn "Not Implemented" 63 | Gen (mserve, gen) -> do 64 | case mserve of 65 | Just (ServeCommand host port) -> do 66 | outDir <- getOutputDir 67 | appEnv <- getAppEnv 68 | liftIO $ 69 | race_ (runApp appEnv $ genAct gen) $ do 70 | runApp appEnv $ Backend.serve host port outDir 71 | Nothing -> 72 | genAct gen 73 | New newCommand -> 74 | newZettelFile newCommand 75 | Open openCommand -> 76 | openLocallyGeneratedFile openCommand 77 | Query queryCommand -> do 78 | runQuery queryCommand 79 | Search searchCmd -> do 80 | interactiveSearch searchCmd 81 | -------------------------------------------------------------------------------- /src/Neuron/CLI/Logging.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DerivingStrategies #-} 2 | {-# LANGUAGE GADTs #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE PatternSynonyms #-} 6 | {-# LANGUAGE RankNTypes #-} 7 | {-# LANGUAGE RecordWildCards #-} 8 | {-# LANGUAGE ScopedTypeVariables #-} 9 | {-# LANGUAGE ViewPatterns #-} 10 | {-# LANGUAGE NoImplicitPrelude #-} 11 | 12 | module Neuron.CLI.Logging where 13 | 14 | import Colog 15 | ( LogAction, 16 | Msg (Msg, msgSeverity, msgStack, msgText), 17 | cmap, 18 | logTextStderr, 19 | ) 20 | import qualified Data.Text as T 21 | import Relude 22 | import System.Console.ANSI 23 | ( Color (..), 24 | ColorIntensity (Vivid), 25 | ConsoleLayer (Foreground), 26 | SGR (..), 27 | setSGRCode, 28 | ) 29 | import qualified Text.Show as Text 30 | 31 | -- | Emoji prefix in log messages 32 | data LogMoji 33 | = Done 34 | | Wait 35 | | WWW 36 | | Sent 37 | | Trashed 38 | | Received 39 | | -- | Custom string can be used to substitute for an emoji. 40 | -- It must be of length two, inasmuch as many emojis take two-letter space. 41 | Custom Char Char 42 | deriving stock (Read, Eq, Ord) 43 | 44 | instance Text.Show LogMoji where 45 | show = \case 46 | Done -> 47 | "🥅" 48 | Wait -> 49 | "⏳" 50 | WWW -> 51 | "🌐" 52 | Sent -> 53 | "💾" 54 | Trashed -> 55 | "🗑️ " 56 | Received -> 57 | "️📝" 58 | Custom a b -> 59 | [a, b] 60 | 61 | -- | Severity with an optional emoji 62 | data Severity 63 | = Debug (Maybe LogMoji) 64 | | Info (Maybe LogMoji) 65 | | Warning (Maybe LogMoji) 66 | | Error (Maybe LogMoji) 67 | deriving stock (Show, Read, Eq, Ord) 68 | 69 | pattern D, I, W, E, EE :: Severity 70 | pattern D <- Debug Nothing where D = Debug Nothing 71 | pattern I <- Info Nothing where I = Info Nothing 72 | pattern W <- Warning Nothing where W = Warning Nothing 73 | pattern E <- Error Nothing where E = Error Nothing 74 | pattern EE <- Error (Just (Custom '!' '!')) where EE = Error (Just $ Custom '!' '!') 75 | 76 | pattern D', I', W', E' :: LogMoji -> Severity 77 | pattern D' le <- Debug (Just le) where D' le = Debug (Just le) 78 | pattern I' le <- Info (Just le) where I' le = Info (Just le) 79 | pattern W' le <- Warning (Just le) where W' le = Warning (Just le) 80 | pattern E' le <- Error (Just le) where E' le = Error (Just le) 81 | 82 | type Message = Msg Severity 83 | 84 | mkLogAction :: (MonadIO m) => Bool -> LogAction m Message 85 | mkLogAction useColors = 86 | cmap fmtNeuronMsg logTextStderr 87 | where 88 | fmtNeuronMsg :: Message -> Text 89 | fmtNeuronMsg Msg {..} = 90 | let emptyEmoji = Custom ' ' ' ' 91 | f c (fromMaybe emptyEmoji -> le) = bool id (colorize c) useColors $ show le <> " " <> msgText 92 | in case msgSeverity of 93 | Debug mle -> f Black mle 94 | Info mle -> f Blue mle 95 | Warning mle -> f Yellow mle 96 | Error mle -> f Red mle 97 | colorize :: Color -> Text -> Text 98 | colorize c txt = 99 | T.pack (setSGRCode [SetColor Foreground Vivid c]) 100 | <> txt 101 | <> T.pack (setSGRCode [Reset]) 102 | 103 | indentAllButFirstLine :: Int -> Text -> Text 104 | indentAllButFirstLine n = T.strip . unlines . go . lines 105 | where 106 | go [] = [] 107 | go [x] = [x] 108 | go (x : xs) = 109 | x : fmap (toText . (replicate n ' ' <>) . toString) xs 110 | -------------------------------------------------------------------------------- /src/Neuron/CLI/New.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RankNTypes #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE ViewPatterns #-} 8 | {-# LANGUAGE NoImplicitPrelude #-} 9 | 10 | module Neuron.CLI.New 11 | ( newZettelFile, 12 | ) 13 | where 14 | 15 | import Colog (WithLog) 16 | import qualified Data.Set as Set 17 | import Data.Some (withSome) 18 | import Data.Text (strip) 19 | import qualified Data.Text as T 20 | import Data.Time.DateMayTime (DateMayTime, formatDateMayTime) 21 | import Neuron.CLI.Logging 22 | import Neuron.CLI.Types (MonadApp, NewCommand (..), getNotesDir) 23 | import qualified Neuron.Cache.Type as Cache 24 | import Neuron.Reactor as Reactor (loadZettelkasten) 25 | import qualified Neuron.Zettelkasten.Graph as G 26 | import Neuron.Zettelkasten.ID (zettelIDSourceFileName) 27 | import qualified Neuron.Zettelkasten.ID.Scheme as IDScheme 28 | import Neuron.Zettelkasten.Zettel (zettelID) 29 | import Relude 30 | import System.Directory (setCurrentDirectory) 31 | import System.FilePath (()) 32 | import qualified System.Posix.Env as Env 33 | import System.Posix.Process (executeFile) 34 | 35 | -- | Create a new zettel file and open it in editor if requested 36 | -- 37 | -- As well as print the path to the created file. 38 | newZettelFile :: (MonadIO m, MonadApp m, MonadFail m, WithLog env Message m) => NewCommand -> m () 39 | newZettelFile NewCommand {..} = do 40 | Reactor.loadZettelkasten >>= \case 41 | Left e -> fail $ toString e 42 | Right (g, _, _) -> do 43 | mzid <- withSome idScheme $ \scheme -> do 44 | val <- liftIO $ IDScheme.genVal scheme 45 | pure $ 46 | IDScheme.nextAvailableZettelID 47 | (Set.fromList $ fmap zettelID $ G.getZettels $ Cache.neuroncacheGraph g) 48 | val 49 | scheme 50 | case mzid of 51 | Left e -> die $ show e 52 | Right zid -> do 53 | notesDir <- getNotesDir 54 | let zettelFile = zettelIDSourceFileName zid 55 | liftIO $ do 56 | fileAction :: FilePath -> FilePath -> IO () <- 57 | bool (pure showAction) mkEditActionFromEnv edit 58 | writeFileText (notesDir zettelFile) $ defaultZettelContent date 59 | fileAction notesDir zettelFile 60 | where 61 | mkEditActionFromEnv :: IO (FilePath -> FilePath -> IO ()) 62 | mkEditActionFromEnv = 63 | getEnvNonEmpty "EDITOR" >>= \case 64 | Nothing -> 65 | die "\n-e option can only be used with EDITOR environment variable set" 66 | Just editorCli -> 67 | pure $ editAction editorCli 68 | editAction editorCli notesDir zettelFile = do 69 | -- Show the path first, in case the editor launch fails 70 | showAction notesDir zettelFile 71 | setCurrentDirectory notesDir 72 | executeShellCommand $ editorCli <> " " <> zettelFile 73 | showAction notesDir zettelFile = 74 | putStrLn $ notesDir zettelFile 75 | -- Like `executeFile` but takes a shell command. 76 | executeShellCommand cmd = 77 | executeFile "bash" True ["-c", cmd] Nothing 78 | getEnvNonEmpty name = 79 | Env.getEnv name >>= \case 80 | Nothing -> pure Nothing 81 | Just (toString . strip . toText -> v) -> 82 | if null v then pure Nothing else pure (Just v) 83 | 84 | defaultZettelContent :: DateMayTime -> Text 85 | defaultZettelContent (formatDateMayTime -> date) = 86 | T.intercalate 87 | "\n" 88 | [ "---", 89 | "date: " <> date, 90 | "---", 91 | "" 92 | ] 93 | -------------------------------------------------------------------------------- /src/Neuron/CLI/Open.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | 4 | module Neuron.CLI.Open 5 | ( openLocallyGeneratedFile, 6 | ) 7 | where 8 | 9 | import Data.Some (foldSome) 10 | import Neuron.CLI.Types (MonadApp, OpenCommand (..), getOutputDir) 11 | import Neuron.Frontend.Route (routeHtmlPath) 12 | import Relude 13 | import System.Directory (doesFileExist) 14 | import System.FilePath (()) 15 | import System.Info (os) 16 | import System.Posix.Process (executeFile) 17 | 18 | openLocallyGeneratedFile :: (MonadIO m, MonadApp m, MonadFail m) => OpenCommand -> m () 19 | openLocallyGeneratedFile (OpenCommand route) = do 20 | let relHtmlPath = routeHtmlPath `foldSome` route 21 | opener = if os == "darwin" then "open" else "xdg-open" 22 | htmlPath <- fmap ( relHtmlPath) getOutputDir 23 | liftIO (doesFileExist htmlPath) >>= \case 24 | False -> do 25 | fail "No generated HTML found. Try runing `neuron gen` first." 26 | True -> do 27 | liftIO $ executeFile opener True [htmlPath] Nothing 28 | -------------------------------------------------------------------------------- /src/Neuron/CLI/Query.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE GADTs #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RankNTypes #-} 6 | {-# LANGUAGE RecordWildCards #-} 7 | {-# LANGUAGE ScopedTypeVariables #-} 8 | {-# LANGUAGE NoImplicitPrelude #-} 9 | 10 | module Neuron.CLI.Query 11 | ( runQuery, 12 | ) 13 | where 14 | 15 | import Colog (WithLog) 16 | import Data.Aeson (ToJSON) 17 | import qualified Data.Aeson as Aeson 18 | import qualified Data.Aeson.Encode.Pretty as AesonPretty 19 | import qualified Data.Set as Set 20 | import Data.Some (withSome) 21 | import qualified Data.TagTree as TagTree 22 | import Neuron.CLI.Logging (Message) 23 | import Neuron.CLI.Types 24 | import qualified Neuron.Cache as Cache 25 | import qualified Neuron.Cache.Type as Cache 26 | import qualified Neuron.Plugin.Plugins.Tags as Tags 27 | import qualified Neuron.Reactor as Reactor 28 | import qualified Neuron.Zettelkasten.Graph as G 29 | import qualified Neuron.Zettelkasten.Query as Q 30 | import Relude 31 | 32 | runQuery :: forall m env. (MonadApp m, MonadFail m, MonadApp m, MonadIO m, WithLog env Message m) => QueryCommand -> m () 33 | runQuery QueryCommand {..} = do 34 | Cache.NeuronCache {..} <- 35 | fmap Cache.stripCache $ 36 | if cached 37 | then Cache.getCache 38 | else do 39 | Reactor.loadZettelkasten >>= \case 40 | Left e -> fail $ toString e 41 | Right (ch, _, _) -> pure ch 42 | case query of 43 | CliQuery_ById zid -> do 44 | let result = G.getZettel zid neuroncacheGraph 45 | bool printPrettyJson (printJsonLine . maybeToList) jsonl result 46 | CliQuery_Zettels -> do 47 | let result = G.getZettels neuroncacheGraph 48 | bool printPrettyJson printJsonLine jsonl result 49 | CliQuery_Tags -> do 50 | let result = Set.unions $ Tags.getZettelTags <$> G.getZettels neuroncacheGraph 51 | bool printPrettyJson (printJsonLine . Set.toList) jsonl result 52 | CliQuery_ByTag tag -> do 53 | let q = TagTree.mkDefaultTagQuery $ one $ TagTree.mkTagPatternFromTag tag 54 | zs = G.getZettels neuroncacheGraph 55 | result = Tags.zettelsByTag Tags.getZettelTags zs q 56 | bool printPrettyJson printJsonLine jsonl result 57 | CliQuery_Graph someQ -> 58 | withSome someQ $ \q -> do 59 | result <- either (fail . show) pure $ Q.runGraphQuery neuroncacheGraph q 60 | bool printPrettyJson printJsonLine jsonl [Q.graphQueryResultJson q result neuroncacheErrors] 61 | where 62 | printJsonLine :: ToJSON a => [a] -> m () 63 | printJsonLine = mapM_ (putLBSLn . Aeson.encode) 64 | printPrettyJson :: ToJSON a => a -> m () 65 | printPrettyJson = 66 | putLBSLn 67 | . AesonPretty.encodePretty' 68 | AesonPretty.defConfig 69 | { -- Sort hash map by keys for consistency 70 | AesonPretty.confCompare = compare 71 | } 72 | -------------------------------------------------------------------------------- /src/Neuron/CLI/Search.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE RankNTypes #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE TemplateHaskell #-} 7 | {-# LANGUAGE NoImplicitPrelude #-} 8 | 9 | module Neuron.CLI.Search 10 | ( interactiveSearch, 11 | ) 12 | where 13 | 14 | import Neuron.CLI.Types (MonadApp, SearchBy (SearchByContent, SearchByTitle), SearchCommand (..), getNotesDir) 15 | import Relude 16 | import System.Posix.Process (executeFile) 17 | import System.Which (staticWhich) 18 | 19 | neuronSearchScript :: FilePath 20 | neuronSearchScript = $(staticWhich "neuron-search") 21 | 22 | searchScriptArgs :: SearchCommand -> [String] 23 | searchScriptArgs SearchCommand {..} = 24 | let extensionPattern = "**/*{.md}" 25 | searchByArgs = 26 | case searchBy of 27 | SearchByTitle -> ["(^# )|(^title: )", "2", extensionPattern] 28 | SearchByContent -> ["", "2", extensionPattern] 29 | editArg = 30 | bool "echo" "$EDITOR" searchEdit 31 | in searchByArgs <> [editArg] 32 | 33 | interactiveSearch :: (MonadIO m, MonadApp m) => SearchCommand -> m () 34 | interactiveSearch searchCmd = do 35 | notesDir <- getNotesDir 36 | liftIO $ execScript neuronSearchScript $ notesDir : searchScriptArgs searchCmd 37 | where 38 | execScript scriptPath args = 39 | -- We must use the low-level execvp (via the unix package's `executeFile`) 40 | -- here, such that the new process replaces the current one. fzf won't work 41 | -- otherwise. 42 | void $ executeFile scriptPath False args Nothing 43 | -------------------------------------------------------------------------------- /src/Neuron/Cache.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE NoImplicitPrelude #-} 7 | 8 | -- | Responsible for caching zettelkasten graph on disk 9 | module Neuron.Cache where 10 | 11 | import Data.Aeson (eitherDecodeFileStrict, encodeFile) 12 | import Neuron.CLI.Types (MonadApp, getOutputDir) 13 | import Neuron.Cache.Type (NeuronCache (neuroncacheGraph), ReadMode (..)) 14 | import Neuron.Config.Type (Config) 15 | import qualified Neuron.Plugin as Plugin 16 | import Relude 17 | import System.Directory (createDirectoryIfMissing) 18 | import System.FilePath (()) 19 | 20 | cacheFile :: (MonadIO m, MonadApp m) => m FilePath 21 | cacheFile = do 22 | outputDir <- getOutputDir 23 | liftIO $ createDirectoryIfMissing True outputDir 24 | pure $ outputDir "cache.json" 25 | 26 | updateCache :: (MonadIO m, MonadApp m) => NeuronCache -> m () 27 | updateCache v = do 28 | f <- cacheFile 29 | liftIO $ encodeFile f v 30 | 31 | getCache :: (MonadIO m, MonadApp m, MonadFail m) => m NeuronCache 32 | getCache = do 33 | (liftIO . eitherDecodeFileStrict =<< cacheFile) >>= \case 34 | Left err -> fail err 35 | Right v -> pure v 36 | 37 | stripCache :: NeuronCache -> NeuronCache 38 | stripCache cache = 39 | cache {neuroncacheGraph = Plugin.stripSurroundingContext $ neuroncacheGraph cache} 40 | 41 | evalUnlessCacheRequested :: 42 | (MonadIO m, MonadFail m, MonadApp m) => ReadMode -> (Config -> m NeuronCache) -> m NeuronCache 43 | evalUnlessCacheRequested mode f = do 44 | case mode of 45 | ReadMode_Direct config -> 46 | f config 47 | ReadMode_Cached -> do 48 | (liftIO . eitherDecodeFileStrict =<< cacheFile) >>= \case 49 | Left err -> fail err 50 | Right v -> pure v 51 | -------------------------------------------------------------------------------- /src/Neuron/Cache/Type.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE DeriveAnyClass #-} 3 | {-# LANGUAGE DeriveGeneric #-} 4 | {-# LANGUAGE FlexibleContexts #-} 5 | {-# LANGUAGE LambdaCase #-} 6 | {-# LANGUAGE OverloadedStrings #-} 7 | {-# LANGUAGE ScopedTypeVariables #-} 8 | {-# LANGUAGE NoImplicitPrelude #-} 9 | 10 | module Neuron.Cache.Type where 11 | 12 | import Data.Aeson (FromJSON, ToJSON) 13 | import qualified Data.Aeson as Aeson 14 | import Neuron.Config.Type (Config) 15 | import Neuron.Frontend.Route.Data.Types (NeuronVersion) 16 | import Neuron.Frontend.Widget (LoadableData (..)) 17 | import Neuron.Zettelkasten.Graph.Type (ZettelGraph) 18 | import Neuron.Zettelkasten.ID (ZettelID) 19 | import Neuron.Zettelkasten.Zettel (shortRecordFields) 20 | import Neuron.Zettelkasten.Zettel.Error (ZettelIssue) 21 | import Reflex.Dom.Core 22 | import Relude 23 | import qualified Relude.String as BL 24 | 25 | data ReadMode 26 | = ReadMode_Direct Config 27 | | ReadMode_Cached 28 | deriving (Eq, Show) 29 | 30 | data NeuronCache = NeuronCache 31 | { neuroncacheGraph :: ZettelGraph, 32 | neuroncacheErrors :: Map ZettelID ZettelIssue, 33 | neuroncacheConfig :: Config, 34 | neuroncacheNeuronVersion :: NeuronVersion 35 | } 36 | deriving (Eq, Show, Generic) 37 | 38 | instance ToJSON NeuronCache where 39 | toJSON = Aeson.genericToJSON shortRecordFields 40 | 41 | instance FromJSON NeuronCache where 42 | parseJSON = Aeson.genericParseJSON shortRecordFields 43 | 44 | reflexDomGetCache :: 45 | ( DomBuilder t m, 46 | Prerender js t m, 47 | TriggerEvent t m, 48 | PerformEvent t m, 49 | PostBuild t m, 50 | MonadHold t m 51 | ) => 52 | LoadableData NeuronCache -> 53 | m (Dynamic t (LoadableData NeuronCache)) 54 | reflexDomGetCache staticCache = do 55 | join <$> prerender (pure $ constDyn staticCache) getCacheRemote 56 | where 57 | getCacheRemote = do 58 | -- TODO: refactor 59 | pb <- getPostBuild 60 | resp' <- 61 | performRequestAsyncWithError $ 62 | XhrRequest "GET" "cache.json" def <$ pb 63 | let resp = ffor resp' $ first show >=> decodeXhrResponseWithError 64 | mresp <- fmap LoadableData <$> holdDyn Nothing (Just <$> resp) 65 | -- Workaround for thrice triggering bug? 66 | holdUniqDyn mresp 67 | where 68 | decodeXhrResponseWithError :: FromJSON a => XhrResponse -> Either String a 69 | decodeXhrResponseWithError = 70 | fromMaybe (Left "Empty response") . sequence 71 | . traverse (Aeson.eitherDecode . BL.fromStrict . encodeUtf8) 72 | . _xhrResponse_responseText 73 | -------------------------------------------------------------------------------- /src/Neuron/Config.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE DerivingStrategies #-} 4 | {-# LANGUAGE FlexibleContexts #-} 5 | {-# LANGUAGE LambdaCase #-} 6 | {-# LANGUAGE OverloadedStrings #-} 7 | {-# LANGUAGE QuasiQuotes #-} 8 | {-# LANGUAGE RecordWildCards #-} 9 | {-# LANGUAGE ScopedTypeVariables #-} 10 | {-# LANGUAGE StandaloneDeriving #-} 11 | {-# LANGUAGE TypeApplications #-} 12 | {-# LANGUAGE ViewPatterns #-} 13 | {-# LANGUAGE NoImplicitPrelude #-} 14 | -- For deriving of FromDhall Config 15 | {-# OPTIONS_GHC -fno-warn-orphans #-} 16 | 17 | module Neuron.Config 18 | ( getConfigFromFile, 19 | missingConfigError, 20 | parsePure, 21 | ) 22 | where 23 | 24 | import Data.Either.Validation (validationToEither) 25 | import qualified Data.Text as T 26 | import Dhall (FromDhall) 27 | import qualified Dhall (Decoder (extract), auto) 28 | import qualified Dhall.Core (normalize) 29 | import qualified Dhall.Parser (exprFromText) 30 | import qualified Dhall.TypeCheck (typeOf) 31 | import Neuron.Config.Type (Config, configFile, defaultConfig, mergeWithDefault) 32 | import Relude 33 | 34 | deriving instance FromDhall Config 35 | 36 | getConfigFromFile :: MonadIO m => FilePath -> m (Either Text Config) 37 | getConfigFromFile configPath = do 38 | s <- readFileText configPath 39 | -- Accept empty neuron.dhall (used to signify a directory to be used with neuron) 40 | let configVal = 41 | if T.null (T.strip s) 42 | then defaultConfig 43 | else mergeWithDefault s 44 | pure $ first toText $ parsePure configFile $ mergeWithDefault configVal 45 | 46 | missingConfigError :: FilePath -> Text 47 | missingConfigError notesDir = do 48 | T.intercalate 49 | "\n" 50 | [ "No neuron.dhall found", 51 | "You must add a neuron.dhall to " <> toText notesDir, 52 | "You can add one by running:", 53 | " touch " <> toText notesDir <> "/neuron.dhall" 54 | ] 55 | 56 | -- | Pure version of `Dhall.input Dhall.auto` 57 | -- 58 | -- The config file cannot have imports, as that requires IO. 59 | parsePure :: forall a. FromDhall a => FilePath -> Text -> Either String a 60 | parsePure fn s = do 61 | expr0 <- first show $ Dhall.Parser.exprFromText fn s 62 | expr <- maybeToRight "Cannot have imports" $ traverse (const Nothing) expr0 63 | void $ first show $ Dhall.TypeCheck.typeOf expr 64 | first show $ 65 | validationToEither $ 66 | Dhall.extract @a Dhall.auto $ 67 | Dhall.Core.normalize expr 68 | -------------------------------------------------------------------------------- /src/Neuron/Config/Type.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE ViewPatterns #-} 8 | {-# LANGUAGE NoImplicitPrelude #-} 9 | 10 | module Neuron.Config.Type 11 | ( Config (..), 12 | configFile, 13 | defaultConfig, 14 | mergeWithDefault, 15 | getSiteBaseUrl, 16 | getPlugins, 17 | ) 18 | where 19 | 20 | import Data.Aeson 21 | import Neuron.Plugin (PluginRegistry) 22 | import qualified Neuron.Plugin as Plugin 23 | import Relude hiding (readEither) 24 | import Text.URI (URI, mkURI) 25 | 26 | configFile :: FilePath 27 | configFile = "neuron.dhall" 28 | 29 | -- | Config type for @neuron.dhall@ 30 | -- 31 | -- See for description of the fields. 32 | -- 33 | -- TODO: Implement custom `FromDhall` instance, while using original field types 34 | data Config = Config 35 | { author :: Maybe Text, 36 | editUrl :: Maybe Text, 37 | siteBaseUrl :: Maybe Text, 38 | siteTitle :: Text, 39 | theme :: Text, 40 | plugins :: [Text] 41 | } 42 | deriving (Eq, Show, Generic, FromJSON, ToJSON) 43 | 44 | getSiteBaseUrl :: MonadFail m => Config -> m (Maybe URI) 45 | getSiteBaseUrl Config {..} = 46 | runMaybeT $ do 47 | s <- MaybeT $ pure siteBaseUrl 48 | case mkURI s of 49 | Left e -> 50 | fail $ displayException e 51 | Right uri -> 52 | pure uri 53 | 54 | getPlugins :: Config -> PluginRegistry 55 | getPlugins = Plugin.lookupPlugins . plugins 56 | 57 | defaultConfig :: Text 58 | defaultConfig = 59 | "{ siteTitle =\ 60 | \ \"My Zettelkasten\" \ 61 | \, author =\ 62 | \ None Text\ 63 | \, siteBaseUrl =\ 64 | \ None Text\ 65 | \, editUrl =\ 66 | \ None Text\ 67 | \, theme =\ 68 | \ \"blue\"\ 69 | \, plugins =\ 70 | \ [\"neuronignore\", \"links\", \"tags\", \"uptree\", \"feed\"] \ 71 | \}" 72 | 73 | -- Dhall's combine operator (`//`) allows us to merge two records, 74 | -- effectively merging the record with defaults with the user record. 75 | mergeWithDefault :: Text -> Text 76 | mergeWithDefault userConfig = 77 | defaultConfig <> " // " <> userConfig 78 | -------------------------------------------------------------------------------- /src/Neuron/Frontend/CSS.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE TypeApplications #-} 4 | {-# LANGUAGE NoImplicitPrelude #-} 5 | 6 | module Neuron.Frontend.CSS where 7 | 8 | import Clay (Css, em, gray, important, pct, (?)) 9 | import qualified Clay as C 10 | import Neuron.Frontend.Common (neuronCommonStyle) 11 | import qualified Neuron.Frontend.Impulse as Impulse 12 | import Neuron.Frontend.Theme (Theme) 13 | import qualified Neuron.Frontend.Zettel.CSS as ZettelCSS 14 | import Relude 15 | 16 | neuronStyleForTheme :: Theme -> Css 17 | neuronStyleForTheme theme = do 18 | "body" ? do 19 | neuronCommonStyle 20 | Impulse.style 21 | ZettelCSS.zettelCss theme 22 | footerStyle 23 | where 24 | footerStyle = do 25 | ".footer-version img" ? do 26 | C.filter $ C.grayscale $ pct 100 27 | ".footer-version img:hover" ? do 28 | C.filter $ C.grayscale $ pct 0 29 | ".footer-version, .footer-version a, .footer-version a:visited" ? do 30 | C.color gray 31 | ".footer-version a" ? do 32 | C.fontWeight C.bold 33 | ".footer-version" ? do 34 | important $ C.marginTop $ em 1 35 | C.fontSize $ em 0.7 36 | -------------------------------------------------------------------------------- /src/Neuron/Frontend/Common.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE NoImplicitPrelude #-} 4 | 5 | module Neuron.Frontend.Common where 6 | 7 | import Clay (Css, (?)) 8 | import qualified Clay as C 9 | import Relude 10 | 11 | neuronCommonStyle :: Css 12 | neuronCommonStyle = do 13 | C.important $ C.backgroundColor "#eee" 14 | C.important $ C.fontFamily [bodyFont] [C.serif] 15 | ".ui.container" ? do 16 | -- Override Semantic UI's font 17 | C.important $ C.fontFamily [bodyFont] [C.serif] 18 | "h1, h2, h3, h4, h5, h6, .ui.header, .headerFont" ? do 19 | C.important $ C.fontFamily [headerFont] [C.sansSerif] 20 | "code, pre, tt, .monoFont" ? do 21 | C.important $ C.fontFamily [monoFont, "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New"] [C.monospace] 22 | 23 | neuronFonts :: [Text] 24 | neuronFonts = [headerFont, bodyFont, monoFont] 25 | 26 | headerFont :: Text 27 | headerFont = "Merriweather" 28 | 29 | bodyFont :: Text 30 | bodyFont = "Libre Franklin" 31 | 32 | monoFont :: Text 33 | monoFont = "Roboto Mono" 34 | -------------------------------------------------------------------------------- /src/Neuron/Frontend/Manifest.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE RecordWildCards #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE ViewPatterns #-} 5 | {-# LANGUAGE NoImplicitPrelude #-} 6 | 7 | module Neuron.Frontend.Manifest 8 | ( Manifest, 9 | manifestPatterns, 10 | mkManifest, 11 | mkManifestFromTree, 12 | getIconPath, 13 | renderManifest, 14 | ) 15 | where 16 | 17 | import Data.Default 18 | import qualified Data.Set as Set 19 | import Reflex.Dom.Core 20 | import Relude 21 | import qualified System.Directory.Contents as DC 22 | 23 | data Manifest = Manifest 24 | { manifestFavicons :: Maybe Favicons, 25 | manifestWebAppManifest :: Maybe FilePath 26 | } 27 | deriving (Eq, Show) 28 | 29 | instance Default Manifest where 30 | def = Manifest Nothing Nothing 31 | 32 | data Favicons = Favicons 33 | { faviconsDefault :: FilePath, 34 | faviconsAlts :: [FilePath], 35 | faviconsAppleTouch :: Maybe FilePath 36 | } 37 | deriving (Eq, Show) 38 | 39 | mkManifestFromTree :: DC.DirTree FilePath -> Manifest 40 | mkManifestFromTree fileTree = 41 | let availableFiles = fforMaybe manifestPatterns $ \fp -> do 42 | case DC.walkContents fp fileTree of 43 | Just (DC.DirTree_File _ _) -> 44 | Just fp 45 | _ -> 46 | Nothing 47 | in mkManifest availableFiles 48 | 49 | mkManifest :: [FilePath] -> Manifest 50 | mkManifest (Set.fromList -> files) = 51 | Manifest 52 | { manifestFavicons = mkFavicons, 53 | manifestWebAppManifest = lookupSet webmanifestFile files 54 | } 55 | where 56 | mkFavicons = 57 | case filter (`Set.member` files) favicons of 58 | (ico : alts) -> 59 | Just $ 60 | Favicons 61 | { faviconsDefault = ico, 62 | faviconsAlts = alts, 63 | faviconsAppleTouch = lookupSet appleTouchIcon files 64 | } 65 | [] -> 66 | Nothing 67 | lookupSet x s = 68 | if Set.member x s 69 | then Just x 70 | else Nothing 71 | 72 | getIconPath :: Manifest -> Maybe FilePath 73 | getIconPath m = do 74 | icos <- manifestFavicons m 75 | pure $ faviconsDefault icos 76 | 77 | renderManifest :: DomBuilder t m => Manifest -> m () 78 | renderManifest Manifest {..} = do 79 | linkRel "manifest" `mapM_` manifestWebAppManifest 80 | case manifestFavicons of 81 | Nothing -> 82 | linkRel "icon" defaultFaviconUrl 83 | Just Favicons {..} -> do 84 | linkRel "icon" faviconsDefault 85 | linkRel "alternate icon" `mapM_` faviconsAlts 86 | linkRel "apple-touch-icon" `mapM_` faviconsAppleTouch 87 | where 88 | defaultFaviconUrl :: String 89 | defaultFaviconUrl = 90 | "https://raw.githubusercontent.com/srid/neuron/master/assets/neuron.svg" 91 | linkRel rel path = 92 | -- crossorigin="use-credentials" 93 | elAttr 94 | "link" 95 | ( "rel" =: rel 96 | <> "href" =: toText path 97 | -- The use-credentials value must be used when fetching a manifest that requires credentials, even if the file is from the same origin. 98 | -- cf. https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin 99 | -- cf. https://thatemil.com/blog/2018/02/21/pwa-basic-auth/ 100 | <> (if rel == "manifest" then "crossorigin" =: "use-credentials" else mempty) 101 | ) 102 | blank 103 | 104 | manifestPatterns :: [FilePath] 105 | manifestPatterns = 106 | favicons <> [appleTouchIcon, webmanifestFile] 107 | 108 | -- | Supported favicons, in order of preference 109 | favicons :: [FilePath] 110 | favicons = fmap ("static/favicon." <>) ["svg", "png", "ico", "jpg", "jpeg"] 111 | 112 | appleTouchIcon :: FilePath 113 | appleTouchIcon = "static/apple-touch-icon.png" 114 | 115 | webmanifestFile :: FilePath 116 | webmanifestFile = "static/manifest.webmanifest" 117 | -------------------------------------------------------------------------------- /src/Neuron/Frontend/Route/Data.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric #-} 2 | {-# LANGUAGE GADTs #-} 3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 4 | {-# LANGUAGE LambdaCase #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE RecordWildCards #-} 7 | {-# LANGUAGE ScopedTypeVariables #-} 8 | {-# LANGUAGE NoImplicitPrelude #-} 9 | 10 | module Neuron.Frontend.Route.Data where 11 | 12 | import qualified Clay.Render as C 13 | import qualified Data.Dependent.Map as DMap 14 | import Data.Foldable (Foldable (maximum)) 15 | import Data.TagTree (mkDefaultTagQuery, mkTagPattern) 16 | import Data.Tree (Forest, Tree (..)) 17 | import Neuron.Cache.Type (NeuronCache (..)) 18 | import qualified Neuron.Config.Type as Config 19 | import Neuron.Frontend.CSS (neuronStyleForTheme) 20 | import Neuron.Frontend.Manifest (Manifest) 21 | import Neuron.Frontend.Route (RouteConfig) 22 | import Neuron.Frontend.Route.Data.Types 23 | import qualified Neuron.Frontend.Theme as Theme 24 | import qualified Neuron.Plugin as Plugin 25 | import qualified Neuron.Plugin.Plugins.Tags as Tags 26 | import Neuron.Zettelkasten.Connection (Connection (Folgezettel)) 27 | import qualified Neuron.Zettelkasten.Graph as G 28 | import Neuron.Zettelkasten.ID (indexZid) 29 | import Neuron.Zettelkasten.Zettel 30 | import Relude hiding (traceShowId) 31 | 32 | mkZettelData :: RouteConfig t m -> [ZettelC] -> NeuronCache -> SiteData -> ZettelC -> ZettelData 33 | mkZettelData routeCfg zs NeuronCache {..} siteData zC = do 34 | let z = sansContent zC 35 | pluginData = 36 | DMap.fromList $ 37 | Plugin.routePluginData routeCfg siteData zs neuroncacheGraph zC <$> maybe mempty DMap.toList (zettelPluginData z) 38 | ZettelData zC pluginData 39 | 40 | mkImpulseData :: NeuronCache -> ImpulseData 41 | mkImpulseData NeuronCache {..} = 42 | buildImpulse neuroncacheGraph neuroncacheErrors 43 | where 44 | buildImpulse graph errors = 45 | let (orphans, clusters) = partitionEithers $ 46 | flip fmap (G.categoryClusters graph) $ \case 47 | [Node z []] -> Left z -- Orphans (cluster of exactly one) 48 | x -> Right x 49 | clustersWithUplinks :: [Forest (Zettel, [Zettel])] = 50 | -- Compute backlinks for each node in the tree. 51 | flip fmap clusters $ \(zs :: [Tree Zettel]) -> 52 | G.backlinksMulti Folgezettel zs graph 53 | stats = Stats (length $ G.getZettels graph) (G.connectionCount graph) 54 | pinnedZettels = Tags.zettelsByTag Tags.getZettelTags (G.getZettels graph) $ mkDefaultTagQuery [mkTagPattern "pinned"] 55 | in ImpulseData (fmap sortCluster clustersWithUplinks) orphans errors stats pinnedZettels 56 | -- TODO: Either optimize or get rid of this (or normalize the sorting somehow) 57 | sortCluster fs = 58 | sortZettelForest $ 59 | flip fmap fs $ \Node {..} -> 60 | Node rootLabel $ sortZettelForest subForest 61 | -- Sort zettel trees so that trees containing the most recent zettel (by ID) come first. 62 | sortZettelForest = sortOn (Down . maximum) 63 | 64 | mkSiteData :: NeuronCache -> HeadHtml -> Manifest -> SiteData 65 | mkSiteData NeuronCache {..} headHtml manifest = 66 | let theme = Theme.mkTheme $ Config.theme neuroncacheConfig 67 | siteTitle = Config.siteTitle neuroncacheConfig 68 | siteAuthor = Config.author neuroncacheConfig 69 | baseUrl = join $ Config.getSiteBaseUrl neuroncacheConfig 70 | indexZettel = G.getZettel indexZid neuroncacheGraph 71 | editUrl = Config.editUrl neuroncacheConfig 72 | style = do 73 | neuronStyleForTheme theme 74 | Plugin.pluginStyles (Config.getPlugins neuroncacheConfig) theme 75 | bodyCss = toText $ C.renderWith C.compact [] style 76 | in SiteData 77 | theme 78 | siteTitle 79 | siteAuthor 80 | baseUrl 81 | editUrl 82 | bodyCss 83 | headHtml 84 | manifest 85 | neuroncacheNeuronVersion 86 | indexZettel 87 | -------------------------------------------------------------------------------- /src/Neuron/Frontend/Static/HeadHtml.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE NoImplicitPrelude #-} 4 | 5 | module Neuron.Frontend.Static.HeadHtml 6 | ( HeadHtml, 7 | getHeadHtml, 8 | getHeadHtmlFromTree, 9 | renderHeadHtml, 10 | ) 11 | where 12 | 13 | import Neuron.CLI.Types (MonadApp (getNotesDir)) 14 | import Neuron.Frontend.Route.Data.Types (HeadHtml (..)) 15 | import Reflex.Dom.Core 16 | import Reflex.Dom.Pandoc.Raw (RawBuilder, elRawHtml) 17 | import Relude 18 | import System.Directory (doesFileExist) 19 | import qualified System.Directory.Contents as DC 20 | import System.FilePath (()) 21 | 22 | getHeadHtmlFromTree :: (MonadIO m, MonadApp m) => DC.DirTree FilePath -> m HeadHtml 23 | getHeadHtmlFromTree fileTree = do 24 | case DC.walkContents "head.html" fileTree of 25 | Just (DC.DirTree_File fp _) -> do 26 | headHtmlPath <- getNotesDir <&> ( fp) 27 | HeadHtml . Just <$> readFileText headHtmlPath 28 | _ -> 29 | pure $ HeadHtml Nothing 30 | 31 | getHeadHtml :: (MonadIO m, MonadApp m) => m HeadHtml 32 | getHeadHtml = do 33 | headHtmlPath <- getNotesDir <&> ( "head.html") 34 | liftIO (doesFileExist headHtmlPath) >>= \case 35 | True -> do 36 | HeadHtml . Just <$> readFileText headHtmlPath 37 | False -> 38 | pure $ HeadHtml Nothing 39 | 40 | renderHeadHtml :: (DomBuilder t m, RawBuilder m) => HeadHtml -> m () 41 | renderHeadHtml (HeadHtml headHtml) = case headHtml of 42 | Nothing -> do 43 | -- If the user doesn't specify a head.html, we provide sensible defaults. 44 | -- For Math support: 45 | js' 46 | ("id" =: "MathJax-script" <> "async" =: "") 47 | "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" 48 | -- For syntax highlighting: 49 | css "https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css" 50 | js "https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/components/prism-core.min.js" 51 | js "https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/plugins/autoloader/prism-autoloader.min.js" 52 | Just html -> 53 | elRawHtml html 54 | where 55 | css x = elAttr "link" ("rel" =: "stylesheet" <> "href" =: x) blank 56 | js = js' mempty 57 | js' attrs x = elAttr "script" (attrs <> "src" =: x) blank 58 | -------------------------------------------------------------------------------- /src/Neuron/Frontend/Static/Html.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ConstraintKinds #-} 2 | {-# LANGUAGE DataKinds #-} 3 | {-# LANGUAGE GADTs #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE TemplateHaskell #-} 8 | {-# LANGUAGE TypeApplications #-} 9 | {-# LANGUAGE NoImplicitPrelude #-} 10 | 11 | module Neuron.Frontend.Static.Html where 12 | 13 | import Control.Monad.Fix (MonadFix) 14 | import Neuron.Frontend.Manifest (renderManifest) 15 | import Neuron.Frontend.Route (Route (..)) 16 | import qualified Neuron.Frontend.Route as R 17 | import qualified Neuron.Frontend.Route.Data.Types as R 18 | import Neuron.Frontend.Static.HeadHtml (renderHeadHtml) 19 | import Neuron.Frontend.Static.StructuredData (renderStructuredData) 20 | import qualified Neuron.Frontend.View as V 21 | import qualified Neuron.Frontend.Widget as W 22 | import Reflex.Dom.Core 23 | import Reflex.Dom.Pandoc.Raw (RawBuilder) 24 | import Relude 25 | 26 | -- | Render the given route 27 | renderRoutePage :: 28 | forall t m js a. 29 | ( DomBuilder t m, 30 | RawBuilder m, 31 | MonadHold t m, 32 | PostBuild t m, 33 | MonadFix m, 34 | Prerender js t m, 35 | PerformEvent t m, 36 | TriggerEvent t m 37 | ) => 38 | R.RouteConfig t m -> 39 | Route a -> 40 | Dynamic t (W.LoadableData a) -> 41 | m () 42 | renderRoutePage routeCfg r val = do 43 | el "html" $ do 44 | el "head" $ do 45 | V.headTemplate r val 46 | W.loadingWidget' val blank (const blank) $ \valDyn -> 47 | dyn_ $ 48 | ffor valDyn $ \v -> do 49 | renderManifest $ R.siteDataManifest (R.routeSiteData v r) 50 | renderStructuredData routeCfg r v 51 | elAttr "style" ("type" =: "text/css") $ do 52 | text $ R.siteDataBodyCss (R.routeSiteData v r) 53 | renderHeadHtml $ R.siteDataHeadHtml (R.routeSiteData v r) 54 | pure () 55 | el "body" $ do 56 | () <- case r of 57 | Route_Impulse {} -> do 58 | R.runNeuronWeb routeCfg $ 59 | V.renderRouteImpulse val 60 | Route_Zettel {} -> do 61 | R.runNeuronWeb routeCfg $ 62 | V.renderRouteZettel val 63 | pure () 64 | -------------------------------------------------------------------------------- /src/Neuron/Frontend/Static/StructuredData.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GADTs #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE NamedFieldPuns #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE ViewPatterns #-} 8 | {-# LANGUAGE NoImplicitPrelude #-} 9 | 10 | module Neuron.Frontend.Static.StructuredData 11 | ( renderStructuredData, 12 | ) 13 | where 14 | 15 | import qualified Data.Dependent.Map as DMap 16 | import Data.Some (Some (..)) 17 | import Data.Structured.OpenGraph 18 | ( Article (Article), 19 | OGType (..), 20 | OpenGraph (..), 21 | ) 22 | import Data.Structured.OpenGraph.Render (renderOpenGraph) 23 | import Data.TagTree (unTag) 24 | import qualified Data.Text as T 25 | import Neuron.Frontend.Route (Route (..)) 26 | import qualified Neuron.Frontend.Route as R 27 | import qualified Neuron.Frontend.Route.Data.Types as R 28 | import qualified Neuron.Plugin as Plugin 29 | import qualified Neuron.Plugin.Plugins.Tags as Tags 30 | import Neuron.Zettelkasten.ID (unZettelID) 31 | import Neuron.Zettelkasten.Zettel 32 | ( Zettel, 33 | ZettelT (..), 34 | sansContent, 35 | ) 36 | import Reflex.Dom.Core 37 | import Relude 38 | import Text.Pandoc.Definition (Inline (Image), Pandoc (..)) 39 | import Text.Pandoc.Util (getFirstParagraphText, plainify) 40 | import qualified Text.Pandoc.Walk as W 41 | import qualified Text.URI as URI 42 | 43 | renderStructuredData :: DomBuilder t m => R.RouteConfig t m -> Route a -> a -> m () 44 | renderStructuredData routeCfg route val = do 45 | renderOpenGraph $ routeOpenGraph routeCfg val route 46 | case route of 47 | R.Route_Zettel zslug -> do 48 | let z :: Zettel = sansContent $ R.zettelDataZettel $ snd val 49 | zid = zettelID z 50 | tags = Tags.getZettelTags z 51 | elAttr "meta" ("property" =: "neuron:zettel-id" <> "content" =: unZettelID zid) blank 52 | elAttr "meta" ("property" =: "neuron:zettel-slug" <> "content" =: zslug) blank 53 | forM_ tags $ \(unTag -> s) -> 54 | elAttr "meta" ("property" =: "neuron:zettel-tag" <> "content" =: s) blank 55 | forM_ (DMap.toList (R.zettelDataPlugin (snd val))) $ 56 | Plugin.renderZettelHead routeCfg val 57 | _ -> blank 58 | 59 | routeOpenGraph :: R.RouteConfig t m -> a -> Route a -> OpenGraph 60 | routeOpenGraph routeCfg v r = 61 | OpenGraph 62 | { _openGraph_title = R.routeTitle' v r, 63 | _openGraph_siteName = R.siteDataSiteTitle (R.routeSiteData v r), 64 | _openGraph_description = case r of 65 | Route_Impulse -> Just "Impulse" 66 | Route_Zettel _slug -> do 67 | let zData = snd v 68 | doc <- getPandocDoc $ R.zettelDataZettel zData 69 | para <- getFirstParagraphText doc 70 | let paraText = plainify para 71 | pure $ T.take 300 paraText, 72 | _openGraph_author = R.siteDataSiteAuthor (R.routeSiteData v r), 73 | _openGraph_type = case r of 74 | Route_Zettel _ -> Just $ OGType_Article (Article Nothing Nothing Nothing Nothing mempty) 75 | _ -> Just OGType_Website, 76 | _openGraph_image = case r of 77 | Route_Zettel _ -> do 78 | doc <- getPandocDoc (R.zettelDataZettel $ snd v) 79 | image <- URI.mkURI =<< getFirstImg doc 80 | baseUrl <- R.siteDataSiteBaseUrl (fst v) 81 | URI.relativeTo image baseUrl 82 | _ -> Nothing, 83 | _openGraph_url = do 84 | baseUrl <- R.siteDataSiteBaseUrl (R.routeSiteData v r) 85 | let relUrl = R.routeConfigRouteURL routeCfg (Some r) 86 | pure $ R.routeUri baseUrl relUrl 87 | } 88 | where 89 | getPandocDoc = either (const Nothing) (Just . zettelContent) 90 | getFirstImg :: 91 | Pandoc -> 92 | -- | Relative URL path to the image 93 | Maybe Text 94 | getFirstImg (Pandoc _ bs) = listToMaybe $ 95 | flip W.query bs $ \case 96 | Image _ _ (url, _) -> [toText url] 97 | _ -> [] 98 | -------------------------------------------------------------------------------- /src/Neuron/Frontend/Theme.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE TypeApplications #-} 4 | {-# LANGUAGE NoImplicitPrelude #-} 5 | 6 | -- | HTML & CSS 7 | module Neuron.Frontend.Theme 8 | ( Theme (..), 9 | mkTheme, 10 | themeCss, 11 | semanticColor, 12 | textBackgroundColor, 13 | textColor, 14 | titleH1Id, 15 | ) 16 | where 17 | 18 | import Clay (Css, rgb, rgba, (?)) 19 | import qualified Clay as C 20 | import Data.Text (toLower) 21 | import Relude 22 | 23 | -- | Neuron color theme 24 | -- 25 | -- Each theme corresponds to the color supported by Semantic UI 26 | -- https://semantic-ui.com/usage/theming.html#sitewide-defaults 27 | data Theme 28 | = Teal 29 | | Brown 30 | | Red 31 | | Orange 32 | | Yellow 33 | | Olive 34 | | Green 35 | | Blue 36 | | Violet 37 | | Purple 38 | | Pink 39 | | Grey 40 | | Black 41 | deriving (Eq, Show, Enum, Bounded) 42 | 43 | -- | Make Theme from Semantic UI color name 44 | mkTheme :: Text -> Theme 45 | mkTheme s = 46 | fromMaybe (error $ "Unsupported theme: " <> s) $ 47 | listToMaybe $ 48 | catMaybes $ 49 | flip fmap [minBound .. maxBound] $ 50 | \theme -> 51 | if s == semanticColor theme 52 | then Just theme 53 | else Nothing 54 | 55 | -- | Convert Theme to Semantic UI color name 56 | semanticColor :: Theme -> Text 57 | semanticColor = toLower . show @Text 58 | 59 | -- | Theme-specific color for notable text 60 | textColor :: Theme -> C.Color 61 | textColor theme = withRgb theme rgb 62 | 63 | -- | Theme-specific background color to use on some text 64 | textBackgroundColor :: Theme -> C.Color 65 | textBackgroundColor theme = withRgb theme rgba 0.1 66 | 67 | titleH1Id :: Text 68 | titleH1Id = "title-h1" 69 | 70 | themeCss :: Theme -> Css 71 | themeCss theme = do 72 | let backgroundColorLighter = withRgb theme rgba 0.02 73 | -- Zettel title's background color 74 | (fromString . toString $ ".zettel-content h1#" <> titleH1Id) ? do 75 | C.backgroundColor (textBackgroundColor theme) 76 | -- Bottom stuff 77 | "nav.bottomPane" ? do 78 | C.backgroundColor backgroundColorLighter 79 | -- Zettel footnote's top marging line 80 | "div#footnotes" ? do 81 | C.borderTopColor (textColor theme) 82 | 83 | withRgb :: Theme -> (Integer -> Integer -> Integer -> a) -> a 84 | withRgb theme f = 85 | case theme of 86 | Teal -> 87 | f 0 181 173 88 | Brown -> 89 | f 165 103 63 90 | Red -> 91 | f 219 40 40 92 | Orange -> 93 | f 242 113 28 94 | Yellow -> 95 | f 251 189 8 96 | Olive -> 97 | f 181 204 24 98 | Green -> 99 | f 33 186 69 100 | Blue -> 101 | f 33 133 208 102 | Violet -> 103 | f 100 53 201 104 | Purple -> 105 | f 163 51 200 106 | Pink -> 107 | f 224 57 151 108 | Grey -> 109 | f 118 118 118 110 | Black -> 111 | f 27 28 29 112 | -------------------------------------------------------------------------------- /src/Neuron/LSP.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE ScopedTypeVariables #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | {-# LANGUAGE ViewPatterns #-} 6 | {-# LANGUAGE NoImplicitPrelude #-} 7 | 8 | -- TODO: Just a stub for now. 9 | -- See https://github.com/srid/neuron/issues/213 10 | module Neuron.LSP where 11 | 12 | import Language.LSP.Server 13 | import Language.LSP.Types 14 | import Neuron.CLI.Types (App, MonadApp (getNotesDir)) 15 | import Relude 16 | 17 | handlers :: FilePath -> Handlers (LspM ()) 18 | handlers notesDir = 19 | mconcat 20 | [ notificationHandler SInitialized $ \_not -> do 21 | let params = 22 | ShowMessageRequestParams 23 | MtInfo 24 | "Turn on code lenses?" 25 | (Just [MessageActionItem "Turn on", MessageActionItem "Don't"]) 26 | _ <- sendRequest SWindowShowMessageRequest params $ \case 27 | Right (Just (MessageActionItem "Turn on")) -> do 28 | let regOpts = CodeLensRegistrationOptions Nothing Nothing (Just False) 29 | 30 | _ <- registerCapability STextDocumentCodeLens regOpts $ \_req responder -> do 31 | let cmd' = Command "Say hello" "lsp-hello-command" Nothing 32 | rsp = List [CodeLens (mkRange 0 0 0 100) (Just cmd') Nothing] 33 | responder (Right rsp) 34 | pure () 35 | Right _ -> 36 | sendNotification SWindowShowMessage (ShowMessageParams MtInfo "Not turning on code lenses") 37 | Left err -> 38 | sendNotification SWindowShowMessage (ShowMessageParams MtError $ "Something went wrong!\n" <> show err) 39 | pure (), 40 | requestHandler STextDocumentHover $ \req responder -> do 41 | let RequestMessage _ _ _ (HoverParams _doc pos _workDone) = req 42 | Position _l _c' = pos 43 | rsp = Hover ms (Just range) 44 | ms = HoverContents $ markedUpContent "markdown" ("**Notebook**:\n\n`" <> toText notesDir <> "`") 45 | range = Range pos pos 46 | responder (Right $ Just rsp) 47 | ] 48 | 49 | lspServer :: App () 50 | lspServer = do 51 | notesDir <- getNotesDir 52 | void $ 53 | liftIO $ 54 | runServer $ 55 | ServerDefinition 56 | { onConfigurationChange = const $ pure $ Right (), 57 | doInitialize = \env _req -> pure $ Right env, 58 | staticHandlers = handlers notesDir, 59 | interpretHandler = \env -> Iso (runLspT env) liftIO, 60 | options = defaultOptions 61 | } 62 | -------------------------------------------------------------------------------- /src/Neuron/Plugin/Plugins/NeuronIgnore.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RankNTypes #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE TupleSections #-} 7 | {-# LANGUAGE TypeApplications #-} 8 | {-# LANGUAGE ViewPatterns #-} 9 | {-# LANGUAGE NoImplicitPrelude #-} 10 | 11 | module Neuron.Plugin.Plugins.NeuronIgnore 12 | ( plugin, 13 | shouldIgnore, 14 | mandatoryIgnorePats, 15 | ) 16 | where 17 | 18 | import Data.Default (Default (def)) 19 | import qualified Data.Text as T 20 | import Neuron.Plugin.Type (Plugin (..)) 21 | import Reflex.Dom.Core (fforMaybe) 22 | import Relude hiding (trace, traceShow, traceShowId) 23 | import qualified System.Directory.Contents as DC 24 | import System.FilePattern (FilePattern, (?==)) 25 | 26 | plugin :: Plugin () 27 | plugin = 28 | def 29 | { _plugin_filterSources = applyNeuronIgnore 30 | } 31 | 32 | -- | Ignore files based on the top-level .neuronignore file. If the file does 33 | -- not exist, apply the default patterns. 34 | applyNeuronIgnore :: DC.DirTree FilePath -> IO (Maybe (DC.DirTree FilePath)) 35 | applyNeuronIgnore t = do 36 | -- Note that filterDirTree invokes the function only files, not directory paths 37 | -- FIXME(performance): `filterADirTree` unfortunately won't filter directories; so 38 | -- even if a top-level directory is configured to be ignored, this 39 | -- filter will traverse that entire directory tree to apply the glob 40 | -- pattern filter. 41 | ignorePats :: [FilePattern] <- fmap (mandatoryIgnorePats <>) $ case DC.walkDirTree "./.neuronignore" t of 42 | Just (DC.DirTree_File _ fp) -> do 43 | ls <- T.lines <$> readFileText fp 44 | pure $ 45 | fforMaybe ls $ \(T.strip -> s) -> do 46 | guard $ not $ T.null s 47 | -- Ignore line comments 48 | guard $ not $ "#" `T.isPrefixOf` s 49 | pure $ toString s 50 | _ -> 51 | pure defaultIgnorePats 52 | let mTreeFiltered = DC.filterDirTree (includeDirEntry ignorePats) t 53 | pure $ DC.pruneDirTree =<< mTreeFiltered 54 | where 55 | defaultIgnorePats = 56 | [ -- Ignore dotfiles and dotdirs 57 | "**/.*/**" 58 | -- Ignore everything under sub directories 59 | -- "*/*/**" 60 | ] 61 | includeDirEntry pats name = 62 | Just True 63 | == ( do 64 | guard $ not $ shouldIgnore pats name 65 | pure True 66 | ) 67 | 68 | shouldIgnore :: [FilePattern] -> FilePath -> Bool 69 | shouldIgnore (fmap ("./" <>) -> pats) fp = 70 | any (?== fp) pats 71 | 72 | mandatoryIgnorePats :: [FilePattern] 73 | mandatoryIgnorePats = 74 | [ "**/.neuron/**", 75 | "**/.git/**" 76 | ] 77 | -------------------------------------------------------------------------------- /src/Neuron/Plugin/Plugins/UpTree.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE TupleSections #-} 7 | {-# LANGUAGE TypeApplications #-} 8 | {-# LANGUAGE ViewPatterns #-} 9 | {-# LANGUAGE NoImplicitPrelude #-} 10 | 11 | module Neuron.Plugin.Plugins.UpTree (plugin, routePluginData, render, renderZettelHead) where 12 | 13 | import Data.Some 14 | import qualified Data.Structured.Breadcrumb as Breadcrumb 15 | import Data.Tree (Forest) 16 | import Neuron.Frontend.Route (NeuronWebT) 17 | import qualified Neuron.Frontend.Route as R 18 | import Neuron.Frontend.Route.Data.Types 19 | import qualified Neuron.Frontend.Route.Data.Types as R 20 | import qualified Neuron.Frontend.Widget.InvertedTree as IT 21 | import qualified Neuron.Plugin.Plugins.Links as Links 22 | import Neuron.Plugin.Type (Plugin (..)) 23 | import qualified Neuron.Zettelkasten.Graph as G 24 | import Neuron.Zettelkasten.Graph.Type (ZettelGraph) 25 | import Neuron.Zettelkasten.Zettel 26 | import Reflex.Dom.Core 27 | import Relude hiding (trace, traceShow, traceShowId) 28 | 29 | plugin :: Plugin (Forest Zettel) 30 | plugin = 31 | def 32 | { _plugin_afterZettelParse = bimap enable enable, 33 | -- _plugin_routeData = routePluginData, 34 | _plugin_renderPanel = const render, 35 | _plugin_css = const IT.style 36 | } 37 | 38 | enable :: ZettelT c -> ZettelT c 39 | enable = 40 | setPluginData UpTree () 41 | 42 | routePluginData :: ZettelGraph -> ZettelC -> Forest Zettel 43 | routePluginData g (sansContent -> z) = 44 | G.uplinkForest z g 45 | 46 | render :: (DomBuilder t m, PostBuild t m) => Forest Zettel -> NeuronWebT t m () 47 | render upTree = do 48 | unless (null upTree) $ do 49 | IT.renderInvertedHeadlessTree "zettel-uptree" "deemphasized" upTree $ \z2 -> 50 | Links.renderZettelLink Nothing Nothing def z2 51 | 52 | -- Render breadcrumbs for uptree 53 | renderZettelHead :: DomBuilder t m => R.RouteConfig t m -> (SiteData, ZettelData) -> Forest Zettel -> m () 54 | renderZettelHead routeCfg v uptree = do 55 | Breadcrumb.renderBreadcrumbs $ case R.siteDataSiteBaseUrl (fst v) of 56 | Nothing -> 57 | -- No base url set in neuron.dhall; nothing to do. 58 | [] 59 | Just baseUrl -> 60 | let mkCrumb :: Zettel -> Breadcrumb.Item 61 | mkCrumb Zettel {..} = 62 | let zettelRelUrl = R.routeConfigRouteURL routeCfg (Some $ R.Route_Zettel zettelSlug) 63 | in Breadcrumb.Item zettelTitle (Just $ R.routeUri baseUrl zettelRelUrl) 64 | in Breadcrumb.fromForest $ fmap mkCrumb <$> uptree 65 | -------------------------------------------------------------------------------- /src/Neuron/Plugin/Type.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE FlexibleContexts #-} 3 | {-# LANGUAGE GADTs #-} 4 | {-# LANGUAGE RankNTypes #-} 5 | {-# LANGUAGE NoImplicitPrelude #-} 6 | 7 | module Neuron.Plugin.Type where 8 | 9 | import Clay (Css) 10 | import qualified Commonmark as CM 11 | import Control.Monad.Writer 12 | import Data.Default (Default (..)) 13 | import Data.Dependent.Map (DMap) 14 | import Neuron.Frontend.Route (NeuronWebT, Route) 15 | import Neuron.Frontend.Route.Data.Types 16 | import Neuron.Frontend.Theme (Theme) 17 | import Neuron.Markdown 18 | import Neuron.Zettelkasten.Connection (ContextualConnection) 19 | import Neuron.Zettelkasten.ID (Slug, ZettelID) 20 | import Neuron.Zettelkasten.Resolver (ZIDRef) 21 | import Neuron.Zettelkasten.Zettel 22 | import Reflex.Dom.Core (DomBuilder, PostBuild) 23 | import Reflex.Dom.Widget (blank) 24 | import Relude 25 | import qualified System.Directory.Contents.Types as DC 26 | import Text.Pandoc.Definition (Inline, Pandoc) 27 | 28 | data Plugin routeData = Plugin 29 | { -- | Markdown, custom parser 30 | _plugin_markdownSpec :: forall m il bl. NeuronSyntaxSpec m il bl => CM.SyntaxSpec m il bl, 31 | -- | Apply any filter on the source tree before beginning any processing 32 | _plugin_filterSources :: DC.DirTree FilePath -> IO (Maybe (DC.DirTree FilePath)), 33 | -- | Called after zettel files read into memory 34 | _plugin_afterZettelRead :: forall m. MonadState (Map ZettelID ZIDRef) m => DC.DirTree FilePath -> m (), 35 | -- | Called after zettel files are fully parsed 36 | _plugin_afterZettelParse :: ZettelC -> ZettelC, 37 | -- | Called before building the graph. Allows the plugin to create new connections on demand. 38 | _plugin_graphConnections :: 39 | forall m. 40 | ( -- Running queries requires the zettels list. 41 | MonadReader [Zettel] m, 42 | -- Track missing zettel links in writer 43 | MonadWriter [MissingZettel] m 44 | ) => 45 | Zettel -> 46 | m [(ContextualConnection, Zettel)], 47 | -- | Pre-compute all data required to render this plugin's view. Only at 48 | -- this stage, is the zettel graph made available. 49 | -- _plugin_routeData :: SiteData -> ZettelGraph -> ZettelC -> routeData, 50 | -- | Plugin-specific HTML rendering to do on the zettel pages. 51 | _plugin_renderPanel :: forall t m. (DomBuilder t m, PostBuild t m) => (Pandoc -> NeuronWebT t m ()) -> routeData -> NeuronWebT t m (), 52 | -- | CSS to inject 53 | _plugin_css :: Theme -> Css, 54 | -- | Hooks for rendering custom DOM elements; here, url links. 55 | _plugin_renderHandleLink :: forall t m. (DomBuilder t m, PostBuild t m) => routeData -> Text -> Maybe [Inline] -> Maybe (NeuronWebT t m ()), 56 | -- | Custom action during route write. Return True if a file was written. 57 | -- TODO: This is not used! 58 | _plugin_afterRouteWrite :: 59 | forall m. 60 | MonadIO m => 61 | (ZettelData -> Pandoc -> IO ByteString) -> 62 | DMap Route Identity -> 63 | Slug -> 64 | FeedData -> 65 | m (Either Text [(Text, FilePath, LText)]), 66 | -- | Strip data you don't want in JSON dumps 67 | _plugin_preJsonStrip :: Zettel -> Zettel 68 | } 69 | 70 | instance Default (Plugin a) where 71 | def = 72 | Plugin 73 | { _plugin_markdownSpec = mempty, 74 | _plugin_filterSources = pure . Just, 75 | _plugin_afterZettelRead = void . pure, 76 | _plugin_afterZettelParse = id, 77 | _plugin_graphConnections = const $ pure mempty, 78 | -- _plugin_routeData = def, 79 | _plugin_renderPanel = \_ _ -> blank, 80 | _plugin_css = mempty, 81 | _plugin_renderHandleLink = \_ _ _ -> Nothing, 82 | _plugin_afterRouteWrite = \_ _ _ _ -> pure (Right mempty), 83 | _plugin_preJsonStrip = id 84 | } 85 | -------------------------------------------------------------------------------- /src/Neuron/Version.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | {-# LANGUAGE NoImplicitPrelude #-} 4 | 5 | module Neuron.Version where 6 | 7 | import Data.Tagged (Tagged (..)) 8 | import Data.Version (showVersion) 9 | import Neuron.Frontend.Route.Data.Types (NeuronVersion) 10 | import Paths_neuron (version) 11 | import Relude (ToText (toText), ($)) 12 | 13 | -- | Neuron cabal library version 14 | neuronVersion :: NeuronVersion 15 | neuronVersion = Tagged $ toText $ showVersion version 16 | -------------------------------------------------------------------------------- /src/Neuron/Zettelkasten/Connection.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE NoImplicitPrelude #-} 5 | 6 | module Neuron.Zettelkasten.Connection where 7 | 8 | import Data.Aeson 9 | import Data.Default (Default (..)) 10 | import Relude hiding (show) 11 | import Text.Pandoc.Definition (Block) 12 | import Text.Read 13 | import Text.Show (Show (show)) 14 | 15 | -- | Represent the connection between zettels 16 | -- 17 | -- The order of constructors will reflect the order in backlinks panel (see 18 | -- Links plugin) 19 | data Connection 20 | = FolgezettelInverse 21 | | OrdinaryConnection 22 | | Folgezettel 23 | deriving (Eq, Ord, Enum, Bounded, Generic) 24 | 25 | instance FromJSON Connection where 26 | parseJSON = maybe (fail "Unknown connection") pure . readMaybe <=< parseJSON 27 | 28 | instance ToJSON Connection where 29 | toJSON = toJSON . show 30 | 31 | instance Default Connection where 32 | def = OrdinaryConnection 33 | 34 | instance Semigroup Connection where 35 | -- A folgezettel link trumps all other kinds in that zettel. 36 | FolgezettelInverse <> _ = FolgezettelInverse 37 | _ <> FolgezettelInverse = FolgezettelInverse 38 | Folgezettel <> _ = Folgezettel 39 | _ <> Folgezettel = Folgezettel 40 | OrdinaryConnection <> OrdinaryConnection = OrdinaryConnection 41 | 42 | instance Show Connection where 43 | show = \case 44 | Folgezettel -> "folge" 45 | FolgezettelInverse -> "folgeinv" 46 | OrdinaryConnection -> "cf" 47 | 48 | instance Read Connection where 49 | readsPrec _ s 50 | | s == show Folgezettel = [(Folgezettel, "")] 51 | | s == show FolgezettelInverse = [(FolgezettelInverse, "")] 52 | | s == show OrdinaryConnection = [(OrdinaryConnection, "")] 53 | | otherwise = [] 54 | 55 | -- | A connection with context represented by Pandoc blocks 56 | type ContextualConnection = (Connection, [Block]) 57 | -------------------------------------------------------------------------------- /src/Neuron/Zettelkasten/Graph/Type.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE TypeFamilies #-} 4 | {-# LANGUAGE NoImplicitPrelude #-} 5 | 6 | module Neuron.Zettelkasten.Graph.Type 7 | ( -- * Graph type 8 | ZettelGraph, 9 | ) 10 | where 11 | 12 | import Data.Graph.Labelled (LabelledGraph) 13 | import Neuron.Zettelkasten.Connection 14 | import Neuron.Zettelkasten.Zettel (Zettel) 15 | import Relude 16 | 17 | -- | The Zettelkasten graph 18 | -- 19 | -- Edges are labelled with `Connection`; Maybe is used to provide the 20 | -- `Algebra.Graph.Label.zero` value for the edge label, which is `Nothing` in 21 | -- our case, and is effectively the same as there not being an edge between 22 | -- those vertices. 23 | type ZettelGraph = LabelledGraph Zettel (Maybe ContextualConnection) 24 | -------------------------------------------------------------------------------- /src/Neuron/Zettelkasten/ID.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE GADTs #-} 4 | {-# LANGUAGE LambdaCase #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE TypeApplications #-} 8 | {-# LANGUAGE ViewPatterns #-} 9 | {-# LANGUAGE NoImplicitPrelude #-} 10 | 11 | module Neuron.Zettelkasten.ID 12 | ( ZettelID (..), 13 | InvalidID (..), 14 | Slug, 15 | indexZid, 16 | parseZettelID, 17 | allowedSpecialChars, 18 | idParser, 19 | idParser', 20 | getZettelID, 21 | zettelIDSourceFileName, 22 | ) 23 | where 24 | 25 | import Data.Aeson 26 | ( FromJSON (parseJSON), 27 | FromJSONKey (fromJSONKey), 28 | FromJSONKeyFunction (FromJSONKeyTextParser), 29 | ToJSON (toJSON), 30 | ToJSONKey (toJSONKey), 31 | ) 32 | import Data.Aeson.Types (toJSONKeyText) 33 | import qualified Data.Text.Normalize as UT 34 | import Relude hiding (traceShowId) 35 | import System.FilePath (splitExtension, takeFileName) 36 | import qualified Text.Megaparsec as M 37 | import qualified Text.Megaparsec.Char as M 38 | import Text.Megaparsec.Simple (Parser, parse) 39 | import qualified Text.Show 40 | 41 | type Slug = Text 42 | 43 | newtype ZettelID = ZettelID {unZettelID :: Text} 44 | deriving (Show, Ord, Eq, Generic) 45 | 46 | indexZid :: ZettelID 47 | indexZid = ZettelID "index" 48 | 49 | instance Show InvalidID where 50 | show (InvalidIDParseError s) = 51 | "Invalid Zettel ID: " <> toString s 52 | 53 | instance ToJSON ZettelID where 54 | toJSON = toJSON . unZettelID 55 | 56 | instance FromJSON ZettelID where 57 | parseJSON = fmap ZettelID . parseJSON 58 | 59 | instance ToJSONKey ZettelID where 60 | toJSONKey = toJSONKeyText unZettelID 61 | 62 | instance FromJSONKey ZettelID where 63 | fromJSONKey = FromJSONKeyTextParser $ \s -> 64 | case parseZettelID s of 65 | Right v -> pure v 66 | Left e -> fail $ show e 67 | 68 | zettelIDSourceFileName :: ZettelID -> FilePath 69 | zettelIDSourceFileName zid = 70 | toString (unZettelID zid <> ".md") 71 | 72 | --------- 73 | -- Parser 74 | --------- 75 | 76 | data InvalidID = InvalidIDParseError Text 77 | deriving (Eq, Generic, ToJSON, FromJSON) 78 | 79 | parseZettelID :: Text -> Either InvalidID ZettelID 80 | parseZettelID = 81 | first InvalidIDParseError . parse idParser "parseZettelID" 82 | 83 | -- | Characters, aside from alpha numeric characters, to allow in IDs 84 | allowedSpecialChars :: [Char] 85 | allowedSpecialChars = 86 | [ '_', 87 | '-', 88 | '.', 89 | -- Whitespace is essential for title IDs 90 | -- This gets replaced with underscope in ID slug 91 | ' ', 92 | -- Allow some puctuation letters that are common in note titles 93 | ',', 94 | ';', 95 | '(', 96 | ')', 97 | ':', 98 | '"', 99 | '\'', 100 | '@', 101 | '§' 102 | ] 103 | 104 | idParser :: Parser ZettelID 105 | idParser = idParser' allowedSpecialChars 106 | 107 | idParser' :: String -> Parser ZettelID 108 | idParser' cs = do 109 | s <- M.some $ M.alphaNumChar <|> M.choice (M.char <$> cs) 110 | pure $ ZettelID $ toText s 111 | 112 | -- | Parse the ZettelID if the given filepath is a Markdown zettel. 113 | getZettelID :: FilePath -> Maybe ZettelID 114 | getZettelID fp = do 115 | let ( -- Apply unicode normalization per https://github.com/srid/neuron/issues/611 116 | UT.normalize UT.NFC . toText -> 117 | fileName, 118 | ext 119 | ) = 120 | splitExtension $ takeFileName fp 121 | guard $ ".md" == toText ext 122 | rightToMaybe $ parseZettelID fileName 123 | -------------------------------------------------------------------------------- /src/Neuron/Zettelkasten/ID/Scheme.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GADTs #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RankNTypes #-} 5 | {-# LANGUAGE TemplateHaskell #-} 6 | {-# LANGUAGE NoImplicitPrelude #-} 7 | 8 | -- TODO: Simplify or eliminate this module, now that date IDs are gone. 9 | module Neuron.Zettelkasten.ID.Scheme 10 | ( nextAvailableZettelID, 11 | genVal, 12 | IDScheme (..), 13 | IDConflict (..), 14 | ) 15 | where 16 | 17 | import Control.Monad.Except 18 | import Data.GADT.Compare.TH 19 | import Data.GADT.Show.TH 20 | import qualified Data.Set as Set 21 | import qualified Data.Text as T 22 | import Data.UUID (UUID) 23 | import qualified Data.UUID as UUID 24 | import Data.UUID.V4 (nextRandom) 25 | import Neuron.Zettelkasten.ID 26 | import Relude 27 | import Text.Megaparsec.Simple 28 | import Text.Show 29 | 30 | -- | The scheme to use when generating new IDs 31 | data IDScheme a where 32 | -- | Random IDs (default) 33 | IDSchemeHash :: IDScheme UUID 34 | -- | Custom ID (specified by the user) 35 | IDSchemeCustom :: Text -> IDScheme () 36 | 37 | data IDConflict 38 | = IDConflict_AlreadyExists 39 | | IDConflict_HashConflict Text 40 | | IDConflict_BadCustomID Text Text 41 | deriving (Eq) 42 | 43 | instance Show IDConflict where 44 | show = \case 45 | IDConflict_AlreadyExists -> 46 | "A zettel with that ID already exists" 47 | IDConflict_HashConflict s -> 48 | "Hash conflict on " <> toString s <> "; try again" 49 | IDConflict_BadCustomID s e -> 50 | "The custom ID " <> toString s <> " is malformed: " <> toString e 51 | 52 | -- | Produce a value that is required ahead to run an ID scheme. 53 | genVal :: forall a. IDScheme a -> IO a 54 | genVal = \case 55 | IDSchemeHash -> 56 | nextRandom 57 | IDSchemeCustom _ -> 58 | pure () 59 | 60 | -- | Create a new zettel ID based on the given scheme 61 | -- 62 | -- This is a pure function, with all impure actions done in @genVal@ 63 | -- 64 | -- Ensures that new ID doesn't conflict with existing zettels. 65 | nextAvailableZettelID :: 66 | forall a. 67 | -- Existing zettels 68 | Set ZettelID -> 69 | -- Seed value for the scheme 70 | a -> 71 | -- Scheme to use when generating an ID 72 | IDScheme a -> 73 | Either IDConflict ZettelID 74 | nextAvailableZettelID zs val = \case 75 | IDSchemeHash -> do 76 | let s = T.take 8 $ UUID.toText val 77 | if s `Set.member` (unZettelID `Set.map` zs) 78 | then throwError $ IDConflict_HashConflict s 79 | else 80 | either (error . toText) pure $ 81 | parse idParser "" s 82 | IDSchemeCustom s -> runExcept $ do 83 | zid <- 84 | either (throwError . IDConflict_BadCustomID s) pure $ 85 | parse idParser "" s 86 | if zid `Set.member` zs 87 | then throwError IDConflict_AlreadyExists 88 | else pure zid 89 | 90 | deriveGEq ''IDScheme 91 | 92 | deriveGShow ''IDScheme 93 | -------------------------------------------------------------------------------- /src/Neuron/Zettelkasten/Query.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE GADTs #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE TupleSections #-} 8 | {-# LANGUAGE ViewPatterns #-} 9 | {-# LANGUAGE NoImplicitPrelude #-} 10 | 11 | -- | Queries to the Zettel store 12 | module Neuron.Zettelkasten.Query where 13 | 14 | import Data.Aeson (KeyValue ((.=)), ToJSON (toJSON), Value, object) 15 | import Data.Tagged 16 | import Neuron.Zettelkasten.Graph (backlinks, getZettel) 17 | import Neuron.Zettelkasten.Graph.Type (ZettelGraph) 18 | import Neuron.Zettelkasten.ID (ZettelID) 19 | import Neuron.Zettelkasten.Query.Graph (GraphQuery (..)) 20 | import Neuron.Zettelkasten.Zettel (MissingZettel) 21 | import Neuron.Zettelkasten.Zettel.Error (ZettelIssue) 22 | import Relude 23 | 24 | runGraphQuery :: ZettelGraph -> GraphQuery r -> Either MissingZettel r 25 | runGraphQuery g = \case 26 | GraphQuery_Id -> Right g 27 | GraphQuery_BacklinksOf conn zid -> 28 | case getZettel zid g of 29 | Nothing -> 30 | Left $ Tagged zid 31 | Just z -> 32 | Right $ backlinks (maybe isJust (const (== conn)) conn) z g 33 | 34 | graphQueryResultJson :: 35 | forall r. 36 | (ToJSON (GraphQuery r)) => 37 | GraphQuery r -> 38 | r -> 39 | Map ZettelID ZettelIssue -> 40 | Value 41 | graphQueryResultJson q r errors = 42 | toJSON $ 43 | object 44 | [ "query" .= toJSON q, 45 | "result" .= resultJson r, 46 | "errors" .= errors 47 | ] 48 | where 49 | resultJson :: r -> Value 50 | resultJson r' = case q of 51 | GraphQuery_Id -> 52 | toJSON r' 53 | GraphQuery_BacklinksOf _ _ -> 54 | toJSON r' 55 | -------------------------------------------------------------------------------- /src/Neuron/Zettelkasten/Query/Graph.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE GADTs #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE StandaloneDeriving #-} 6 | {-# LANGUAGE TemplateHaskell #-} 7 | {-# LANGUAGE UndecidableInstances #-} 8 | {-# LANGUAGE NoImplicitPrelude #-} 9 | 10 | -- TODO: Rename to GraphQuery or just Query 11 | module Neuron.Zettelkasten.Query.Graph where 12 | 13 | import Data.Aeson.GADT.TH (deriveJSONGADT) 14 | import Data.Dependent.Sum.Orphans () 15 | import Data.GADT.Compare.TH (DeriveGEQ (deriveGEq)) 16 | import Data.GADT.Show.TH (DeriveGShow (deriveGShow)) 17 | import Neuron.Zettelkasten.Connection 18 | import Neuron.Zettelkasten.Graph.Type (ZettelGraph) 19 | import Neuron.Zettelkasten.ID (ZettelID) 20 | import Neuron.Zettelkasten.Zettel (Zettel) 21 | import Relude 22 | 23 | -- | Like `GraphQuery` but focused on the relationship between zettels. 24 | data GraphQuery r where 25 | -- | Query the entire graph. 26 | GraphQuery_Id :: GraphQuery ZettelGraph 27 | -- | Query backlinks. 28 | GraphQuery_BacklinksOf :: 29 | Maybe Connection -> 30 | ZettelID -> 31 | GraphQuery [(ContextualConnection, Zettel)] 32 | 33 | deriveJSONGADT ''GraphQuery 34 | 35 | deriveGEq ''GraphQuery 36 | 37 | deriveGShow ''GraphQuery 38 | 39 | deriving instance Show (GraphQuery ZettelGraph) 40 | 41 | deriving instance Eq (GraphQuery ZettelGraph) 42 | -------------------------------------------------------------------------------- /src/Neuron/Zettelkasten/Resolver.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE ViewPatterns #-} 4 | {-# LANGUAGE NoImplicitPrelude #-} 5 | 6 | -- | Transform a directory of Markdown files to Zettelkasten-strict zettel texts. 7 | -- 8 | -- This module is responsible only for "resolving" the filename IDs. It will 9 | -- return file contents as is (i.e., as text), without doing any parsing itself. 10 | -- It can also be used by plugins. 11 | module Neuron.Zettelkasten.Resolver where 12 | 13 | import Data.Dependent.Map (DMap) 14 | import qualified Data.Map.Strict as Map 15 | import Neuron.Zettelkasten.ID (ZettelID, getZettelID) 16 | import Neuron.Zettelkasten.Zettel (PluginZettelData) 17 | import Relude hiding (traceShowId) 18 | import qualified System.Directory.Contents.Types as DC 19 | 20 | -- | What does a Zettel ID refer to? 21 | data ZIDRef 22 | = -- | The ZID maps to a file on disk with the given contents 23 | ZIDRef_Available FilePath !Text (DMap PluginZettelData Identity) 24 | | -- | The ZID maps to more than one file, hence ambiguous. 25 | ZIDRef_Ambiguous (NonEmpty FilePath) 26 | deriving (Eq, Show) 27 | 28 | resolveZidRefsFromDirTree :: 29 | Monad m => 30 | (FilePath -> m Text) -> 31 | DC.DirTree FilePath -> 32 | StateT (Map ZettelID ZIDRef) m () 33 | resolveZidRefsFromDirTree readFileF = \case 34 | DC.DirTree_File relPath _ -> do 35 | whenJust (getZettelID relPath) $ \zid -> do 36 | addZettel relPath zid mempty $ 37 | lift $ readFileF relPath 38 | DC.DirTree_Dir _absPath contents -> do 39 | forM_ (Map.toList contents) $ \(_, ct) -> 40 | resolveZidRefsFromDirTree readFileF ct 41 | _ -> 42 | -- We ignore symlinks, and paths configured to be excluded. 43 | pure () 44 | 45 | addZettel :: MonadState (Map ZettelID ZIDRef) m => FilePath -> ZettelID -> DMap PluginZettelData Identity -> m Text -> m () 46 | addZettel zpath zid pluginData ms = do 47 | gets (Map.lookup zid) >>= \case 48 | Just (ZIDRef_Available oldPath _s _m) -> do 49 | -- The zettel ID is already used by `oldPath`. Mark it as a dup. 50 | modify $ Map.insert zid (ZIDRef_Ambiguous $ zpath :| [oldPath]) 51 | Just (ZIDRef_Ambiguous (toList -> ambiguities)) -> do 52 | -- Third or later duplicate file with the same Zettel ID 53 | markAmbiguous zid $ zpath :| ambiguities 54 | Nothing -> do 55 | s <- ms 56 | modify $ Map.insert zid (ZIDRef_Available zpath s pluginData) 57 | 58 | markAmbiguous :: (MonadState (Map ZettelID ZIDRef) m) => ZettelID -> NonEmpty FilePath -> m () 59 | markAmbiguous zid fs = 60 | modify $ Map.insert zid (ZIDRef_Ambiguous fs) 61 | -------------------------------------------------------------------------------- /src/Neuron/Zettelkasten/Zettel/Error.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE TupleSections #-} 7 | {-# LANGUAGE ViewPatterns #-} 8 | {-# LANGUAGE NoImplicitPrelude #-} 9 | 10 | module Neuron.Zettelkasten.Zettel.Error where 11 | 12 | import Data.Aeson 13 | import qualified Data.Map.Strict as Map 14 | import Data.Tagged (untag) 15 | import qualified Data.Text as T 16 | import Neuron.Markdown (ZettelParseError) 17 | import Neuron.Zettelkasten.ID (Slug, ZettelID) 18 | import Neuron.Zettelkasten.Zettel (MissingZettel) 19 | import Relude 20 | 21 | -- | All possible errors for a given zettel ID 22 | -- 23 | -- The first two constructors correspond to pre-resolved state, the later two to 24 | -- post-resolved zettels (either unparsed, or parsed but with bad queries) 25 | data ZettelIssue 26 | = ZettelIssue_Error ZettelError 27 | | ZettelIssue_MissingLinks 28 | ( -- Slug of the zettel which has 1 or more missing wiki-links 29 | Slug, 30 | -- List of missing wiki-links 31 | NonEmpty MissingZettel 32 | ) 33 | deriving (Eq, Show, Generic, ToJSON, FromJSON) 34 | 35 | data ZettelError 36 | = -- | A zettel ID may refer one of several zettel files 37 | ZettelError_AmbiguousID (NonEmpty FilePath) 38 | | -- | A slug is shared more than one zettel file 39 | ZettelError_AmbiguousSlug Slug 40 | | -- | The zettel file content is malformed 41 | ZettelError_ParseError (Slug, ZettelParseError) 42 | deriving (Eq, Show, Generic, ToJSON, FromJSON) 43 | 44 | splitZettelIssues :: 45 | Map ZettelID ZettelIssue -> 46 | ([(ZettelID, ZettelError)], [(ZettelID, (Slug, NonEmpty MissingZettel))]) 47 | splitZettelIssues m = 48 | lefts &&& rights $ 49 | flip fmap (Map.toList m) $ \(zid, issue) -> 50 | case issue of 51 | ZettelIssue_Error err -> Left (zid, err) 52 | ZettelIssue_MissingLinks x -> Right (zid, x) 53 | 54 | zettelErrorText :: ZettelError -> Text 55 | zettelErrorText = \case 56 | ZettelError_AmbiguousID filePaths -> 57 | "Multiple zettels have the same ID: " <> T.intercalate ", " (toText <$> toList filePaths) 58 | ZettelError_AmbiguousSlug slug -> 59 | "Slug '" <> slug <> "' is already used by another zettel" 60 | ZettelError_ParseError (untag . snd -> parseErr) -> 61 | parseErr 62 | -------------------------------------------------------------------------------- /src/Neuron/Zettelkasten/Zettel/Parser.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveAnyClass #-} 2 | {-# LANGUAGE DeriveGeneric #-} 3 | {-# LANGUAGE FlexibleContexts #-} 4 | {-# LANGUAGE FlexibleInstances #-} 5 | {-# LANGUAGE GADTs #-} 6 | {-# LANGUAGE LambdaCase #-} 7 | {-# LANGUAGE OverloadedStrings #-} 8 | {-# LANGUAGE ScopedTypeVariables #-} 9 | {-# LANGUAGE TupleSections #-} 10 | {-# LANGUAGE TypeApplications #-} 11 | {-# LANGUAGE ViewPatterns #-} 12 | {-# LANGUAGE NoImplicitPrelude #-} 13 | 14 | module Neuron.Zettelkasten.Zettel.Parser where 15 | 16 | import Data.Default (Default (def)) 17 | import Data.Dependent.Map (DMap) 18 | import qualified Data.Text as T 19 | import Data.YAML.ToJSON () 20 | import Neuron.Frontend.Theme (titleH1Id) 21 | import Neuron.Markdown (ZettelParser, lookupZettelMeta) 22 | import Neuron.Zettelkasten.ID (Slug, ZettelID (unZettelID)) 23 | import Neuron.Zettelkasten.Zettel 24 | import Relude 25 | import qualified Text.Pandoc.Util as P 26 | 27 | parseZettel :: 28 | ZettelParser -> 29 | FilePath -> 30 | ZettelID -> 31 | Text -> 32 | DMap PluginZettelData Identity -> 33 | ZettelC 34 | parseZettel parser fn zid s pluginData = 35 | either unparseableZettel id $ do 36 | (metadata, doc) <- parser fn s 37 | let (tit, doc') = consolidateTitle metadata doc 38 | -- Compute final values from user (and post-plugin) metadata 39 | slug = fromMaybe (mkDefaultSlug $ unZettelID zid) $ lookupZettelMeta "slug" metadata 40 | date = lookupZettelMeta "date" metadata 41 | pure $ Right $ Zettel zid metadata slug date fn tit doc' (Just pluginData) 42 | where 43 | unparseableZettel err = 44 | let slug = mkDefaultSlug $ unZettelID zid 45 | in Left $ Zettel zid def slug Nothing fn "Unknown" (s, err) (Just pluginData) 46 | -- We keep the default slug as close to zettel ID is possible. Spaces (and 47 | -- colons) are replaced with underscore for legibility. 48 | mkDefaultSlug :: Text -> Slug 49 | mkDefaultSlug ss = 50 | foldl' (\s' x -> T.replace x "_" s') ss (charsDisallowedInURL <> [" "]) 51 | charsDisallowedInURL :: [Text] 52 | charsDisallowedInURL = 53 | [":"] 54 | -- Determine the effective title of the zettel, ensuring that the same exists in the document. 55 | consolidateTitle metadata doc = 56 | case lookupZettelMeta "title" metadata of 57 | Just tit -> 58 | (tit, P.setTitleH1 titleH1Id tit doc) 59 | Nothing -> 60 | P.ensureTitleH1 titleH1Id (unZettelID zid) doc 61 | 62 | -- | Like `parseZettel` but operates on multiple files. 63 | parseZettels :: 64 | ZettelParser -> 65 | [(ZettelID, (FilePath, (Text, DMap PluginZettelData Identity)))] -> 66 | [ZettelC] 67 | parseZettels p files = 68 | flip fmap files $ \(zid, (path, (s, pluginData))) -> 69 | parseZettel p path zid s pluginData 70 | -------------------------------------------------------------------------------- /src/Options/Applicative/Extra.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ApplicativeDo #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | {-# LANGUAGE ViewPatterns #-} 7 | {-# LANGUAGE NoImplicitPrelude #-} 8 | 9 | module Options.Applicative.Extra where 10 | 11 | import Options.Applicative 12 | import Relude 13 | import System.FilePath (addTrailingPathSeparator) 14 | import qualified Text.Megaparsec as M 15 | import qualified Text.Megaparsec.Char as M 16 | 17 | hostPortOption :: Parser (Maybe (Text, Int)) 18 | hostPortOption = 19 | optional 20 | ( option 21 | (megaparsecReader hostPortParser) 22 | ( long "serve" 23 | <> short 's' 24 | <> metavar "[HOST]:PORT" 25 | <> help "Run a HTTP server on the generated directory" 26 | ) 27 | ) 28 | <|> fmap 29 | (bool Nothing $ Just (defaultHost, 8080)) 30 | ( switch (short 'S' <> help ("Like `-s " <> toString defaultHost <> ":8080`")) 31 | ) 32 | where 33 | hostPortParser :: M.Parsec Void Text (Text, Int) 34 | hostPortParser = do 35 | host <- 36 | optional $ 37 | M.string "localhost" 38 | <|> M.try parseIP 39 | void $ M.char ':' 40 | port <- parseNumRange 1 65535 41 | pure (fromMaybe defaultHost host, port) 42 | where 43 | readNum = maybe (fail "Not a number") pure . readMaybe 44 | parseIP :: M.Parsec Void Text Text 45 | parseIP = do 46 | a <- parseNumRange 0 255 <* M.char '.' 47 | b <- parseNumRange 0 255 <* M.char '.' 48 | c <- parseNumRange 0 255 <* M.char '.' 49 | d <- parseNumRange 0 255 50 | pure $ toText $ intercalate "." $ show <$> [a, b, c, d] 51 | parseNumRange :: Int -> Int -> M.Parsec Void Text Int 52 | parseNumRange a b = do 53 | n <- readNum =<< M.some M.digitChar 54 | if a <= n && n <= b 55 | then pure n 56 | else fail $ "Number not in range: " <> show a <> "-" <> show b 57 | 58 | defaultHost :: Text 59 | defaultHost = "127.0.0.1" 60 | 61 | megaparsecReader :: M.Parsec Void Text a -> ReadM a 62 | megaparsecReader p = 63 | eitherReader (first M.errorBundlePretty . M.parse p "" . toText) 64 | 65 | -- | Like `str` but adds a trailing slash if there isn't one. 66 | directoryReader :: ReadM FilePath 67 | directoryReader = fmap addTrailingPathSeparator str 68 | -------------------------------------------------------------------------------- /src/System/Directory/Contents/Extra.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE GADTs #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE ScopedTypeVariables #-} 7 | {-# LANGUAGE TupleSections #-} 8 | {-# LANGUAGE TypeApplications #-} 9 | {-# LANGUAGE ViewPatterns #-} 10 | {-# LANGUAGE NoImplicitPrelude #-} 11 | 12 | module System.Directory.Contents.Extra where 13 | 14 | import Colog (WithLog, log) 15 | import qualified Data.Map.Strict as Map 16 | import Neuron.CLI.Logging 17 | import Relude 18 | import System.Directory (copyFile, createDirectoryIfMissing) 19 | import qualified System.Directory.Contents as DC 20 | import System.FilePath (()) 21 | import System.Posix (fileExist, getFileStatus, modificationTime) 22 | 23 | -- | Copy the given directory tree from @src@ as base directory, to @dest@ 24 | rsyncDir :: (MonadIO m, WithLog env Message m) => FilePath -> FilePath -> DC.DirTree FilePath -> m Int 25 | rsyncDir src dest = \case 26 | DC.DirTree_File fp _ -> do 27 | let (a, b) = (src ) &&& (dest ) $ fp 28 | aT <- liftIO $ modificationTime <$> getFileStatus a 29 | -- TODO: if a file gets deleted, we must remove it. 30 | mBT <- liftIO $ do 31 | fileExist b >>= \case 32 | True -> do 33 | bT <- modificationTime <$> getFileStatus b 34 | pure $ Just bT 35 | False -> 36 | pure Nothing 37 | if maybe True (aT >) mBT 38 | then do 39 | log I $ toText $ "+ " <> fp 40 | liftIO $ copyFile a b 41 | pure 1 42 | else pure 0 43 | DC.DirTree_Symlink {} -> 44 | pure 0 45 | DC.DirTree_Dir dp children -> do 46 | liftIO $ createDirectoryIfMissing False (dest dp) 47 | fmap sum $ 48 | forM (Map.elems children) $ \childTree -> do 49 | rsyncDir src dest childTree 50 | -------------------------------------------------------------------------------- /src/Text/Megaparsec/Simple.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ScopedTypeVariables #-} 2 | {-# LANGUAGE NoImplicitPrelude #-} 3 | 4 | -- | A simple API for megaparsec 5 | module Text.Megaparsec.Simple 6 | ( Parser, 7 | parse, 8 | ) 9 | where 10 | 11 | import Relude 12 | import qualified Text.Megaparsec as M 13 | 14 | type Parser a = M.Parsec Void Text a 15 | 16 | parse :: Parser a -> String -> Text -> Either Text a 17 | parse p fn s = 18 | first (toText . M.errorBundlePretty) $ 19 | M.parse (p <* M.eof) fn s 20 | -------------------------------------------------------------------------------- /src/Text/Pandoc/Util.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE TupleSections #-} 6 | {-# LANGUAGE ViewPatterns #-} 7 | {-# LANGUAGE NoImplicitPrelude #-} 8 | 9 | -- TODO Review and delete what's unused in this module 10 | module Text.Pandoc.Util 11 | ( getFirstParagraphText, 12 | setTitleH1, 13 | ensureTitleH1, 14 | deleteTitleH1, 15 | plainify, 16 | PandocLink (..), 17 | mkPandocAutoLink, 18 | isAutoLink, 19 | getLinks, 20 | ) 21 | where 22 | 23 | import Reflex.Dom.Pandoc.Util (plainify) 24 | import Relude 25 | import qualified Text.Pandoc.Builder as B 26 | import Text.Pandoc.Definition (Pandoc (..)) 27 | import qualified Text.Pandoc.Walk as W 28 | import Text.URI (URI) 29 | 30 | data PandocLink = PandocLink 31 | { -- | This is set to Nothing for autolinks 32 | _pandocLink_inner :: Maybe [B.Inline], 33 | _pandocLink_uri :: URI 34 | } 35 | deriving (Eq, Show, Ord) 36 | 37 | mkPandocAutoLink :: URI -> PandocLink 38 | mkPandocAutoLink = PandocLink Nothing 39 | 40 | isAutoLink :: PandocLink -> Bool 41 | isAutoLink PandocLink {..} = 42 | isNothing _pandocLink_inner 43 | 44 | -- | Get all links that have a valid URI 45 | -- TODO: Move to pandoc-link-context 46 | getLinks :: W.Walkable B.Inline b => b -> [([(Text, Text)], Text)] 47 | getLinks = W.query go 48 | where 49 | go :: B.Inline -> [([(Text, Text)], Text)] 50 | go = maybeToList . uriLinkFromInline 51 | uriLinkFromInline :: B.Inline -> Maybe ([(Text, Text)], Text) 52 | uriLinkFromInline inline = do 53 | B.Link (_, _, attrs) _inlines (url, title) <- pure inline 54 | pure (("title", title) : attrs, url) 55 | 56 | getFirstParagraphText :: Pandoc -> Maybe [B.Inline] 57 | getFirstParagraphText = listToMaybe . W.query go 58 | where 59 | go :: B.Block -> [[B.Inline]] 60 | go = \case 61 | B.Para inlines -> 62 | [inlines] 63 | _ -> 64 | [] 65 | 66 | -- | Ensure that a title H1 is present in the Pandoc doc. 67 | -- 68 | -- A "title H1" is a H1 that appears as the very first block element of the AST. 69 | -- If this element is absent, then create it using the given text inline. 70 | -- 71 | -- Either way, apply the given `id` attribute to the title H1 (so as to 72 | -- distinguish it from other headings). And return the title plainify'ied. 73 | ensureTitleH1 :: Text -> Text -> Pandoc -> (Text, Pandoc) 74 | ensureTitleH1 idAttr defaultTitle = \case 75 | (Pandoc meta (B.Header 1 (_id, classes, kw) inlines : rest)) -> 76 | -- Add `idAttr` to existing H1 77 | let h1 = B.Header 1 (idAttr, classes, kw) inlines 78 | tit = plainify inlines 79 | in (tit,) $ Pandoc meta (h1 : rest) 80 | doc -> 81 | (defaultTitle,) $ setTitleH1 idAttr defaultTitle doc 82 | 83 | setTitleH1 :: Text -> Text -> Pandoc -> Pandoc 84 | setTitleH1 idAttr s (Pandoc meta rest) = 85 | let h1 = B.Header 1 (idAttr, mempty, mempty) [B.Str s] 86 | in Pandoc meta (h1 : rest) 87 | 88 | deleteTitleH1 :: Pandoc -> Pandoc 89 | deleteTitleH1 (Pandoc meta (B.Header 1 _ _ : rest)) = 90 | Pandoc meta rest 91 | deleteTitleH1 doc = 92 | doc 93 | -------------------------------------------------------------------------------- /src/Text/URI/Util.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | {-# LANGUAGE FlexibleContexts #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE TypeFamilies #-} 6 | {-# LANGUAGE ViewPatterns #-} 7 | {-# LANGUAGE NoImplicitPrelude #-} 8 | 9 | module Text.URI.Util where 10 | 11 | import Relude 12 | import qualified Text.URI as URI 13 | 14 | getQueryParam :: URI.RText 'URI.QueryKey -> URI.URI -> Maybe Text 15 | getQueryParam k uri = 16 | listToMaybe $ 17 | catMaybes $ 18 | flip fmap (URI.uriQuery uri) $ \case 19 | URI.QueryFlag _ -> Nothing 20 | URI.QueryParam key (URI.unRText -> val) -> 21 | if key == k 22 | then Just val 23 | else Nothing 24 | 25 | hasQueryFlag :: URI.RText 'URI.QueryKey -> URI.URI -> Bool 26 | hasQueryFlag k uri = 27 | Just True 28 | == listToMaybe 29 | ( catMaybes $ 30 | flip fmap (URI.uriQuery uri) $ \case 31 | URI.QueryFlag key -> 32 | if key == k 33 | then Just True 34 | else Nothing 35 | _ -> Nothing 36 | ) 37 | -------------------------------------------------------------------------------- /static.nix: -------------------------------------------------------------------------------- 1 | args@{ ... }: 2 | let 3 | nixpkgs = import ./nixpkgs.nix { }; 4 | nixpkgsStatic = import ./nixpkgs.nix { 5 | overlays = [ 6 | (self: super: { 7 | # https://github.com/NixOS/nixpkgs/issues/131557 8 | python3 = super.python3.override { enableLTO = false; }; 9 | }) 10 | ]; 11 | }; 12 | pkgs = nixpkgsStatic.pkgsMusl; 13 | in 14 | (import ./project.nix { 15 | inherit pkgs; 16 | # We have to use original nixpkgs for fzf, etc. otherwise this will give 17 | # error: missing bootstrap url for platform x86_64-unknown-linux-musl 18 | pkgsForBins = nixpkgs; 19 | neuronFlags = [ 20 | "--ghc-option=-optl=-static" 21 | # Disabling shared as workaround. But - https://github.com/nh2/static-haskell-nix/issues/99#issuecomment-665400600 22 | # TODO: Patch ghc bootstrap binary to use ncurses6, which might also obviate the nixpkgs revert. 23 | "--disable-shared" 24 | "--extra-lib-dirs=${pkgs.gmp6.override { withStatic = true; }}/lib" 25 | "--extra-lib-dirs=${pkgs.zlib.static}/lib" 26 | "--extra-lib-dirs=${pkgs.libffi.overrideAttrs (old: { dontDisableStatic = true; })}/lib" 27 | "--extra-lib-dirs=${pkgs.ncurses.override { enableStatic = true; }}/lib" 28 | ]; 29 | }).neuron 30 | 31 | -------------------------------------------------------------------------------- /test/Data/Graph/Labelled/AlgorithmSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GeneralisedNewtypeDeriving #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | {-# LANGUAGE TypeApplications #-} 4 | {-# LANGUAGE TypeFamilies #-} 5 | {-# LANGUAGE NoImplicitPrelude #-} 6 | 7 | module Data.Graph.Labelled.AlgorithmSpec 8 | ( spec, 9 | ) 10 | where 11 | 12 | import Data.Graph.Labelled 13 | import Relude 14 | import Test.Hspec 15 | 16 | newtype SimpleVertex = SimpleVertex String 17 | deriving (Show, Eq, Ord, IsString) 18 | 19 | instance Vertex SimpleVertex where 20 | type VertexID SimpleVertex = String 21 | vertexID (SimpleVertex v) = v 22 | 23 | spec :: Spec 24 | spec = do 25 | let mkG = mkGraphFrom @(Maybe ()) @SimpleVertex 26 | e = Just () 27 | describe "cluster" $ do 28 | it "detects 1-graph cluster" $ do 29 | clusters (mkG ["a"] []) `shouldBe` ["a" :| []] 30 | it "detects 2-graph clusters" $ do 31 | sort (clusters (mkG ["a", "b"] [])) 32 | `shouldBe` ["a" :| [], "b" :| []] 33 | sort (clusters (mkG ["a", "b"] [(e, "a", "b")])) 34 | `shouldBe` ["a" :| []] 35 | it "detects 2-cyclic-graph clusters" $ do 36 | sort (clusters (mkG ["a", "b"] [(e, "a", "b"), (e, "b", "a")])) 37 | -- Returns empty list on cyclic graphs 38 | `shouldBe` [] 39 | it "detects 3-cyclic-graph clusters" $ do 40 | sort (clusters (mkG ["a", "b", "c"] [(e, "a", "b"), (e, "b", "a")])) 41 | -- a,b are not included, because they have no mothers. 42 | `shouldBe` ["c" :| []] 43 | -------------------------------------------------------------------------------- /test/Data/PathTreeSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | 3 | module Data.PathTreeSpec 4 | ( spec, 5 | ) 6 | where 7 | 8 | import Data.PathTree 9 | import Data.Tree (Forest, Tree (..)) 10 | import Relude 11 | import System.FilePath (()) 12 | import Test.Hspec 13 | 14 | spec :: Spec 15 | spec = do 16 | describe "Path tree" $ do 17 | context "Tree building" $ do 18 | forM_ treeCases $ \(name, paths, tree) -> do 19 | it name $ do 20 | mkTreeFromPaths paths `shouldBe` tree 21 | context "Tree folding" $ do 22 | forM_ foldingCases $ \(name, tree, folded) -> do 23 | it name $ do 24 | let mergePaths (p, n) (p', b') = bool Nothing (Just (p p', b')) n 25 | res = fst <$> foldSingleParentsWith mergePaths tree 26 | res `shouldBe` folded 27 | 28 | treeCases :: [(String, [[String]], Forest String)] 29 | treeCases = 30 | [ ( "works on one level", 31 | [["journal"], ["science"]], 32 | [Node "journal" [], Node "science" []] 33 | ), 34 | ( "groups paths with common prefix", 35 | [["math", "algebra"], ["math", "calculus"]], 36 | [Node "math" [Node "algebra" [], Node "calculus" []]] 37 | ), 38 | ( "ignores tag when there is also tag/subtag", 39 | [["math"], ["math", "algebra"]], 40 | [Node "math" [Node "algebra" []]] 41 | ) 42 | ] 43 | 44 | foldingCases :: [(String, Tree (String, Bool), Tree String)] 45 | foldingCases = 46 | [ ( "folds tree on one level", 47 | Node ("math", True) [Node ("note", False) []], 48 | Node "math/note" [] 49 | ), 50 | ( "folds across multiple levels", 51 | Node ("math", True) [Node ("algebra", True) [Node ("note", False) []]], 52 | Node "math/algebra/note" [] 53 | ), 54 | ( "does not fold tree when the predicate is false", 55 | Node ("math", False) [Node ("note", False) []], 56 | Node "math" [Node "note" []] 57 | ) 58 | ] 59 | -------------------------------------------------------------------------------- /test/Data/TagTreeSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ViewPatterns #-} 3 | {-# LANGUAGE NoImplicitPrelude #-} 4 | 5 | module Data.TagTreeSpec 6 | ( spec, 7 | ) 8 | where 9 | 10 | import Data.TagTree 11 | import Relude 12 | import Test.Hspec 13 | 14 | spec :: Spec 15 | spec = do 16 | describe "Tag matching" $ do 17 | forM_ tagMatchCases $ \(name, mkTagPattern -> pat, fmap Tag -> matching, fmap Tag -> failing) -> do 18 | it name $ do 19 | forM_ matching $ \tag -> do 20 | pat `shouldMatch` tag 21 | forM_ failing $ \tag -> do 22 | pat `shouldNotMatch` tag 23 | 24 | tagMatchCases :: [(String, Text, [Text], [Text])] 25 | tagMatchCases = 26 | [ ( "simple tag", 27 | "journal", 28 | ["journal"], 29 | ["science", "journal/work"] 30 | ), 31 | ( "simple tag with slash", 32 | "journal/note", 33 | ["journal/note"], 34 | ["science/physics", "journal", "journal/note/foo"] 35 | ), 36 | ( "tag pattern with **", 37 | "journal/**", 38 | ["journal", "journal/work", "journal/work/clientA"], 39 | ["math", "science/physics", "jour"] 40 | ), 41 | ( "tag pattern with */**", 42 | "journal/*/**", 43 | ["journal/foo", "journal/foo/bar"], 44 | ["science", "journal"] 45 | ), 46 | ( "tag pattern with ** in the middle", 47 | "math/**/note", 48 | ["math/note", "math/algebra/note", "math/algebra/linear/note"], 49 | ["math/algebra", "journal/note"] 50 | ), 51 | ( "tag pattern with * in the middle", 52 | "project/*/task", 53 | ["project/foo/task", "project/bar-baz/task"], 54 | ["project", "project/foo", "project/task", "project/foo/bar/task"] 55 | ) 56 | ] 57 | 58 | shouldMatch :: TagPattern -> Tag -> Expectation 59 | shouldMatch pat tag 60 | | tagMatch pat tag = pure () 61 | | otherwise = 62 | expectationFailure $ 63 | unTagPattern pat <> " was expected to match " <> toString (unTag tag) <> " but didn't" 64 | 65 | shouldNotMatch :: TagPattern -> Tag -> Expectation 66 | shouldNotMatch pat tag 67 | | tagMatch pat tag = 68 | expectationFailure $ 69 | unTagPattern pat <> " wasn't expected to match tag " <> toString (unTag tag) <> " but it did" 70 | | otherwise = pure () 71 | -------------------------------------------------------------------------------- /test/Neuron/VersionSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE QuasiQuotes #-} 3 | {-# LANGUAGE NoImplicitPrelude #-} 4 | 5 | module Neuron.VersionSpec 6 | ( spec, 7 | ) 8 | where 9 | 10 | import Data.Tagged 11 | import qualified Data.Text as T 12 | import Neuron.Version (neuronVersion) 13 | import Relude 14 | import Test.Hspec 15 | 16 | spec :: Spec 17 | spec = do 18 | describe "Application version" $ do 19 | it "should have dots" $ do 20 | untag neuronVersion `shouldSatisfy` T.isInfixOf "." 21 | -------------------------------------------------------------------------------- /test/Neuron/Zettelkasten/ID/SchemeSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE NoImplicitPrelude #-} 3 | 4 | module Neuron.Zettelkasten.ID.SchemeSpec 5 | ( spec, 6 | ) 7 | where 8 | 9 | import qualified Data.Set as Set 10 | import Neuron.Zettelkasten.ID (ZettelID (ZettelID), parseZettelID) 11 | import Neuron.Zettelkasten.ID.Scheme 12 | ( IDConflict (IDConflict_AlreadyExists), 13 | IDScheme (IDSchemeCustom, IDSchemeHash), 14 | genVal, 15 | nextAvailableZettelID, 16 | ) 17 | import Relude 18 | import Test.Hspec 19 | 20 | spec :: Spec 21 | spec = do 22 | describe "nextAvailableZettelID" $ do 23 | let zettels = 24 | Set.fromList $ 25 | fmap 26 | (either (error . show) id . parseZettelID) 27 | [ "ribeye-steak", 28 | "2015403" 29 | ] 30 | nextAvail scheme = do 31 | v <- genVal scheme 32 | pure $ nextAvailableZettelID zettels v scheme 33 | context "custom ID" $ do 34 | it "checks if already exists" $ do 35 | nextAvail (IDSchemeCustom "ribeye-steak") 36 | `shouldReturn` Left IDConflict_AlreadyExists 37 | it "succeeds" $ do 38 | nextAvail (IDSchemeCustom "sunny-side-eggs") 39 | `shouldReturn` Right (ZettelID "sunny-side-eggs") 40 | context "hash ID" $ do 41 | it "should succeed" $ 42 | nextAvail IDSchemeHash 43 | >>= (`shouldNotSatisfy` isLeft) 44 | -------------------------------------------------------------------------------- /test/Neuron/Zettelkasten/IDSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE NoImplicitPrelude #-} 3 | 4 | module Neuron.Zettelkasten.IDSpec 5 | ( spec, 6 | ) 7 | where 8 | 9 | import qualified Data.Aeson as Aeson 10 | import qualified Neuron.Zettelkasten.ID as Z 11 | import Relude 12 | import Test.Hspec 13 | 14 | spec :: Spec 15 | spec = do 16 | describe "ID parsing" $ do 17 | context "custom id parsing" $ do 18 | let zid = Z.ZettelID "20abcde" 19 | it "parses a custom zettel ID" $ do 20 | Z.parseZettelID "20abcde" `shouldBe` Right zid 21 | it "parses a custom zettel ID from zettel filename" $ do 22 | Z.getZettelID "20abcde.md" `shouldBe` Just zid 23 | Z.zettelIDSourceFileName zid `shouldBe` "20abcde.md" 24 | let deceptiveZid = Z.ZettelID "2136537e" 25 | it "parses a custom zettel ID that looks like date ID" $ do 26 | Z.parseZettelID "2136537e" `shouldBe` Right deceptiveZid 27 | it "parses a custom zettel ID with dot" $ do 28 | Z.parseZettelID "foo.bar" `shouldBe` Right (Z.ZettelID "foo.bar") 29 | -- Even if there is a ".md" (not a file extension) 30 | Z.parseZettelID "foo.md" `shouldBe` Right (Z.ZettelID "foo.md") 31 | it "parses full-phrase IDs" $ do 32 | Z.parseZettelID "foo bar" `shouldBe` Right (Z.ZettelID "foo bar") 33 | context "i18n" $ do 34 | it "deals with unicode chars" $ do 35 | Z.parseZettelID "计算机" `shouldBe` Right (Z.ZettelID "计算机") 36 | context "failures" $ do 37 | it "fails to parse ID with disallowed characters" $ do 38 | Z.parseZettelID "/foo" `shouldSatisfy` isLeft 39 | Z.parseZettelID "foo$" `shouldSatisfy` isLeft 40 | describe "ID converstion" $ do 41 | context "JSON encoding" $ do 42 | it "Converts ID to text when encoding to JSON" $ do 43 | Aeson.toJSON (Z.ZettelID "20abcde") `shouldBe` Aeson.String "20abcde" 44 | -------------------------------------------------------------------------------- /test/Neuron/Zettelkasten/ZettelSpec.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | {-# LANGUAGE NoImplicitPrelude #-} 4 | 5 | module Neuron.Zettelkasten.ZettelSpec 6 | ( spec, 7 | ) 8 | where 9 | 10 | import Data.Default 11 | import Data.Time.Calendar (fromGregorian) 12 | import Data.Time.DateMayTime (DateMayTime, mkDateMayTime) 13 | import Data.Time.LocalTime 14 | ( LocalTime (LocalTime), 15 | TimeOfDay (TimeOfDay), 16 | ) 17 | import Neuron.Zettelkasten.ID (ZettelID (ZettelID)) 18 | import Neuron.Zettelkasten.Zettel 19 | ( MetadataOnly, 20 | Zettel, 21 | ZettelT (Zettel), 22 | sortZettelsReverseChronological, 23 | ) 24 | import Relude 25 | import Test.Hspec 26 | 27 | spec :: Spec 28 | spec = do 29 | let noContent :: MetadataOnly = Nothing 30 | describe "sortZettelsReverseChronological" $ do 31 | let mkDay = fromGregorian 2020 3 32 | mkZettelDay n = 33 | Just $ mkDateMayTime $ Left (mkDay n) 34 | mkZettelLocalTime day hh mm = 35 | Just $ mkDateMayTime $ Right $ LocalTime (mkDay day) (TimeOfDay hh mm 0) 36 | 37 | mkZettel :: Text -> Maybe DateMayTime -> Zettel 38 | mkZettel s datetime = 39 | Zettel 40 | (ZettelID s) 41 | def 42 | s 43 | datetime 44 | ".md" 45 | "Some title" 46 | -- (Set.fromList [Tag "science", Tag "journal/class"]) 47 | noContent 48 | mempty 49 | 50 | it "sorts correctly with day" $ do 51 | let zs = 52 | [ mkZettel "a" (mkZettelDay 3), 53 | mkZettel "b" (mkZettelDay 5) 54 | ] 55 | sortZettelsReverseChronological zs 56 | `shouldBe` reverse zs 57 | 58 | it "sorts correctly with localtime" $ do 59 | let zs = 60 | [ mkZettel "a" (mkZettelLocalTime 3 9 59), 61 | mkZettel "b" (mkZettelLocalTime 3 10 0) 62 | ] 63 | sortZettelsReverseChronological zs 64 | `shouldBe` reverse zs 65 | 66 | it "sorts correctly with mixed dates" $ do 67 | let zs = 68 | [ mkZettel "c" (mkZettelLocalTime 7 0 0), 69 | mkZettel "a" (mkZettelDay 5), 70 | mkZettel "b" (mkZettelLocalTime 3 0 0) 71 | ] 72 | sortZettelsReverseChronological zs 73 | `shouldBe` zs 74 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | {-# OPTIONS_GHC -F -pgmF hspec-discover #-} 2 | --------------------------------------------------------------------------------