├── .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 | [](https://en.wikipedia.org/wiki/Affero_General_Public_License)
6 | [](https://builtwithnix.org)
7 | [](https://www.fairforall.org/about/)
8 | [](https://app.element.io/#/room/#neuron:matrix.org "Chat on Matrix")
9 | [](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 |
--------------------------------------------------------------------------------
/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 | {.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 | {.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 | {.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 | 
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 | {.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 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------