Compare commits

...

149 Commits
v0.4.0 ... main

Author SHA1 Message Date
Sören Tempel 00a4361d20
Determine fixture path using the current working directory (#422) 2 weeks ago
Tillman Jex 578894f45b
release zk v0.14.1 (#417) 1 month ago
Tillman Jex e21519a1b4
readme: exit maintenance mode, add logo, contributing (#416) 1 month ago
Tillman Jex cfa74a7f0c
update Makefile (go 1.21 and alpine), update contributing.md (#412) 1 month ago
Tillman Jex 8e2add0606
raise sqlite version for musl build compatibility (#414) 1 month ago
guangwu bf5cad60dc
chore: pkg imported more than once (#410)
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
Co-authored-by: tjex <tjex@tjex.net>
1 month ago
Alexis Praga c3f26ca12b
Doc: how to edit today's daily note (#407)
documentation daily note tutorial extra detail
2 months ago
Tillman Jex 0973f9929d
allow notebook as hidden dir (#402) 2 months ago
dependabot[bot] 45b68121ce
Bump actions/configure-pages from 4 to 5 (#398)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tillman Jex <tjex@tjex.net>
3 months ago
Tadeas Uhlir c9d4734d3c
fix(lsp): ignore diagnostic check within code blocks (#399)
Co-authored-by: tjex <tjex@tjex.net>
3 months ago
Tadeas Uhlir 05c50a70d5
fix(lsp): fix trigger completion of zk LSP (#397) 3 months ago
Tillman Jex b10d51dbc2
accept tripple dash file URIs as valid links (#391)
Co-authored-by: Jurica Bacurin <jurica@bacurin.de>
3 months ago
Tillman Jex 56f4e650ea
git: ignore delve debug files (#396) 3 months ago
Sumit Sahrawat 6a06ded3c4
Fix hyperlink from `README.md` to `docs/getting-started.md` (#395) 3 months ago
Kyle Huggins 1471c6be2c
Update documentation to restore GitHub Pages functionality (#387) 5 months ago
Tillman Jex d4e542b70c
tesh test case for yaml dates without time stamp (#385) 5 months ago
Tillman Jex 537c7f7554
fix day range parsing (zk-org/zk#382) (#384) 5 months ago
Michael McDonagh 55d5487d24
Fix broken links (#381) 6 months ago
dependabot[bot] f7079be6c0
Bump actions/stale from 7 to 9 (#365)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tillman Jex <tjex@tjex.net>
6 months ago
dependabot[bot] a1f2a701af
Bump github/codeql-action from 2 to 3 (#367)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tillman Jex <tjex@tjex.net>
6 months ago
dependabot[bot] 70a9afb9f5
Bump actions/setup-go from 4 to 5 (#363)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
Michael McDonagh 0eaf26483f
Remove references to old repo (#373)
Finalise transfer from old repo (github.com/mickael-menu/zk) to new (github.com/zk-org/zk)

Co-authored-by: tjex <tjex@tjex.net>
6 months ago
Julio Lopez 5a2333d0af
test: add TestFormatDateHelperElapsed (#359)
improve and extend test coverage in handlebars_test.go, which deals with time and date formatting.
6 months ago
Mickaël Menu e3e52dfe69
Add `CONTRIBUTING.md` (#378) 6 months ago
Julio Lopez 87f3680a9b
chore(ci): use Go 1.21 (#360)
Co-authored-by: Jurica Bacurin <jurica@bacurin.de>
6 months ago
dependabot[bot] f120f9546c
Bump golang.org/x/crypto from 0.0.0-20220525230936-793ad666bf5e to 0.17.0 (#368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jurica Bacurin <jurica@bacurin.de>

Ran tests against the PR, all pass.
6 months ago
Tillman Jex 5bb1c0ba89
Maintenance mode callout fix (#377) 6 months ago
Tillman Jex 1e28fe4b92
notification of maintenance mode (#376)
update readme to communicate maintenance mode due to project handover
6 months ago
Michael McDonagh 50fb638dd9
Update the arch repo link (#372) 6 months ago
Michael McDonagh 7ba9df6526
Relative output needs relative input (#374) 6 months ago
Julio Lopez be1249d7a6
Use 'go-version-file' option in build workflow (#355) 8 months ago
Julio Lopez 228c96fcea
Address io/ioutil deprecation (#354) 8 months ago
dependabot[bot] 1fd1298c00
Bump actions/checkout from 3 to 4 (#344)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
khimaros 0b4db9ade6
Fix parsing large notes (#339) 11 months ago
Mickaël Menu 072fae2f6c
Fix `{{date}}` helper test (#324) 1 year ago
Mickaël Menu b71a74eabc Release zk 0.14.0 1 year ago
Mickaël Menu 1ec777de7e Update documentation of config alias 1 year ago
Mickaël Menu 75205fe099
Rename `note.ignore` config property to `note.exclude` (#322) 1 year ago
wrvsrx 6252e51595
Fix LSP positions using UTF-16 offsets (#317) 1 year ago
Leonardo Mello e26ac5133e
Add `notebook` configuration to set default notebook path (#304) 1 year ago
Leonardo Mello a8d1db4c57
Add `tool.shell` configuration key (#302) 1 year ago
dependabot[bot] ba611e1c1e
Bump actions/setup-go from 3 to 4 (#299) 1 year ago
Mickaël Menu 39bcb8069f
Bump go-sqlite3 (#296) 1 year ago
dependabot[bot] 89ff022593
Bump golang.org/x/text from 0.3.7 to 0.3.8 (#292) 1 year ago
Mickaël Menu ea4457ad67 Release zk 0.13.0 1 year ago
Cyril Dutrieux 6401a4e1f6
Add "title" to the Command in code actions (#288) 1 year ago
Patrick Anker f7d4db07d6
Add `zk.link` LSP command (#284) 1 year ago
Leiser Fernández Gallo 150c82fb22
Fix emanote URL (#283) 1 year ago
Matthias Vogelgesang 279f9ef3cd
Add `zk-spaced` to list of related projects (#281) 1 year ago
dependabot[bot] 6506198743
Bump actions/stale from 6 to 7 (#277) 2 years ago
Mickaël Menu ae3a86dbfa
Add new options for LSP command `zk.new` (#276) 2 years ago
Mickaël Menu d79da8933a Release zk 0.12.0 2 years ago
Mickaël Menu 15d4cfc921
Rename the `{{date}}` helper to `{{format-date}}` (#274) 2 years ago
Oliver Marriott 142b636342
Support multiple `--match` flags (#268) 2 years ago
Oliver Marriott 9d88245102
Fix `--working-dir=` and `--notebook-dir=` options parsing (#267) 2 years ago
Zach Leslie 404ef9d6f5
Add the `get-date` template helper (#262) 2 years ago
Mickaël Menu c21c4fc21f
LSP: Add support for external URLs with `documentLink` (#261) 2 years ago
dependabot[bot] cdf4f8e0c1
Bump actions/stale from 5 to 6 (#258) 2 years ago
Mickaël Menu 1745097256
Fix error when pairing `--link-to` and `--linked-by` (#255) 2 years ago
Sridhar Ratnakumar 9faec3628a
neuron.md: Add link to docs for Emanote (#247) 2 years ago
Kristof Lünenschloß 814a5d7c07
Link to mickael-menu/zk-nvim instead of megalithic/zk.nvim (#249) 2 years ago
bibor ab1d8fd0bd
LSP: Fix double use of `notebook.RelPath` (#246) 2 years ago
Mickaël Menu 61b9c0f5d7
LSP: Fix finding backlink references for notes in a folder (#245) 2 years ago
Mickaël Menu 78c40eeecf Release zk 0.11.1 2 years ago
Pete Kazmier a6e522562e
Add explicit flag to read from standard input (#242) 2 years ago
Mickaël Menu eefc3be9c6 Release zk 0.11.0 2 years ago
Mickaël Menu c06375ee3a
Fix updating links after creating a new note (#237) 2 years ago
Mickaël Menu 8bfafe5dab
Fix LSP link recognition with unicode (#238) 2 years ago
Mickaël Menu 53aabce1ed
Support standard input via shell redirection with `zk new` (#240) 2 years ago
Mickaël Menu aaf6c42bd8
Hide index progress in non-interactive shells (#234) 2 years ago
Mickaël Menu 68e6b70eae
Upgrade to Go 1.18 and update dependencies (#221) 2 years ago
Mickaël Menu 4b76fbadf1
Support regular expressions with `--match` (#222) 2 years ago
Mickaël Menu 1a05a04432
LSP: Deprecate title matching for wiki links (#218) 2 years ago
Mickaël Menu dbd791f672 Release zk 0.10.1 2 years ago
Mickaël Menu c356b7bd00
Update existing links when adding a new note (#219) 2 years ago
Mickaël Menu 3c634fb00a
Remove dependency on `libicu` (#213) 2 years ago
dependabot[bot] 1167cb99ae
Bump GitHub workflow actions (#210) 2 years ago
Arto Jonsson d9be3c04a6
Add Dependabot configuration (#206) 2 years ago
Arto Jonsson bfa065d1e3
Enable GitHub code scanning (#207) 2 years ago
Mickaël Menu 60e9491c1b Fix running the Homebrew workflow (#204) 2 years ago
Sridhar Ratnakumar 94f563cd51
Add Nix install instructions (#198) 2 years ago
Mickaël Menu d8c9031f9c
Release zk 0.10.0 2 years ago
Mickaël Menu 7622d9ee19
Don't parse Markdown table headers a colon tags (#196) 2 years ago
Nelyah 94e8a0d437
Customize `fzf` options and key bindings (#154) 2 years ago
Mickaël Menu 525047fab9
Add support for double star globbing in `note.ignore` config option (#195) 2 years ago
Mickaël Menu c237b4d57d
Fix broken wiki links in subdirectories (#193) 2 years ago
Demaro Stanberry 0bfab74eac
Fix file url parsing on Windows (#186) 2 years ago
Stephen Bolton 9e33d679e2
Add `--id` flag to `zk new` (#183) 2 years ago
codito a0ced3c330
Fix LSP path autocompletion on Windows (#175) 2 years ago
codito 83c15cc927
Fix Windows build (#171) 2 years ago
Mickaël Menu e037befdf1
Add support for `ZK_SHELL` (#166) 2 years ago
Mickaël Menu c6e529fdfa Add a triage GitHub workflow 2 years ago
Mickaël Menu c429517c6b
Fix incorrect timezone for natural dates (#156) 2 years ago
Mickaël Menu 7b92ca06cc
Disable `additionalTextEdits` for completion items by default (#160) 2 years ago
Mickaël Menu a4b31b4794
Bump the Homebrew formula on GitHub releases (#157) 2 years ago
Mitchell Hanberg 73dfc83aeb
Point Homebrew install instructions to homebrew-core (#153) 2 years ago
Mickaël Menu da6e842406
Run `tesh` with the GitHub checks (#148) 2 years ago
Mickaël Menu eb095e5fc0
Add `tesh` test suite (#147) 2 years ago
Mickaël Menu 6ccbbe8613
LSP auto-completion of YAML frontmatter tags (#146) 2 years ago
Mickaël Menu 04a157f3be
Clarify the Homebrew instructions (#145) 3 years ago
Karim Abou Zeid 63dee0ce3f
Add install instructions for Arch Linux. (#140) 3 years ago
Mickaël Menu 8fe0671b1d
Mention Homebrew tap and zk-nvim plugin
cc @mhanberg
3 years ago
Mickaël Menu eddc4b5845
Fix embedded image links shown as not found (#127) 3 years ago
Mickaël Menu f8ef8cc093 Release zk 0.9.0 3 years ago
Mickaël Menu f566916470
Support RFC 3339 dates with the time flags (#123) 3 years ago
Mickaël Menu 5b623074e0
Take into account `--no-input` with `zk init` (#122) 3 years ago
Mickaël Menu e317f0d879
Fix infinite loop when parsing a single-character hashtag (#119) 3 years ago
Mickaël Menu 7d5752b04a
Remove Git LFS (#117) 3 years ago
Mickaël Menu f3527ba60e
Don't match all notes when `--link-to` or `--linked-by` have no match (#115) 3 years ago
Mickaël Menu 9c06068cce
Add LSP commands `zk.list` and `zk.tag.list` (#114) 3 years ago
Mickaël Menu 0e88685140
Make filename take precedence over folders when matching sub-paths (#112) 3 years ago
Mickaël Menu 9ae8e5b041 Release zk 0.8.0 3 years ago
Mickaël Menu a7a82cf7d6
Filter through the note frontmatter with `--interactive` (#110) 3 years ago
Mickaël Menu 16e1904096
Add a command to produce a graph of the indexed notes (#106) 3 years ago
Mickaël Menu 3b05a0061d
Fix indexing wiki links using partial paths (#104) 3 years ago
Mickaël Menu aea5308f66
Add a verbose mode to `zk index` (#103) 3 years ago
Mickaël Menu 59b8269344
Add `--dry-run` flag for `zk new` (#96) 3 years ago
Mickaël Menu 439c1b1a69
Customize LSP completion items (#92) 3 years ago
Mickaël Menu b1c69b4765
Add filename and filename-stem template variables (#91) 3 years ago
Adam Reese 39467a1b7c
Allow indexing from outside the notebook directory (#90) 3 years ago
Mickaël Menu 9bd2eacf06 Fix error with fs.DirExists()
It errors out when providing a subdirectory of a file, e.g. `index.md/.zk`
3 years ago
Mickaël Menu cfb337c967
Release zk 0.7.0 3 years ago
Mickaël Menu c4dfb4af9f
Fix indexing encoded Markdown links (#88) 3 years ago
Mickaël Menu d74cf52d1e
List note tags (#85) 3 years ago
Mickaël Menu 977dc20bcb
Fix crash when creating a new note fails (#83) 3 years ago
Mickaël Menu dc27a7dd7c
Improve Markdown and wiki links matching and generation (#71)
Fallback wiki link resolution by matching on title or path
Add new template variables when generating Markdown links
Add a {{substring}} template helper
3 years ago
Mickaël Menu aed57452f7
Do not exclude notes containing broken links from the index (#80) 3 years ago
Mickaël Menu 1fc7ceafac
Fallback LSP references to the current note (#79) 3 years ago
Michał Kiełbowicz b0e59bbefa
Support pandoc filetype (#77)
Enable compatibility with vim-pandoc-syntax
3 years ago
Mickaël Menu 5abbab4c73
Upgrade dependencies (#73) 3 years ago
Mickaël Menu 8c7ec93ca5
Allow setting the --working-dir and --notebook-dir flags before the subcommand when using aliases (#67) 3 years ago
Peter Stuifzand 977625bb3d
Add "abs-path" to template format (#60) 3 years ago
Mickaël Menu dba28a6436
Set \n the default value for the --footer flag 3 years ago
Mickaël Menu d8e3aa86f6
Release zk 0.6.0 3 years ago
Mickaël Menu 8ae67868b6
Add a new configuration option to ignore files when indexing notes (#59) 3 years ago
Peter Stuifzand a72a2a5f74
LSP references to browse the backlinks of the link under the caret (#58) 3 years ago
Alys 7642cf91a8
Remove outdated instructions from README (#57) 3 years ago
Mickaël Menu 970be5bb71
Replace the ./go build script with a Makefile (#55) 3 years ago
Mickaël Menu 5b877dd953
Fix unicode support in wiki links (#49) 3 years ago
Mickaël Menu 729dbe9760
Add JSON and JSON lines list formats (#52) 3 years ago
Mickaël Menu 274234e608
Fix links with section anchors (#51) 3 years ago
Mickaël Menu 45f4d85169
Add a GitHub build action (#50) 3 years ago
Mickaël Menu ac80a3cca1
Release zk 0.5.0 3 years ago
Mickaël Menu 6771eb0e43
Auto-complete only the path of a Markdown link by typing: [custom title](( (#43) 3 years ago
Mickaël Menu aa68199df8
Publish LSP diagnostics for dead links and wiki-link titles (#42) 3 years ago
Mickaël Menu e8cb1d8046
Fix completion with Neovim's built-in LSP client (#41) 3 years ago
Mickaël Menu b17b42a06f
Add LSP custom commands and code actions to create new notes (#40) 3 years ago
Cormac Relf 3664734bda
Fix parsing LSP URIs containing spaces (#38) 3 years ago
Mickaël Menu 83b14ca827
Customize the format of fzf's lines with your own template (#36) 3 years ago

2
.gitattributes vendored

@ -1,2 +0,0 @@
*.db filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text

@ -0,0 +1,19 @@
title: "[Help] "
body:
- type: checkboxes
id: checks
attributes:
label: Verify
options:
- label: I searched the existing discussions for help
required: true
- type: textarea
id: help
attributes:
label: How can we help you?
validations:
required: true
- type: markdown
attributes:
value: |
:warning: Unfortunately, my time is limited and I can't offer reliable user support. I might answer if you catch me on a slow day, or hopefully someone else will.

@ -0,0 +1,14 @@
body:
- type: checkboxes
id: checks
attributes:
label: Verify
options:
- label: I searched the existing discussions for similar ideas
required: true
- type: textarea
id: help
attributes:
label: Share your idea or feature request
validations:
required: true

@ -0,0 +1,65 @@
name: Bug report
description: File a bug report to help improve zk.
body:
- type: markdown
attributes:
value: |
Thank you for filing a bug report!
- type: checkboxes
id: checks
attributes:
label: Check if applicable
description: |
:warning: My time is limited and if I don't plan on fixing the reported bug myself, I might close this issue. No hard feelings.
:heart: But if you would like to contribute a fix yourself, **I'll be happy to guide you through the codebase and review a pull request**.
options:
- label: I have searched the existing issues (**required**)
required: true
- label: I'm willing to help fix the problem and contribute a pull request
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: Also tell me, what did you expect to happen?
placeholder: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: bug-steps
attributes:
label: How to reproduce?
description: |
Step by step explanation to reproduce the issue.
If you can, drag and drop:
- a zipped sample notebook
- screenshots or a screencast showing the issue
placeholder: |
1. Add a note with the content "..."
2. Run `zk edit --interactive`
3. See error
...
validations:
required: true
- type: textarea
id: vim-config
attributes:
label: zk configuration
description: |
Paste the minimal `zk` configuration file (`.zk/config.toml`) reproducing the issue.
render: toml
validations:
required: true
- type: textarea
id: bug-environment
attributes:
label: Environment
description: |
Run the following shell commands and paste the result here:
```
zk --version && echo "system: `uname -srmo`"
```
placeholder: |
zk 0.13.0
system: Darwin 22.5.0 arm64
render: bash

@ -0,0 +1,10 @@
name: Feature request
description: Suggest an idea for this project.
body:
- type: checkboxes
id: checks
attributes:
label: If you have an idea, open a discussion
options:
- label: I will [create a new discussion](https://github.com/zk-org/zk/discussions/new?category=ideas) instead of an issue.

@ -0,0 +1,13 @@
name: User support
description: You need help?
body:
- type: markdown
attributes:
value: |
:warning: Unfortunately, my time is limited and I can't offer reliable user support. I might answer if you catch me on a slow day, or hopefully someone else will.
- type: checkboxes
id: checks
attributes:
label: If you need help, open a discussion
options:
- label: I will [create a new discussion](https://github.com/zk-org/zk/discussions/new?category=help) instead of an issue.

@ -0,0 +1,16 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
# Maintain dependencies for gomod
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
# Disable version updates for gomod dependencies
open-pull-requests-limit: 0

@ -0,0 +1,37 @@
name: Build
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
lfs: 'true'
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- name: Install dependencies
run: |
go install github.com/mickael-menu/tesh@latest
- name: Build
run: make build
- name: Test
run: make test
- name: Tesh
# See https://github.com/actions/runner/issues/241#issuecomment-924327172
shell: script --return --quiet --command "bash {0}"
run: make tesh

@ -0,0 +1,40 @@
name: "CodeQL code scanning"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
# Wed 23:33 UTC
- cron: '33 23 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

@ -0,0 +1,43 @@
name: Deploy to GitHub Pages
on:
release:
types:
- "published"
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Fix anchor tag in README
run: ex -s -c '%s/docs\/getting-started\.md/docs\/getting-started/|x' README.md
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

@ -0,0 +1,19 @@
name: Release
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
homebrew:
runs-on: macos-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Update Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.HOMEBREW_GITHUB_TOKEN}}
formula: zk

@ -0,0 +1,31 @@
name: Triage
on:
push:
paths:
- .github/workflows/triage.yml
schedule:
# Once every day at midnight UTC
- cron: "0 0 * * *"
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
days-before-stale: 30
days-before-issue-close: 5
stale-issue-label: 'stale'
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs in the next 5 days.'
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
exempt-issue-labels: 'help wanted,feature request,enhancement,bug'
days-before-pr-close: -1
stale-pr-label: 'stale'
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity.'

9
.gitignore vendored

@ -11,7 +11,14 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Delve debug files
*__debug_bin*
# Dependency directories (remove the comment below to include it)
# vendor/
.zk
# IDEs/Editors
.vscode/
notebook.db
zk

@ -2,7 +2,275 @@
All notable changes to this project will be documented in this file.
<!--## Unreleased-->
## Unreleased
## 0.14.1
### Fixed
* Fixed parsing large notes @khimaros in https://github.com/zk-org/zk/pull/339
* fix day range parsing (zk-org/zk#382) by @tjex in https://github.com/zk-org/zk/pull/384
* accept tripple dash file URIs as valid links by @tjex in https://github.com/zk-org/zk/pull/391
* fix(lsp): fix trigger completion of zk LSP by @Rahlir in https://github.com/zk-org/zk/pull/397
* fix(lsp): ignore diagnostic check within code blocks by @Rahlir in https://github.com/zk-org/zk/pull/399
* allow notebook as hidden dir by @tjex in https://github.com/zk-org/zk/pull/402
## 0.14.0
### Added
* New [`tool.shell`](docs/tool-shell.md) configuration key to set a custom shell (contributed by [@lsvmello](https://github.com/zk-org/zk/pull/302)).
* New [`notebook.dir`](docs/config-notebook.md) configuration key to set the default notebook (contributed by [@lsvmello](https://github.com/zk-org/zk/pull/304)).
### Changed
* The `note.ignore` configuration property was renamed to `note.exclude`, to be more consistent with the CLI flags.
### Fixed
* Fixed LSP positions using UTF-16 offsets (contributed by [@wrvsrx](https://github.com/zk-org/zk/pull/317)).
## 0.13.0
### Added
* LSP:
* `zk.new` now returns the created note's content in its output (`content`), and has two new options:
* `dryRun` will prevent `zk.new` from creating the note on the file system.
* `insertContentAtLocation` can be used to insert the created note's content into an arbitrary location.
* A new `zk.link` command to insert a link to a given note (contributed by [@psanker](https://github.com/zk-org/zk/pull/284)).
## 0.12.0
### Added
* LSP: Support for external URLs with `documentLink`.
* New `{{date}}` template helper to obtain a date object from natural language (contributed by [@zalegrala](https://github.com/zk-org/zk/pull/262)).
```
Get a relative date using natural language:
{{date "next week"}}
Format a date returned by `get-date`:
{{format-date (date "monday") "timestamp"}}
```
* `zk list` now support multiple `--match`/`-m` flags, which allows to search for several tokens appearing in any order in the notes (contributed by [@rktjmp](https://github.com/zk-org/zk/pull/268)).
### Changed
* **Breaking change:** The `{{date}}` template helper was renamed to `{{format-date}}`. You might need to update your configuration and templates.
### Fixed
* [#243](https://github.com/zk-org/zk/issues/243) LSP: Fixed finding backlink references for notes in a folder.
* [#254](https://github.com/zk-org/zk/issues/254) Fixed SQL error when pairing `--link-to` and `--linked-by`.
## 0.11.1
### Changed
* `zk new` now requires the `--interactive`/`-i` flag to read the note body from a pipe or standard input. [See rational](https://github.com/zk-org/zk/pull/242#issuecomment-1182602001).
### Fixed
* [#244](https://github.com/zk-org/zk/issues/244) Fixed `zk new` waiting for `Ctrl-D` to proceed (contributed by [@pkazmier](https://github.com/zk-org/zk/pull/242)).
## 0.11.0
### Added
* Use regular expressions when searching for notes with `--match`.
```sh
# Find notes containing emails.
$ zk list --match-strategy re --match ".+@.+"
$ zk list -Mr -m ".+@.+"
```
### Changed
* The flags `--exact-match`/`-e` are deprecated in favor of `--match-strategy exact`/`-Me`.
### Deprecated
* The LSP server does not support resolving a wiki link to a note title anymore.
* For example, `[[Planet]]` can match a note with filename `i4w0 Planet.md` but not `i4w0.md` with a Markdown title `Planet` anymore.
* This "smart" fallback resolution based on note titles was too fragile and not supported by the `zk` CLI.
### Fixed
* [#233](https://github.com/zk-org/zk/issues/233) Hide index progress in non-interactive shells.
* [#235](https://github.com/zk-org/zk/issues/235) Fix LSP link recognition with unicode (contributed by [@zkbpkp](https://github.com/zk-org/zk/issues/235)).
* [#236](https://github.com/zk-org/zk/issues/236) Fix updating links after creating a new note.
* [#239](https://github.com/zk-org/zk/discussions/239) Support standard input via shell redirection with `zk new`.
## 0.10.1
### Changed
* Removed the dependency on `libicu`.
### Fixed
* Indexed links are now automatically updated when adding a new note, if it is a better match than the previous link target.
## 0.10.0
### Added
* New `--date` flag for `zk new` to set the current date manually.
* New `--id` flag for `zk new` to skip ID generation and use a provided value (contributed by [@skbolton](https://github.com/zk-org/zk/pull/183)).
* [#144](https://github.com/zk-org/zk/issues/144) LSP auto-completion of YAML frontmatter tags.
* [zk-nvim#26](https://github.com/zk-org/zk-nvim/issues/26) The LSP server doesn't use `additionalTextEdits` anymore to remove the trigger characters when completing links.
* You can customize the default behavior with the [`use-additional-text-edits` configuration key](docs/config-lsp.md).
* [#163](https://github.com/zk-org/zk/issues/163) Use the `ZK_SHELL` environment variable to override the shell for `zk` only.
* [#173](https://github.com/zk-org/zk/issues/173) Support for double star globbing in `note.ignore` config option.
* [#137](https://github.com/zk-org/zk/issues/137) Customize the `fzf` options used by `zk`'s interactive modes with the [`fzf-options`](docs/tool-fzf.md) config option (contributed by [@Nelyah](https://github.com/zk-org/zk/pull/154)).
* [#168](https://github.com/zk-org/zk/discussions/168) Customize the `fzf` key binding to create new notes with the [`fzf-bind-new`](docs/tool-fzf.md) config option.
### Changed
* The default `fzf` key binding to create a new note with `zk edit --interactive` was changed to `Ctrl-E`, to avoid conflict with the default `Ctrl-N` binding.
### Fixed
* [#126](https://github.com/zk-org/zk/issues/126) Embedded image links shown as not found.
* [#152](https://github.com/zk-org/zk/issues/152) Incorrect timezone for natural dates.
* [#170](https://github.com/zk-org/zk/issues/170) Broken wiki links in subdirectories.
* [#185](https://github.com/zk-org/zk/issues/185) Don't parse a Markdown table header as a colon tag.
## 0.9.0
### Added
* New LSP commands:
* [`zk.list`](docs/editors-integration.md#zklist) to search for notes.
* [`zk.tag.list`](docs/editors-integration.md#zktaglist) to retrieve the list of tags.
* `--debug` mode which prints a stacktrace on `SIGINT`.
### Fixed
* [#111](https://github.com/zk-org/zk/issues/111) Filenames take precedence over folders when matching a sub-path with wiki links.
* [#118](https://github.com/zk-org/zk/issues/118) Fix infinite loop when parsing a single-character hashtag.
* [#121](https://github.com/zk-org/zk/issues/121) Take into account the `--no-input` flag with `zk init`.
* [#120](https://github.com/zk-org/zk/discussions/120) Support RFC 3339 dates with the time flags (e.g. `--created-before`).
## 0.8.0
### Added
* New `zk graph --format json` command which produces a JSON graph of the notes matching the given criteria.
* New template variables `filename` and `filename-stem` when formatting notes (e.g. with `zk list --format`) and for the [`fzf-line`](docs/tool-fzf.md) config key.
* Customize how LSP completion items appear in your editor when auto-completing links with the [`[lsp.completion]` configuration section](docs/config-lsp.md).
```toml
[lsp.completion]
# Show the note title in the completion pop-up, or fallback on its path if empty.
note-label = "{{title-or-path}}"
# Filter out the completion pop-up using the note title or its path.
note-filter-text = "{{title}} {{path}}"
# Show the note filename without extension as detail.
note-detail = "{{filename-stem}}"
```
* New `--dry-run` flag for `zk new` which prints out the path and content of the generated note instead of saving it to the file system.
* New `--verbose` flag for `zk index` which prints detailed information about the indexing process.
* You can now filter through the [YAML frontmatter](docs/note-frontmatter.md) with `zk list --interactive`.
### Fixed
* [#89](https://github.com/zk-org/zk/issues/89) Calling `zk index` from outside the notebook (contributed by [@adamreese](https://github.com/zk-org/zk/pull/90)).
* [#98](https://github.com/zk-org/zk/issues/98) Index wiki links using partial paths for `--linked-by` and `--link-to`.
* [#98](https://github.com/zk-org/zk/issues/98) Ignore spaces around the pipe in wiki links for LSP diagnostics.
## 0.7.0
### Added
* List the tags found in your notebook with `zk tag list`.
* Many options are available to customize the output, including JSON serialization. See `zk tag list --help`.
* Support for LSP references to browse the backlinks of the current note, if the caret is not over a link.
* New template variables are available when [generating custom Markdown links with `link-format`](docs/note-format.md).
* `filename`, `path`, `abs-path` and `rel-path` for many path flavors.
* `metadata` to use information (e.g. `id`) from the YAML frontmatter.
* The LSP server is now matching wiki links to any part of a note's path or its title.
* Given the note `book/z5mj Information Graphics.md` with the title "Book Review of Information Graphics", the following wiki links would work from a note located under `journal/2020-09-25.md`:
```markdown
[[../book/z5mj]]
[[book/z5mj]]
[[z5mj]]
[[book review information]]
[[Information Graphics]]
```
* Use the `{{abs-path}}` template variable when [formatting notes](docs/template-format.md) to print the absolute path to the note (contributed by [@pstuifzand](https://github.com/zk-org/zk/pull/60)).
* A new `{{substring s index length}}` template helper extracts a portion of a given string, e.g.:
* `{{substring 'A full quote' 2 4}}` outputs `full`
* `{{substring 'A full quote' -5 5}` outputs `quote`
### Fixed
* UTF-8 handling in the LSP server.
* [#78](https://github.com/zk-org/zk/issues/78) Do not exclude notes containing broken links from the index.
* Allow setting the `--working-dir` and `--notebook-dir` flags before the `zk` subcommand when using aliases, e.g. `zk -W ~/notes my-alias`.
* [#86](https://github.com/zk-org/zk/issues/86) Index encoded Markdown links.
## 0.6.0
### Added
* Use JSON formats with `zk list` for easy post-processing:
* `--format json` prints a plain JSON array.
* `--format jsonl` prints one JSON note object per line, according to [JSON Lines](https://jsonlines.org/).
* The new `{{json}}` template helper serializes any template context variable into a valid JSON value, e.g.:
* `{{json title}}` prints with quotes `"An interesting note"`
* `{{json .}}` serializes the full template context as a JSON object.
* Use `--header` and `--footer` options with `zk list` to print arbitrary text at the start or end of the list.
* Support for LSP references to browse the backlinks of the link under the caret (contributed by [@pstuifzand](https://github.com/zk-org/zk/pull/58)).
* New [`note.ignore`](docs/config-note.md) configuration option to ignore files matching the given path globs when indexing notes.
```yaml
[note]
ignore = [
"log-*.md"
"drafts/*"
]
```
### Fixed
* [#16](https://github.com/zk-org/zk/issues/16) Links with section anchors, e.g. `[[filename#section]]`.
* Unicode support in wiki links. If you use accents or ideograms, please run `zk index --force` after upgrading to fix your index.
## 0.5.0
### Added
* [Editor integration through LSP](https://github.com/zk-org/zk/issues/22):
* New code actions to create a note using the current selection as title.
* Custom commands to [run `new` and `index` from your editor](docs/editors-integration.md#custom-commands).
* Diagnostics to [report dead links or wiki link titles](docs/config-lsp.md).
* Auto-complete only the path of a Markdown link by typing `[custom title]((`.
* Customize the format of `fzf`'s lines [with your own template](docs/tool-fzf.md).
```toml
[tool]
fzf-line = "{{style 'green' path}}{{#each tags}} #{{this}}{{/each}} {{style 'black' body}}"
```
### Changed
* Automatically index the notebook when saving a note with an LSP-enabled editor.
* This ensures that tags and notes auto-completion lists are up-to-date.
### Fixed
* Creating a new note from `fzf` in a directory containing spaces.
* Fix completion with Neovim's built-in LSP client (contributed by [@cormacrelf](https://github.com/zk-org/zk/pull/39)).
## 0.4.0
@ -10,18 +278,18 @@ All notable changes to this project will be documented in this file.
* Interactive wizard for the `zk init` command.
* An experimental Language Server for LSP-compatible editors:
* Auto-complete Markdown links with `[[` (setup wiki-links in the [note formats configuration](docs/note-format.md))
* Auto-complete Markdown links with `[[` (setup wiki links in the [note formats configuration](docs/note-format.md))
* Auto-complete [hashtags and colon-separated tags](docs/tags.md).
* Preview the content of a note when hovering a link.
* Navigate in your notes by following internal links.
* [And more to come...](https://github.com/mickael-menu/zk/issues/22)
* [And more to come...](https://github.com/zk-org/zk/issues/22)
* See [the documentation](docs/editors-integration.md) for configuration samples.
* Pair `--match` with `--exact-match` / `-e` to search for (case insensitive) exact occurrences in your notes.
* This can be useful when looking for terms including special characters, such as `[[name]]`.
* Generating links to notes.
* Use the `{{link}}` template variable when [formatting notes](docs/template-format.md) to print a link to the note, relative to the working directory.
* Use the `{{format-link path title}}` template helper to render a custom link.
* Customize the link format from the [note formats settings](docs/note-format.md). You can for example choose regular Markdown links, Wiki-links or a custom format.
* Customize the link format from the [note formats settings](docs/note-format.md). You can for example choose regular Markdown links, wiki links or a custom format.
### Changed
@ -59,7 +327,7 @@ All notable changes to this project will be documented in this file.
### Fixed
* [#4](https://github.com/mickael-menu/zk/issues/4) Terminal borked when piping content with Vim
* [#4](https://github.com/zk-org/zk/issues/4) Terminal borked when piping content with Vim
## 0.2.1
@ -92,3 +360,4 @@ All notable changes to this project will be documented in this file.
* Renamed `--linking-to` filtering option to `--link-to`.
* Multiple `--extra` variables are now separated by `,` instead of `;`.

@ -0,0 +1,62 @@
# Contributing to `zk`
## Understanding the codebase
### Building the project
It is recommended to use the `Makefile` for compiling the project, as the `go` command requires a few parameters.
```shell
make build
```
This will be expanded to the following command:
```shell
CGO_ENABLED=1 GOARCH=arm64 go build -tags "fts5" -ldflags "-X=main.Version=`git describe --tags --match v[0-9]* 2> /dev/null` -X=main.Build=`git rev-parse --short HEAD`"
```
- `CGO_ENABLED=1` enables CGO, which is required by the `mattn/go-sqlite3` dependency.
- `GOARCH=arm64` is only required for Apple Silicon chips.
- `-tags "fts5"` enables the FTS option with `mattn/go-sqlite3`, which handles much of the magic behind `zk`'s `--match` filtering option.
- ``-ldflags "-X=main.Version=`git describe --tags --match v[0-9]* 2> /dev/null` -X=main.Build=`git rev-parse --short HEAD`"`` will automatically set `zk`'s build and version numbers using the latest Git tag and commit SHA.
### Automated tests
The project is vetted with two different kind of automated tests: unit tests and end-to-end tests.
#### Unit tests
Unit tests are using the standard [Go testing library](https://pkg.go.dev/testing). To execute them, use the command `make test`.
They are ideal for testing parsing output or individual API edge cases and minutiae.
#### End-to-end tests
Most of `zk`'s functionality is tested with functional tests ran with [`tesh`](https://github.com/mickael-menu/tesh), which you can execute with `make tesh` (or `make teshb`, to debug whitespaces changes).
When addressing a GitHub issue, it's a good idea to begin by creating a `tesh` file in `tests/issue-XXX.tesh`. If a starting notebook state is required, it can be added under `tests/fixtures`.
If you modify the output of `zk`, you may disrupt some `tesh` files. You can use `make tesh-update` to automatically update them with the correct output.
### CI workflows
Several GitHub action workflows are executed when pull requests are merged or releases are created.
- `.github/workflows/build.yml` checks that the project can be built and the tests still pass.
- `.github/workflows/codeql.yml` runs static analysis to vet code quality.
- `.github/workflows/gh-pages.yml` deploy the documentation files to GitHub Pages.
- `.github/workflows/release.yml` submits a new version to Homebrew when a Git version tag is created.
- `.github/workflows/triage.yml` automatically tags old issues and PRs as staled.
## Releasing a new version
When `zk` is ready to be released, you can update the `CHANGELOG.md` ([for example](https://github.com/zk-org/zk/commit/ea4457ad671aa85a6b15747460c6f2c9ad61bf73)) and create a new Git version tag (for example `v0.13.0`). Make sure you follow the [Semantic Versioning](https://semver.org) scheme.
Then, create [a new GitHub release](https://github.com/zk-org/zk/releases) with a copy of the latest `CHANGELOG.md` entries and the binaries for all supported platforms.
Binaries can be created automatically using `make dist-linux` and `make dist-macos`.
Unfortunately, `make dist-macos` must be run manually on both an Apple Silicon and Intel chips. The Linux builds are created using Docker and [these custom images](https://github.com/zk-org/zk-xcompile), which are hosted via [ghcr.io within zk-org](https://github.com/orgs/zk-org/packages/container/package/zk-xcompile).
This process is convoluted because `zk` requires CGO with `mattn/go-sqlite3`, which prevents using Go's native cross-compilation. Transitioning to a CGO-free SQLite driver such as [cznic/sqlite](https://gitlab.com/cznic/sqlite) could simplify the distribution process significantly.

@ -1,15 +1,86 @@
VERSION := `git describe --tags --match v[0-9]* 2> /dev/null`
# Build zk in the current folder.
build:
$(call go,build)
# Build and install `zk` using go's default bin directory.
install:
$(call go,install)
# Run unit tests.
test:
$(call go,test,./...)
# Run end-to-end tests.
tesh: build
@PATH=".:$(shell pwd):$(PATH)" tesh tests tests/fixtures
# Run end-to-end tests and prints difference as raw bytes.
teshb: build
@PATH=".:$(shell pwd):$(PATH)" tesh -b tests tests/fixtures
# Update end-to-end tests.
tesh-update: build
PATH=".:$(shell pwd):$(PATH)" tesh -u tests tests/fixtures
alpine:
$(call alpine,build)
all: macos linux
# Produce a release bundle for all platforms.
dist: dist-macos dist-linux
rm -f zk
macos:
rm -f zk && ./go build && zip -r "zk-${VERSION}-macos-`uname -m`.zip" zk
# Produce a release bundle for macOS.
dist-macos:
rm -f zk && make && zip -r "zk-${VERSION}-macos-`uname -m`.zip" zk
linux:
rm -f zk && docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk mickaelmenu/zk-xcompile:linux-i386 /bin/bash -c './go build' && tar -zcvf "zk-${VERSION}-linux-i386.tar.gz" zk
rm -f zk && docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk mickaelmenu/zk-xcompile:linux-amd64 /bin/bash -c './go build' && tar -zcvf "zk-${VERSION}-linux-amd64.tar.gz" zk
rm -f zk && docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk mickaelmenu/zk-xcompile:linux-arm64 /bin/bash -c './go build' && tar -zcvf "zk-${VERSION}-linux-arm64.tar.gz" zk
# Produce a release bundle for Linux.
dist-linux: dist-linux-amd64 dist-linux-arm64 dist-linux-i386 dist-alpine-amd64 dist-alpine-arm64 dist-alpine-i386
dist-linux-amd64:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:linux-amd64 /bin/bash -c 'make' \
&& tar -zcvf "zk-${VERSION}-linux-amd64.tar.gz" zk
dist-linux-arm64:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:linux-arm64 /bin/bash -c 'make' \
&& tar -zcvf "zk-${VERSION}-linux-arm64.tar.gz" zk
dist-linux-i386:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:linux-i386 /bin/bash -c 'make' \
&& tar -zcvf "zk-${VERSION}-linux-i386.tar.gz" zk
dist-alpine-amd64:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:alpine-amd64 /bin/bash -c 'make alpine' \
&& tar -zcvf "zk-${VERSION}-alpine-amd64.tar.gz" zk
dist-alpine-arm64:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:alpine-arm64 /bin/bash -c 'make alpine' \
&& tar -zcvf "zk-${VERSION}-alpine-arm64.tar.gz" zk
dist-alpine-i386:
rm -f zk \
&& docker run --rm -v "${PWD}":/usr/src/zk -w /usr/src/zk ghcr.io/zk-org/zk-xcompile:alpine-i386 /bin/bash -c 'make alpine' \
&& tar -zcvf "zk-${VERSION}-alpine-i386.tar.gz" zk
# Clean build products.
clean:
rm -rf zk*
VERSION := `git describe --tags --match v[0-9]* 2> /dev/null`
BUILD := `git rev-parse --short HEAD`
ENV_PREFIX := CGO_ENABLED=1
# Add necessary env variables for Apple Silicon.
ifeq ($(shell uname -sm),Darwin arm64)
ENV_PREFIX := $(ENV) GOARCH=arm64
endif
# Wrapper around the go binary, to set all the default parameters.
define go
$(ENV_PREFIX) go $(1) -tags "fts5" -ldflags "-X=main.Version=$(VERSION) -X=main.Build=$(BUILD)" $(2)
endef
# Alpine (musl) requires statically linked libs. This should be compatible for
# Void linux and other musl based distros aswell.
define alpine
$(ENV_PREFIX) go $(1) -tags "fts5" -ldflags "-extldflags=-static -X=main.Version=$(VERSION) -X=main.Build=$(BUILD)" $(2)
endef

@ -1,10 +1,12 @@
<div align="center">
<h1>zk</h1>
<img alt="zk logo" width="20%" src="./docs/assets/media/zk-black-modern.png" />
<h4>A plain text note-taking assistant</h4>
<img alt="Screencast" width="95%" src="docs/assets/media/screencast.svg"/>
<p>Looking for a quick usage example? <a href="docs/getting-started.md">Let's get started</a>.</p>
</div>
Looking for a quick usage example? [Let's get started](docs/getting-started.md).
## Description
`zk` is a command-line tool helping you to maintain a plain text [Zettelkasten](https://zettelkasten.de/introduction/) or [personal wiki](https://en.wikipedia.org/wiki/Personal_wiki).
@ -14,9 +16,10 @@
* [Creating notes from templates](docs/note-creation.md)
* [Advanced search and filtering capabilities](docs/note-filtering.md) including [tags](docs/tags.md), links and mentions
* [Integration with your favorite editors](docs/editors-integration.md):
* [`zk.nvim`](https://github.com/megalithic/zk.nvim) for Neovim 0.5+, maintained by [Seth Messer](https://github.com/megalithic)
* [`zk-vscode`](https://github.com/mickael-menu/zk-vscode) for Visual Studio Code
* [Any LSP-compatible editor](docs/editors-integration.md)
* [`zk-nvim`](https://github.com/zk-org/zk-nvim) for Neovim 0.8+
* [`zk-vscode`](https://github.com/zk-org/zk-vscode) for Visual Studio Code
* (*unmaintained*) [`zk.nvim`](https://github.com/megalithic/zk.nvim) for Neovim 0.5+ by [Seth Messer](https://github.com/megalithic)
* [Interactive browser](docs/tool-fzf.md), powered by `fzf`
* [Git-style command aliases](docs/config-alias.md) and [named filters](docs/config-filter.md)
* [Made with automation in mind](docs/automation.md)
@ -36,47 +39,62 @@
## Install
[Check out the latest release](https://github.com/mickael-menu/zk/releases) for pre-built binaries for macOS and Linux (`zk` was not tested on Windows).
### Build from scratch
[Check out the latest release](https://github.com/zk-org/zk/releases) for pre-built binaries for macOS and Linux (`zk` was not tested on Windows).
Make sure you have a working [Go installation](https://golang.org/), then clone the repository:
### Homebrew
```sh
$ git clone https://github.com/mickael-menu/zk.git
$ cd zk
$ chmod a+x go
brew install zk
```
#### On macOS
`icu4c` is required to build `zk`, which you can install with [Homebrew](https://brew.sh/).
Or, if you want to the latest changes:
```sh
brew install --HEAD zk
```
$ brew install icu4c
$ ./go build
$ ./zk -h
### Nix
```sh
# Run zk from Nix store without installing it:
nix run nixpkgs#zk
# Or, to install it permanently:
nix-env -iA zk
```
##### Apple Silicon
### Arch Linux
The build command needs additional environment variables on Apple Silicon:
You can install [the zk package](https://archlinux.org/packages/extra/x86_64/zk/) from the official repos.
```
$ GOARCH=arm64 CGO_CFLAGS="-I/opt/homebrew/opt/icu4c/include" CGO_LDFLAGS="-L/opt/homebrew/opt/icu4c/lib" ./go build
```sh
sudo pacman -S zk
```
#### On Linux
### Build from scratch
`libicu-dev` is required to build `zk`, use your favorite package manager to install it.
Make sure you have a working [Go 1.21+ installation](https://golang.org/), then clone the repository:
```sh
$ git clone https://github.com/zk-org/zk.git
$ cd zk
```
$ apt-install libicu-dev
$ ./go build
#### On macOS / Linux
```
$ make
$ ./zk -h
```
## Contributing
We warmly welcome issues, PRs and [discussions](https://github.com/zk-org/zk/discussions).
Here you can read [some useful info for contributing to `zk`](./CONTRIBUTING.md).
## Related projects
* [Neuron](https://github.com/srid/neuron) a great tool to publish a Zettelkasten on the web
* [Emanote](https://emanote.srid.ca/) an improved successor to Neuron
* [sirupsen's zk](https://github.com/sirupsen/zk) a collection of scripts with a similar purpose
* [zk-spaced](https://github.com/matze/zk-spaced) spaced repetition plugin for zk

@ -0,0 +1,24 @@
title: "zk"
permalink: /:title
defaults:
- scope:
path: "README.md"
values:
title: "zk"
- scope:
path: "" # all
values:
render_with_liquid: false
exclude:
- ".github/"
- ".gitignore"
- "CHANGELOG.md"
- "CONTRIBUTING.md"
- "LICENSE"
- "Makefile"
- "go.mod"
- "go.sum"
- "internal/"
- "main.go"
- "tests/"

@ -1,2 +0,0 @@
permalink: /:title
theme: jekyll-theme-modernist

@ -1,57 +0,0 @@
<!doctype html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
{% seo %}
<link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
<script src="{{ '/assets/js/scale.fix.js' | relative_url }}"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div class="wrapper">
<header {% unless site.description or site.github.project_tagline %} class="without-description" {% endunless %}>
<h1><a href="https://mickael-menu.github.io/zk/">{{ site.title | default: site.github.repository_name }}</a></h1>
{% if site.description or site.github.project_tagline %}
<p>{{ site.description | default: site.github.project_tagline }}</p>
{% endif %}
<p class="view"><a href="{{ site.github.repository_url }}">View the Project on GitHub <small>{{ github_name }}</small></a></p>
<ul>
{% if site.show_downloads %}
<li><a href="{{ site.github.zip_url }}">Download <strong>ZIP File</strong></a></li>
<li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
{% endif %}
<li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
</ul>
</header>
<section>
{{ content }}
</section>
</div>
<footer>
{% if site.github.is_project_page %}
<p>Project maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a></p>
{% endif %}
</footer>
<!--[if !IE]><script>fixScale(document);</script><![endif]-->
{% if site.google_analytics %}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{ site.google_analytics }}', 'auto');
ga('send', 'pageview');
</script>
{% endif %}
</body>
</html>

@ -1,9 +0,0 @@
---
---
@import "{{ site.theme }}";
code, pre {
font-size: inherit;
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 373 KiB

@ -7,4 +7,4 @@
* [send notes for processing by other programs](external-processing.md)
* [create a note with initial content](note-creation.md) from a standard input pipe
If you find out that `zk` does not behave as expected or could communicate better with other programs, [please post an issue](https://github.com/mickael-menu/zk/issues).
If you find out that `zk` does not behave as expected or could communicate better with other programs, [please post an issue](https://github.com/zk-org/zk/issues).

@ -6,9 +6,10 @@ Declaring your own aliases is a great way to make your experience with `zk` easi
## Configuring aliases
Command aliases are declared in your [configuration file](config.md), under the `[alias]` section. They are executed with `$SHELL -c`, which allows you to:
Command aliases are declared in your [configuration file](config.md), under the `[alias]` section. They are executed with [your default shell](tool-shell.md), which allows you to:
* expand arguments with `$@` or `$*`
* [it is recommended to wrap `$@` in quotes](https://github.com/zk-org/zk/issues/316#issuecomment-1543564168)
* expand environment variables
* run several commands with `&&`
* pipe several commands with `|`
@ -17,7 +18,7 @@ An alias can call other aliases but cannot call itself. This enables you to over
```toml
[alias]
edit = "zk edit --interactive $@"
edit = 'zk edit --interactive "$@"'
```
When running an alias, the `ZK_NOTEBOOK_DIR` environment variable is set to the absolute path of the current notebook. You can use it to run commands working no matter the location of the working directory.

@ -34,7 +34,7 @@ You can override the global [note configuration](config-note.md) and [extra user
```toml
[group.journal.note]
filename = "{{date now}}"
filename = "{{format-date now}}"
template = "journal.md"
[group.journal.extra]

@ -0,0 +1,62 @@
# LSP configuration
The `[lsp]` [configuration file](config.md) section provides settings to fine-tune the [LSP editors integration](editors-integration.md).
## Completion
Customize how completion items appear in your editor when auto-completing links with the `[lsp.completion]` sub-section.
| Setting | Type | Description |
|-----------------------------|------------|---------------------------------------------------------------------------------------|
| `note-label` | `template` | Label displayed in the completion pop-up for each note |
| `note-filter-text` | `template` | Text used as a source when filtering the completion pop-up with keystrokes |
| `note-detail` | `template` | Additional information about a completion item |
| `use-additional-text-edits` | `boolean` | Indicates whether `additionalTextEdits` will be used to remove the trigger characters |
Each key accepts a [template](template.md) with the following context:
| Variable | Type | Description |
|-----------------|----------|--------------------------------------------------------------------|
| `filename` | string | Filename of the note, including its extension |
| `filename-stem` | string | Filename of the note without the file extension |
| `path` | string | File path to the note, relative to the notebook root |
| `abs-path` | string | Absolute file path to the note |
| `rel-path` | string | File path to the note, relative to the current directory |
| `title` | string | Note title |
| `title-or-path` | string | Note title or path if empty |
| `metadata` | map | YAML frontmatter metadata, e.g. `metadata.description`<sup>1</sup> |
1. YAML keys are normalized to lower case.
## Diagnostics
Use the `[lsp.diagnostics]` sub-section to configure how LSP diagnostics are reported to your editors. Each diagnostic setting can be:
* An empty string or `none` to ignore this diagnostic.
* `hint`, `info`, `warning` or `error` to enable and set the severity of the diagnostic.
| Setting | Default | Description |
|--------------|-----------|---------------------------------------------------------------------------|
| `wiki-title` | `"none"` | Report titles of wiki-links, which is useful if you use IDs for filenames |
| `dead-link` | `"error"` | Warn for dead links between notes |
## Complete example
```toml
[lsp]
[lsp.diagnostics]
# Report titles of wiki-links as hints.
wiki-title = "hint"
# Warn for dead links between notes.
dead-link = "error"
[lsp.completion]
# Show the note title in the completion pop-up, or fallback on its path if empty.
note-label = "{{title-or-path}}"
# Filter out the completion pop-up using the note title or its path.
note-filter-text = "{{title}} {{path}}"
# Show the note filename without extension as detail.
note-detail = "{{filename-stem}}"
```

@ -14,6 +14,8 @@ The `[note]` section from the [configuration file](config.md) is used to set the
* `template` (string)
* Path to the [template](template.md) used to generate the note content.
* Either an absolute path, or relative to `.zk/templates/`.
* `exclude` (list of strings)
* List of [path globs](https://en.wikipedia.org/wiki/Glob_\(programming\)) excluded during note indexing.
* `id-charset` (string)
* Characters set used to [generate random IDs](note-id.md).
* You can use:
@ -46,10 +48,10 @@ Here are some common filename patterns you may want to use:
* Readable and practical for web servers, but fragile in case of renaming.
* `{{id}}-{{slug title}}` e.g. `i2hn8-an-interesting-concept.md`
* The best of both worlds? Readable but if you link only with the prefix ID, you can rename without breaking links.
* `{{date now 'timestamp'}}` e.g. `200911172034.md`
* `{{format-date now 'timestamp'}}` e.g. `200911172034.md`
* Verbose, but sortable by creation date and stable.
* `{{date now 'timestamp'}} {{title}}` e.g. `200911172034 An interesting concept.md`
* `{{format-date now 'timestamp'}} {{title}}` e.g. `200911172034 An interesting concept.md`
* The format of [The Archive](https://zettelkasten.de/the-archive/) and [sirupsen's zk](https://github.com/sirupsen/zk).
* `{{date now '%Y-%m-%d'}}` e.g. `2009-11-17.md`
* `{{format-date now '%Y-%m-%d'}}` e.g. `2009-11-17.md`
* Sortable, human-friendly format for a daily journal.
* i.e. [Maintaining a daily journal](daily-journal.md).

@ -0,0 +1,15 @@
# Notebook configuration
The `[notebook]` section from the [configuration file](config.md) is used to set the default notebook directory.
If the path starts with `~` it will be replaced with the user home directory (`$HOME`). This property also supports environment variables.
```toml
[notebook]
dir = "~/notebook" # same as "$HOME/notebook"
```
The following properties are customizable:
* `dir` (string)
* Path of the default notebook.
* Only available in the global config file (`~/.config/zk/config.toml`).

@ -2,14 +2,17 @@
Each [notebook](notebook.md) contains a configuration file used to customize your experience with `zk`. This file is located at `.zk/config.toml` and uses the [TOML format](https://github.com/toml-lang/toml). It is composed of several optional sections:
* `[notebook]` configures the [default notebook](config-notebook.md)
* `[note]` sets the [note creation rules](config-note.md)
* `[extra]` contains free [user variables](config-extra.md) which can be expanded in templates
* `[group]` defines [note groups](config-group.md) with custom rules
* `[format]` configures the [note format settings](note-format.md), such as Markdown options.
* `[format]` configures the [note format settings](note-format.md), such as Markdown options
* `[tool]` customizes interaction with external programs such as:
* [your default editor](tool-editor.md)
* [your default shell](tool-shell.md)
* [your default pager](tool-pager.md)
* [`fzf`](tool-fzf.md)
* `[lsp]` setups the [Language Server Protocol settings](config-lsp.md) for [editors integration](editors-integration.md)
* `[filter]` declares your [named filters](config-filter.md)
* `[alias]` holds your [command aliases](config-alias.md)
@ -24,6 +27,10 @@ Notebook configuration files will inherit the settings defined in the global con
Here's an example of a complete configuration file:
```toml
# NOTEBOOK SETTINGS
[notebook]
dir = "~/notebook"
# NOTE SETTINGS
[note]
@ -62,11 +69,11 @@ author = "Mickaël"
# GROUP OVERRIDES
[dir.journal]
[group.journal]
paths = ["journal/weekly", "journal/daily"]
[dir.journal.note]
filename = "{{date now}}"
[group.journal.note]
filename = "{{format-date now}}"
# MARKDOWN SETTINGS
@ -83,6 +90,9 @@ colon-tags = true
# Default editor used to open notes.
editor = "nvim"
# Default shell used by aliases and commands.
shell = "/bin/bash"
# Pager used to scroll through long output.
pager = "less -FIRX"
@ -104,5 +114,13 @@ recent = "zk edit --sort created- --created-after 'last two weeks' --interactive
# Show a random note.
lucky = "zk list --quiet --format full --sort random --limit 1"
```
# LSP (EDITOR INTEGRATION)
[lsp]
[lsp.diagnostics]
# Report titles of wiki-links as hints.
wiki-title = "hint"
# Warn for dead links between notes.
dead-link = "error"
```

@ -2,7 +2,7 @@
Let's assume you want to write daily notes named like `2021-02-16.md` in a `journal/daily` sub-directory. This common use case is a good fit for creating a [note group](config-group.md) overriding the default [note creation](note-creation.md) settings.
First, create a `group` entry in the [configuration file](config.md) to set the note settings for this directory. Refer to the [template syntax reference](template.md) to understand how to use the `{{date}}` helper.
First, create a `group` entry in the [configuration file](config.md) to set the note settings for this directory. Refer to the [template syntax reference](template.md) to understand how to use the `{{format-date}}` helper.
```toml
[group.daily]
@ -10,8 +10,8 @@ First, create a `group` entry in the [configuration file](config.md) to set the
paths = ["journal/daily"]
[group.daily.note]
# %Y-%m-%d is actually the default format, so you could use {{date now}} instead.
filename = "{{date now '%Y-%m-%d'}}"
# %Y-%m-%d is actually the default format, so you could use {{format-date now}} instead.
filename = "{{format-date now '%Y-%m-%d'}}"
extension = "md"
template = "daily.md"
```
@ -19,7 +19,7 @@ template = "daily.md"
Next, create a template file under `.zk/templates/daily.md` to render the note content. Here we used the date again to generate a title like "February 16, 2021".
```markdown
# {{date now "long"}}
# {{format-date now "long"}}
What did I do today?
```
@ -43,3 +43,7 @@ Let's unpack this alias:
* `$ZK_NOTEBOOK_DIR` is set to the absolute path of the current [notebook](notebook.md) when running an alias. Using it allows you to run `zk daily` no matter where you are in the notebook folder hierarchy.
* We need to use double quotes around `$ZK_NOTEBOOK_DIR`, otherwise it will not be expanded.
If you want to edit today's note, simply use this alias:
```sh
$ zk daily
```

@ -2,8 +2,8 @@
There are several extensions available to integrate `zk` in your favorite editor:
* [`zk.nvim`](https://github.com/megalithic/zk.nvim) for Neovim 0.5+, maintained by [Seth Messer](https://github.com/megalithic)
* [`zk-vscode`](https://github.com/mickael-menu/zk-vscode) for Visual Studio Code
* [`zk-nvim`](https://github.com/zk-org/zk-nvim) for Neovim 0.5+
* [`zk-vscode`](https://github.com/zk-org/zk-vscode) for Visual Studio Code
## Language Server Protocol
@ -13,16 +13,24 @@ There are several extensions available to integrate `zk` in your favorite editor
* Auto-complete [hashtags and colon-separated tags](tags.md).
* Preview the content of a note when hovering a link.
* Navigate in your notes by following internal links.
* [And more to come...](https://github.com/mickael-menu/zk/issues/22)
* Create a new note using the current selection as title.
* Diagnostics for dead links and wiki-links titles.
* [And more to come...](https://github.com/zk-org/zk/issues/22)
You can configure some of these features in your notebook's [configuration file](config-lsp.md).
To start the Language Server, use the `zk lsp` command. Refer to the following sections for editor-specific examples. [Feel free to share the configuration for your editor](https://github.com/mickael-menu/zk/issues/22).
### Editor LSP configurations
### Vim and Neovim
To start the Language Server, use the `zk lsp` command. Refer to the following sections for editor-specific examples. [Feel free to share the configuration for your editor](https://github.com/zk-org/zk/issues/22).
#### Vim and Neovim 0.4
#### Vim and Neovim
##### Vim and Neovim 0.4
With [`coc.nvim`](https://github.com/neoclide/coc.nvim), run `:CocConfig` and add the following in the settings file:
<details><summary><tt>coc-settings.json</tt></summary>
```jsonc
{
// Important, otherwise link completion containing spaces and other special characters won't work.
@ -38,11 +46,39 @@ With [`coc.nvim`](https://github.com/neoclide/coc.nvim), run `:CocConfig` and ad
}
}
```
</details>
Here are some additional useful key bindings and custom commands:
<details><summary><tt>~/.config/nvim/init.vim</tt></summary>
```viml
" User command to index the current notebook.
"
" zk.index expects a notebook path as first argument, so we provide the current
" buffer path with expand("%:p").
command! -nargs=0 ZkIndex :call CocAction("runCommand", "zk.index", expand("%:p"))
nnoremap <leader>zi :ZkIndex<CR>
" User command to create and open a new note, to be called like this:
" :ZkNew {"title": "An interesting subject", "dir": "inbox", ...}
"
" Note the concatenation with the "edit" command to open the note right away.
command! -nargs=? ZkNew :exec "edit ".CocAction("runCommand", "zk.new", expand("%:p"), <args>).path
" Create a new note after prompting for its title.
nnoremap <leader>zn :ZkNew {"title": input("Title: ")}<CR>
" Create a new note in the directory journal/daily.
nnoremap <leader>zj :ZkNew {"dir": "journal/daily"}<CR>
```
</details>
#### Neovim 0.5 built-in LSP client
##### Neovim 0.5 built-in LSP client
Using [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig):
<details><summary><tt>~/.config/nvim/init.lua</tt></summary>
```lua
local lspconfig = require('lspconfig')
local configs = require('lspconfig/configs')
@ -62,11 +98,14 @@ lspconfig.zk.setup({ on_attach = function(client, buffer)
-- Add keybindings here, see https://github.com/neovim/nvim-lspconfig#keybindings-and-completion
end })
```
</details>
### Sublime Text
#### Sublime Text
Install the [Sublime LSP](https://github.com/sublimelsp/LSP) package, then run the **Preferences: LSP Settings** command. Add the following to the settings file:
<details><summary><tt>LSP.sublime-settings</tt></summary>
```jsonc
{
"clients": {
@ -80,7 +119,133 @@ Install the [Sublime LSP](https://github.com/sublimelsp/LSP) package, then run t
}
}
```
</details>
### Visual Studio Code
#### Visual Studio Code
Install the [`zk-vscode`](https://marketplace.visualstudio.com/items?itemName=mickael-menu.zk-vscode) extension from the Marketplace.
### Custom commands
Using `zk`'s LSP custom commands, you can call `zk` commands right from your editor. Please refer to your editor's documentation on how to bind keyboard shortcuts to custom LSP commands.
#### `zk.index`
This LSP command calls `zk index` to refresh your notebook's index. It can be useful to make sure that the auto-completion is up-to-date. `zk.index` takes two arguments:
1. A path to a file or directory in the notebook to index.
2. <details><summary>(Optional) A dictionary of additional options (click to expand)</summary>
| Key | Type | Description |
|---------|---------|-----------------------------------|
| `force` | boolean | Reindexes all the notes when true |
</details>
`zk.index` returns a dictionary of indexing statistics.
#### `zk.new`
This LSP command calls `zk new` to create a new note. It can be useful to quickly create a new note with a key binding. `zk.new` takes two arguments:
1. A path to any file or directory in the notebook, to locate it.
2. <details><summary>(Optional) A dictionary of additional options (click to expand)</summary>
| Key | Type | Description |
|---------------------------|----------------------|----------------------------------------------------------------------------------------------------------------------|
| `title` | string | Title of the new note |
| `content` | string | Initial content of the note |
| `dir` | string | Parent directory, relative to the root of the notebook |
| `group` | string | [Note configuration group](config-group.md) |
| `template` | string | [Custom template used to render the note](template-creation.md) |
| `extra` | dictionary | A dictionary of extra variables to expand in the template |
| `date` | string | A date of creation for the note in natural language, e.g. "tomorrow" |
| `edit` | boolean | When true, the editor will open the newly created note (**not supported by all editors**) |
| `dryRun` | boolean | When true, `zk` will not actually create the note on the file system, but will return its generated content and path |
| `insertLinkAtLocation` | location<sup>1</sup> | A location in another note where a link to the new note will be inserted |
| `insertContentAtLocation` | location<sup>1</sup> | A location in another note where the content of the new note will be inserted |
1. The `location` type is an [LSP Location object](https://microsoft.github.io/language-server-protocol/specification#location), for example:
```json
{
"uri":"file:///Users/mickael/notes/9se3.md",
"range": {
"end":{"line": 5, "character":149},
"start":{"line": 5, "character":137}
}
}
```
</details>
`zk.new` returns a dictionary with two properties:
* `path` containing the absolute path to the created note.
* `content` containing the raw content of the created note.
#### `zk.link`
This LSP command allows editors to tap into the note linking mechanism. It takes three arguments:
1. A `path` to any file in the notebook that will be linked to
2. An LSP `location` object that points to where the link will be inserted
3. An optional title of the link. If `title` is not provided, the title of the note will be inserted instead
`zk.link` returns a JSON object with the path to the linked note, if the linking was successful.
**Note**: This command is _not_ exposed in the command line. This command is targeted at editor / plugin authors to extend zk functionality.
#### `zk.list`
This LSP command calls `zk list` to search a notebook. It takes two arguments:
1. A path to any file or directory in the notebook, to locate it.
2. <details><summary>A dictionary of additional options (click to expand)</summary>
| Key | Type | Required? | Description |
| ------------------ | -------------- | ----------- | ------------------------------------------------------------------------- |
| `select` | string array | Yes | List of note fields to return<sup>1</sup> |
| `hrefs` | string array | No | Find notes matching the given path, including its descendants |
| `limit` | integer | No | Limit the number of notes found |
| `match` | string array | No | Terms to search for in the notes |
| `exactMatch` | boolean | No | (deprecated: use `matchStrategy`) Search for exact occurrences of the `match` argument (case insensitive) |
| `matchStrategy` | string | No | Specify match strategy, which may be "fts" (default), "exact" or "re" |
| `excludeHrefs` | string array | No | Ignore notes matching the given path, including its descendants |
| `tags` | string array | No | Find notes tagged with the given tags |
| `mention` | string array | No | Find notes mentioning the title of the given ones |
| `mentionedBy` | string array | No | Find notes whose title is mentioned in the given ones |
| `linkTo` | string array | No | Find notes which are linking to the given ones |
| `linkedBy` | string array | No | Find notes which are linked by the given ones |
| `orphan` | boolean | No | Find notes which are not linked by any other note |
| `related` | string array | No | Find notes which might be related to the given ones |
| `maxDistance` | integer | No | Maximum distance between two linked notes |
| `recursive` | boolean | No | Follow links recursively |
| `created` | string | No | Find notes created on the given date |
| `createdBefore` | string | No | Find notes created before the given date |
| `createdAfter` | string | No | Find notes created after the given date |
| `modified` | string | No | Find notes modified on the given date |
| `modifiedBefore` | string | No | Find notes modified before the given date |
| `modifiedAfter` | string | No | Find notes modified after the given date |
| `sort` | string array | No | Order the notes by the given criterion |
1. As the output of this command might be very verbose and put a heavy load on the LSP client, you need to explicitly set which note fields you want to receive with the `select` option. The following fields are available: `filename`, `filenameStem`, `path`, `absPath`, `title`, `lead`, `body`, `snippets`, `rawContent`, `wordCount`, `tags`, `metadata`, `created`, `modified` and `checksum`.
</details>
`zk.list` returns the found notes as a JSON array.
#### `zk.tag.list`
This LSP command calls `zk tag list` to return the list of tags in a notebook. It takes two arguments:
1. A path to any file or directory in the notebook, to locate it.
2. <details><summary>(Optional) A dictionary of additional options (click to expand)</summary>
| Key | Type | Required? | Description |
|--------|--------------|-----------|--------------------------------------------------|
| `sort` | string array | No | Order the tags by the given criteria<sup>1</sup> |
1. The available sort criteria are `name` and `note-count`. You can change the order by appending `-` or `+` to the criterion.
</details>
`zk.tag.list` returns the tags as a JSON array.

@ -27,7 +27,7 @@ You can customize your experience using [custom templates](template.md) to gener
If you are not sure whether a note already exists for a particular subject, the "search or create" mode might be more appropriate than `zk new`. It is inspired by [Notational Velocity](https://notational.net/) and enables searching for an existing note or creating a new one in a single action.
From `zk`'s interactive edit screen, press `Ctrl-N` to create a new note using the current search query as title.
From `zk`'s interactive edit screen, press `Ctrl-E` to create a new note using the current search query as title.
<div align="center"><img alt="Create a note" width="85%" src="assets/media/new2.svg"/></div>

@ -27,3 +27,7 @@ But you can make your [notebook](notebook.md) even more tightly integrated with
serve = "neuron gen -wS"
gen = "neuron gen -o public"
```
## Emanote
Emanote is neuron's successor. For Emanote-specific configuration, see https://emanote.srid.ca/start/resources/zk.

@ -10,15 +10,21 @@ By default, `zk new` will start [your editor](tool-editor.md) after creating the
If you are not sure whether a note already exists for a particular subject, the "search or create" mode might be more appropriate than `zk new`. It is inspired by [Notational Velocity](https://notational.net/) and enables searching for an existing note or creating a new one in a single action.
This option is available when running `zk edit --interactive`, which spawns [`fzf`](tool-fzf.md) to filter selected notes. From `fzf`, press `Ctrl-N` to create a new note using the current search query as title.
This option is available when running `zk edit --interactive`, which spawns [`fzf`](tool-fzf.md) to filter selected notes. From `fzf`, press `Ctrl-E` to create a new note using the current search query as title.
## Create a note with initial content
Initial content can be fed to the template through a standard input pipe, which will be expandable with the `{{content}}` [template variable](template-creation.md).
Initial content can be fed to the template through standard input using `zk new --interactive`, which will be expandable with the `{{content}}` [template variable](template-creation.md).
For example, to use the content of the macOS clipboard as the initial content you can run:
```sh
$ pbpaste | zk new
$ pbpaste | zk new --interactive
```
Alternatively, you can use the content of a file:
```sh
$ zk new --interactive < file.txt
```

@ -45,11 +45,50 @@ $ zk list --linked-by "`zk inline journal`"
Use `--match <query>` (or `-m`) to search through the title and body of notes.
The search is powered by a [full-text search](https://en.wikipedia.org/wiki/Full-text_search) database enabling near-instant results. Queries are not case-sensitive and terms are tokenized, which means that searching for `create` will also match `created` and `creating`.
The search is powered by different strategies to answer various use cases:
* `fts` (default) uses a [full-text search](https://en.wikipedia.org/wiki/Full-text_search) database to offer near-instant results and advanced search operators.
* `exact` is useful if you need to find patterns containing special characters.
* `re` enables regular expression for advanced use cases.
Change the currently used strategy with `--match-strategy <strategy>` (or `-M`). To set the default strategy, you can declare a [custom alias](config-alias.md):
```toml
[alias]
list = "zk list --match-strategy re $@"
```
The `--match` option may be given multiple times, where each argument will be combined with a boolean AND.
For example,
```sh
$ zk list --tag "recipe" --match "pizza -pineapple" --match "mushrooms"
```
Is equivalent to,
```sh
$ zk list --tag "recipe" --match "(pizza -pineapple) AND (mushrooms)"
```
### Full-text search (`fts`)
The default match strategy is powered by a [full-text search](https://en.wikipedia.org/wiki/Full-text_search) database enabling near-instant results. Queries are not case-sensitive and terms are tokenized, which means that searching for `create` will also match `created` and `creating`.
A syntax similar to Google Search is available for advanced search queries.
### Combining terms
```sh
# FTS is the default match strategy
$ zk list --match "tesla OR edison"
# ...but you can enable it explicitly.
$ zk list --match-strategy fts --match "tesla OR edison"
$ zk list -Mf -m "tesla OR edison"
```
#### Combining terms
By default, the search engine will find the notes containing all the terms in the query, in any order.
@ -83,7 +122,7 @@ Finally, you can filter out results by excluding a term with `NOT` (all caps) or
"tesla -car"
```
### Search in specific fields
#### Search in specific fields
If you want to search only in the title or body of notes, prefix a query with `title:` or `body:`.
@ -92,7 +131,7 @@ If you want to search only in the title or body of notes, prefix a query with `t
"body: (tesla OR edison)"
```
### Prefix terms
#### Prefix terms
Match any term beginning with the given prefix with a wildcard `*`.
@ -106,13 +145,25 @@ Prefixing a query with `^` will match notes whose title or body start with the f
"title: ^journal"
```
### Search for special characters
### Exact matches (`exact`)
If you need to find patterns containing special characters, such as an `email@addre.ss` or a `[[wiki-link]]`, use the `--exact-match` / `-e` option. The search will be case-insensitive.
If you need to find patterns containing special characters, such as an `email@addre.ss` or a `[[wiki-link]]`, use the `exact` match strategy. The search will be case-insensitive.
```sh
$ zk list --match-strategy exact --match "[[link]]"
$ zk list -Me -m "[[link]]"
```
$ zk list --exact-match --match "[[link]]"
$ zk list -em "[[link]]"
### Regular expressions (`re`)
For advanced use cases, you can use the `re` match strategy to search the notebook using regular expressions. The supported syntax is similar to the one used by Python or Perl. [See the full reference](https://golang.org/s/re2syntax).
:warning: Make sure to use quotes to prevent your shell from expanding wildcards.
```sh
# Find notes containing emails.
$ zk list --match-strategy re --match ".+@.+"
$ zk list -Mr -m ".+@.+"
```
## Filter by tags

@ -21,9 +21,22 @@ You can set up some features of `zk`'s Markdown parser from your [configuration
### Customizing the Markdown links generated by `zk`
By default, `zk` will generate regular Markdown links for internal links. If you prefer to use `[[Wiki Links]]` instead, set the `link-format` setting to `wiki`. If you want to override completely the link format, you can also set `link-format` to a [custom template](template.md). Two variables `path` and `title` are available in the template, for example to generate a wiki-link with a title:
By default, `zk` will generate regular Markdown links for internal links. If you prefer to use `[[Wiki Links]]` instead, set the `link-format` setting to `wiki`. If you want to override completely the link format, you can also set `link-format` to a [custom template](template.md). For example, to generate a wiki link using an ID from the frontmatter and a title:
```toml
[format.markdown]
link-format = "[[{{path}}|{{title}}]]"
link-format = "[[{{metadata.id}}|{{title}}]]"
```
The following variables are available in the template:
| Variable | Type | Description |
|------------|--------|-----------------------------------------------------------|
| `filename` | string | Filename of the note |
| `path` | string | File path to the note, relative to the notebook directory |
| `abs-path` | string | Absolute file path to the note |
| `rel-path` | string | File path to the note, relative to the current directory |
| `title` | string | Note title |
| `metadata` | map | YAML frontmatter metadata, e.g. `metadata.id`<sup>1</sup> |
1. YAML keys are normalized to lower case.

@ -6,6 +6,8 @@ To create a new notebook, simply run `zk init [<directory>]`.
Most `zk` commands are operating "Git-style" on the notebook containing the current working directory (or one of its parents). However, you can explicitly set which notebook to use with `--notebook-dir` or the `ZK_NOTEBOOK_DIR` environment variable. Setting `ZK_NOTEBOOK_DIR` in your shell configuration (e.g. `~/.profile`) can be used to define a default notebook which `zk` commands will use when the working directory is not in another notebook.
If the [default notebook](config-notebook.md) is set it will be used as `ZK_NOTEBOOK_DIR`, unless this environment variable is not already set.
## Anatomy of a notebook
Similarly to Git, a notebook is identified by the presence of a `.zk` directory at its root. This directory contains the only `zk`-specific files in your notebook:

@ -12,3 +12,16 @@ You can filter your notes by their tags using the `--tags` option, as demonstrat
```sh
$ zk list --tag "inbox OR todo, NOT done"
```
## Listing tags
You can list all the tags found in your notebook using `zk tag list`.
The following variables are available in the templates used when formatting tags, for example with `zk tag list --format <template>`.
| Variable | Type | Description |
|--------------|--------|------------------------------------------------|
| `id` | int | Unique ID of this tag in the Notebook database |
| `name` | string | Name of the tag |
| `note-count` | int | Number of notes attached to this tag |

@ -9,7 +9,7 @@ The following variables are available in the templates used when [creating new n
| `content` | string | Any text piped through the standard input |
| `dir` | string | Parent directory in the notebook |
| `extra.<key>` | string | [Additional variables](config-extra.md) provided through the config file or `--extra` |
| `now` | date | Current date and time, useful when paired with [`{{date now}}`](template.md) |
| `now` | date | Current date and time, useful when paired with [`{{format-date now}}`](template.md) |
| `env` | map | Dictionary of case-sensitive environment variables, e.g. `{{env.PATH}}`. |
These additional variables are available only to the note content template, once the filename is generated.

@ -2,21 +2,24 @@
The following variables are available in the templates used when formatting notes, for example with `zk list --format <template>`.
| Variable | Type | Description |
|---------------|----------|--------------------------------------------------------------------------|
| `path` | string | File path to the note, relative to the current directory |
| `title` | string | Note title |
| `link` | string | Markdown link to the note, relative to the current directory<sup>1</sup> |
| `lead` | string | First paragraph extracted from the note content |
| `body` | string | All of the note content, minus the heading |
| `snippets` | [string] | List of context-sensitive relevant excerpts from the note |
| `raw-content` | string | The full raw content of the note file |
| `word-count` | int | Number of words in the note |
| `tags` | [string] | List of tags found in the note |
| `metadata` | map | YAML frontmatter metadata, e.g. `metadata.description`<sup>2</sup> |
| `created` | date | Date of creation of the note |
| `modified` | date | Last date of modification of the note |
| `checksum` | string | SHA-256 checksum of the note file |
| Variable | Type | Description |
|-----------------|----------|--------------------------------------------------------------------------|
| `filename` | string | Filename of the note, including its extension |
| `filename-stem` | string | Filename of the note without the file extension |
| `path` | string | File path to the note, relative to the current directory |
| `abs-path` | string | File path to the note, absolute path including the notebook directory |
| `title` | string | Note title |
| `link` | string | Markdown link to the note, relative to the current directory<sup>1</sup> |
| `lead` | string | First paragraph extracted from the note content |
| `body` | string | All of the note content, minus the heading |
| `snippets` | [string] | List of context-sensitive relevant excerpts from the note |
| `raw-content` | string | The full raw content of the note file |
| `word-count` | int | Number of words in the note |
| `tags` | [string] | List of tags found in the note |
| `metadata` | map | YAML frontmatter metadata, e.g. `metadata.description`<sup>2</sup> |
| `created` | date | Date of creation of the note |
| `modified` | date | Last date of modification of the note |
| `checksum` | string | SHA-256 checksum of the note file |
1. The format of the generated Markdown links can be customized in the [note format configuration](note-format.md).
2. YAML keys are normalized to lower case.

@ -24,13 +24,39 @@ can generate (depending on the user config):
The second parameter `title` is optional.
### Date helper
### String helpers
The `{{date}}` helper formats the given date for display.
There are a couple of template helpers operating on strings.
#### Concat helper
The `{{concat s1 s2}}` helper concatenates two strings together. For example `{{concat '> ' 'A quote'}}` produces `> A quote`.
#### Substring helper
* The `{{substring s index length}}` helper extracts a portion of the given string. For example:
* `{{substring 'A full quote' 2 4}}` outputs `full`
* `{{substring 'A full quote' -5 5}}` outputs `quote`
### Date helpers
#### Date from natural string helper
You can get a date object from a natural human date (e.g. `tomorrow`, `2 weeks ago`, `2022-03-24`) using the `{{date}}` helper. It is most useful when paired with the `{{format-date}}` helper.
```
{{date "tomorrow"}}
{{format-date (date "last week") "timestamp"}}
```
#### Date formatting helper
The `{{format-date}}` helper formats the given date for display.
Template contexts usually provide a `now` variable which can be used to print the current date.
The default format output by `{{date <variable>}}` looks like `2009-11-17`, but you can choose a different format by providing a second argument, e.g. `{{date now "medium"}}`.
The default format output by `{{format-date <variable>}}` looks like `2009-11-17`, but you can choose a different format by providing a second argument, e.g. `{{format-date now "medium"}}`.
| Format | Output | Notes |
|------------------|----------------------------|--------------------------------------------------|
@ -44,7 +70,7 @@ The default format output by `{{date <variable>}}` looks like `2009-11-17`, but
| `timestamp-unix` | 1258490098 | Number of seconds since January 1, 1970 |
| `elapsed` | 12 years ago | Time elapsed since then in human-friendly format |
If none of the provided formats suit you, you can use a custom format using `strftime`-style placeholders, e.g. `{{date now "%m-%d-%Y"}}`. See `man strftime` for a list of placeholders.
If none of the provided formats suit you, you can use a custom format using `strftime`-style placeholders, e.g. `{{format-date now "%m-%d-%Y"}}`. See `man strftime` for a list of placeholders.
### Slug helper
@ -99,3 +125,17 @@ The `{{style}}` helper is mostly useful when formatting content for the command-
{{#style 'underline'}}Another text{{/style}}
```
### JSON helper
The `{{json}}` helper serializes its argument to a JSON value. This is useful to generate valid JSON objects, for example:
```
{ "title": {{json title}}, "tags": {{json tags}} }
->
{ "title": "A \"quoted\" title", "tags": ["example", "json"] }
```
**Warning**: The template parser trips on `}}}`, so make sure to add an extra space before the third `}`.
You can serialize the whole template context as a JSON object with `{{json .}}`, which is how `zk list --format json` produces its output.

@ -4,7 +4,7 @@
Besides the standard [`fzf` configuration options](https://github.com/junegunn/fzf) documented on its website, `zk` offers additional options you can set in the `[tool]` [configuration section](config.md).
If you wish to customize more of `fzf` behavior, [please post a feature request](https://github.com/mickael-menu/zk/issues).
If you wish to customize more of `fzf` behavior, [please post a feature request](https://github.com/zk-org/zk/issues).
## Preview command
@ -23,3 +23,70 @@ Or, if you prefer to preview more metadata, you can use a nested `zk` command.
[tool]
fzf-preview = "zk list --quiet --format full --limit 1 {-1}"
```
## Line format
With the `fzf-line` setting property, you can provide your own [template](template.md) to customize the format of each `fzf` line. The lines are used by `fzf` for the fuzzy matching, so if you want to search in the full note content, do not forget to add `{{body}}` in your custom template.
The default line template is `{{style "title" title-or-path}} {{style "understate" body}} {{style "understate" (json metadata)}}`.
Here's an example using different colors and showing the list of tags as #hashtags:
```toml
[tool]
fzf-line = "{{style 'blue' rel-path}}{{#each tags}} #{{this}}{{/each}} {{style 'black' body}}"
```
### Template context
The following variables are available in the line template.
| Variable | Type | Description |
|-----------------|----------|--------------------------------------------------------------------|
| `filename` | string | Filename of the note, including its extension |
| `filename-stem` | string | Filename of the note without the file extension |
| `path` | string | File path to the note, relative to the notebook root |
| `abs-path` | string | Absolute file path to the note |
| `rel-path` | string | File path to the note, relative to the current directory |
| `title` | string | Note title |
| `title-or-path` | string | Note title or path if empty |
| `body` | string | All of the note content, minus the heading |
| `raw-content` | string | The full raw content of the note file |
| `word-count` | int | Number of words in the note |
| `tags` | [string] | List of tags found in the note |
| `metadata` | map | YAML frontmatter metadata, e.g. `metadata.description`<sup>1</sup> |
| `created` | date | Date of creation of the note |
| `modified` | date | Last date of modification of the note |
| `checksum` | string | SHA-256 checksum of the note file |
1. YAML keys are normalized to lower case.
## `fzf` options
You can override the default `fzf` options used by `zk` with `fzf-options`. Look at `man fzf` for the list of available options.
```toml
[tool]
fzf-options = "--height 40% --border"
```
Note that this overrides all the default options used by `zk`, you might want to keep some of them:
* `--tiebreak begin` Prefer matches located at the beginning of the line
* `--exact` Look for exact matches instead of fuzzy ones by default
* `--tabstop 4` Length of tab characters
* `--height 100%` Height of the list relative to the terminal window
* `--layout reverse` Display the input field at the top
* `--no-hscroll` Make sure the path and titles are always visible
* `--color hl:-1,hl+:-1` Don't highlight search terms
* `--preview-window wrap` Enable line wrapping in the preview window
## Key bindings
When running `fzf` with `zk edit --interactive`, you can [create a new note with the `Ctrl-E` key binding](note-creation.md#search-or-create-with-a-single-command). This binding is customizable with `fzf-bind-new`. You can also disable it by setting it to an empty string (`""`).
```toml
[tool]
fzf-bind-new = "Ctrl-C"
```

@ -0,0 +1,14 @@
# Setting your default shell
This is *currently* not supported on Windows (that defaults always to `cmd`).
You can customize which shell to use to run aliases and commands either from the [configuration file](config.md) or environment variables. In order of precedence, `zk` will use:
1. `ZK_SHELL` environment variable
2. `shell` configuration property
```toml
[tool]
shell = "/bin/bash"
```
3. `SHELL` environment variable
4. `sh` as fallback

6
go

@ -1,6 +0,0 @@
#!/bin/bash
VERSION=`git describe --tags --match v[0-9]* 2> /dev/null`
BUILD=`git rev-parse --short HEAD`
CGO_ENABLED=1 go $1 -tags "fts5 icu" -ldflags "-X=main.Version=$VERSION -X=main.Build=$BUILD" ${@:2}

@ -1,37 +1,54 @@
module github.com/mickael-menu/zk
module github.com/zk-org/zk
go 1.15
replace github.com/tliron/glsp => github.com/mickael-menu/glsp v0.1.0
go 1.21
require (
github.com/AlecAivazis/survey/v2 v2.2.12
github.com/alecthomas/kong v0.2.16-0.20210209082517-405b2f4fd9a4
github.com/AlecAivazis/survey/v2 v2.3.4
github.com/alecthomas/kong v0.5.0
github.com/aymerick/raymond v2.0.2+incompatible
github.com/fatih/color v1.10.0
github.com/go-testfixtures/testfixtures/v3 v3.4.1
github.com/google/go-cmp v0.5.2
github.com/gosimple/slug v1.9.0
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/fatih/color v1.13.0
github.com/go-testfixtures/testfixtures/v3 v3.6.1
github.com/google/go-cmp v0.5.8
github.com/gosimple/slug v1.12.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lestrrat-go/strftime v1.0.4
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-sqlite3 v1.14.6
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mickael-menu/pretty v0.2.3
github.com/lestrrat-go/strftime v1.0.6
github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-sqlite3 v1.14.22
github.com/mvdan/xurls v1.1.0
github.com/pelletier/go-toml v1.8.1
github.com/pelletier/go-toml v1.9.5
github.com/pkg/errors v0.9.1
github.com/relvacode/iso8601 v1.1.0
github.com/rogpeppe/go-internal v1.6.2 // indirect
github.com/rvflash/elapsed v0.2.0
github.com/schollz/progressbar/v3 v3.7.4
github.com/schollz/progressbar/v3 v3.8.6
github.com/tj/go-naturaldate v1.3.0
github.com/tliron/glsp v0.0.0-20210308190902-c7ec7df19257
github.com/tliron/kutil v0.1.22
github.com/yuin/goldmark v1.3.2
github.com/yuin/goldmark-meta v1.0.0
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 // indirect
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect
golang.org/x/text v0.3.6 // indirect
gopkg.in/djherbis/times.v1 v1.2.0
github.com/tliron/glsp v0.1.1
github.com/tliron/kutil v0.1.59
github.com/yuin/goldmark v1.4.12
github.com/yuin/goldmark-meta v1.1.0
github.com/zk-org/pretty v0.2.4
gopkg.in/djherbis/times.v1 v1.3.0
)
require (
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/petermattis/goid v0.0.0-20220526132513-07eaf5d0b9f4 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/sasha-s/go-deadlock v0.3.1 // indirect
github.com/sourcegraph/jsonrpc2 v0.1.0 // indirect
github.com/zchee/color/v2 v2.0.6 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

818
go.sum

@ -1,263 +1,49 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AlecAivazis/survey/v2 v2.2.7 h1:5NbxkF4RSKmpywYdcRgUmos1o+roJY8duCLZXbVjoig=
github.com/AlecAivazis/survey/v2 v2.2.7/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk=
github.com/AlecAivazis/survey/v2 v2.2.12 h1:5a07y93zA6SZ09gOa9wLVLznF5zTJMQ+pJ3cZK4IuO8=
github.com/AlecAivazis/survey/v2 v2.2.12/go.mod h1:6d4saEvBsfSHXeN1a5OA5m2+HJ2LuVokllnC77pAIKI=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/kong v0.2.16-0.20210209082517-405b2f4fd9a4 h1:TeW3HEkctVgQL2uQiHERcwxZkN9U4WQ+6pKEgOfhCt0=
github.com/alecthomas/kong v0.2.16-0.20210209082517-405b2f4fd9a4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/AlecAivazis/survey/v2 v2.3.4 h1:pchTU9rsLUSvWEl2Aq9Pv3k0IE2fkqtGxazskAMd9Ng=
github.com/AlecAivazis/survey/v2 v2.3.4/go.mod h1:hrV6Y/kQCLhIZXGcriDCUBtB3wnN7156gMXJ3+b23xM=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/alecthomas/kong v0.5.0 h1:u8Kdw+eeml93qtMZ04iei0CFYve/WPcA5IFh+9wSskE=
github.com/alecthomas/kong v0.5.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0=
github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/bmatcuk/doublestar/v4 v4.0.2 h1:X0krlUVAVmtr2cRoTqR8aDMrDqnB36ht8wpWTiQ3jsA=
github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 h1:OGNva6WhsKst5OZf7eZOklDztV3hwtTHovdrLHV+MsA=
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8=
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-testfixtures/testfixtures/v3 v3.4.1 h1:Qz9y0wUOXPHzKhK6C79A/menChtEu/xd0Dn5ngVyMD0=
github.com/go-testfixtures/testfixtures/v3 v3.4.1/go.mod h1:P4L3WxgOsCLbAeUC50qX5rdj1ULZfUMqgCbqah3OH5U=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/go-testfixtures/testfixtures/v3 v3.6.1 h1:n4Fv95Exp0D05G6l6CAZv22Ck1EJK0pa0TfPqE4ncSs=
github.com/go-testfixtures/testfixtures/v3 v3.6.1/go.mod h1:Bsb2MoHAfHnNsPpSwAjtOs102mqDuM+1u3nE2OCi0N0=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-containerregistry v0.4.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs=
github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hokaccha/go-prettyjson v0.0.0-20210113012101-fb4e108d2519/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc=
github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@ -270,7 +56,6 @@ github.com/jackc/pgconn v1.5.0 h1:oFSOilzIZkyg787M1fEmyMfOUUvwj0daqYMfaWwNL4o=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
@ -299,596 +84,187 @@ github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oA
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.11.9/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
github.com/lestrrat-go/strftime v1.0.4 h1:T1Rb9EPkAhgxKqbcMIPguPq8glqXTA1koF8n9BHElA8=
github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g=
github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mickael-menu/glsp v0.1.0 h1:we6mTssWXxGPVeEcTpCW8AOpdCuUXwUZ6Q2UiYVnCOw=
github.com/mickael-menu/glsp v0.1.0/go.mod h1:ouzTGvQteTU4hdsG+32vIx0if7E9CzMa64d7tYJJ91g=
github.com/mickael-menu/pretty v0.2.3 h1:AXi5WcBuWxwQV6iY/GhmCFpaoboQO2SLtzfujrn7dv0=
github.com/mickael-menu/pretty v0.2.3/go.mod h1:gupeWUSWoo3KX7BItIuouLgTqQLlmRylpaPdIK6IqLk=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
github.com/petermattis/goid v0.0.0-20220526132513-07eaf5d0b9f4 h1:COozsq2xbSoZ6kt+/iHGF7eQqiUZr2uxQoAe4OzNkhE=
github.com/petermattis/goid v0.0.0-20220526132513-07eaf5d0b9f4/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
github.com/relvacode/iso8601 v1.1.0 h1:2nV8sp0eOjpoKQ2vD3xSDygsjAx37NHG2UlZiCkDH4I=
github.com/relvacode/iso8601 v1.1.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0=
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rvflash/elapsed v0.2.0 h1:zpMX24KJzo8jJaCI/6690Hb8tCEe5xtjAAjEgba0cns=
github.com/rvflash/elapsed v0.2.0/go.mod h1:sgjohdXO66LHVgIEQpO92eQjDWyZ5twX1ow122ixFGY=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v3 v3.7.4 h1:G2HfclnGJR2HtTOmFkERQcRqo9J20asOFiuD6AnI5EQ=
github.com/schollz/progressbar/v3 v3.7.4/go.mod h1:1H8m5kMPW6q5fyjpDqtBHW1JT22mu2NwHQ1ApuCPh/8=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c=
github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/jsonrpc2 v0.0.0-20210201082850-366fbb520750 h1:j3HKQAXXj5vV3oHyg9pjK3uIM4bidukvv+tR2iJCvFA=
github.com/sourcegraph/jsonrpc2 v0.0.0-20210201082850-366fbb520750/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/sourcegraph/jsonrpc2 v0.1.0 h1:ohJHjZ+PcaLxDUjqk2NC3tIGsVa5bXThe1ZheSXOjuk=
github.com/sourcegraph/jsonrpc2 v0.1.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160 h1:NSWpaDaurcAJY7PkL8Xt0PhZE7qpvbZl5ljd8r6U0bI=
github.com/tj/assert v0.0.0-20190920132354-ee03d75cd160/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/go-naturaldate v1.3.0 h1:OgJIPkR/Jk4bFMBLbxZ8w+QUxwjqSvzd9x+yXocY4RI=
github.com/tj/go-naturaldate v1.3.0/go.mod h1:rpUbjivDKiS1BlfMGc2qUKNZ/yxgthOfmytQs8d8hKk=
github.com/tliron/kutil v0.1.22 h1:VnwZ6YlTao2ISmm9wdv8CnVy5BjROBPpG65qIRc1LtE=
github.com/tliron/kutil v0.1.22/go.mod h1:HkG4xQS2/BHI8EO9WfdOwnlUil7NhY/wmiV7U1uwEYw=
github.com/tliron/yamlkeys v1.3.5/go.mod h1:8kJ1A/1s3p/I3MQUAbtv72dPEyQGoh0ZkQp0UAkABBo=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.2 h1:YjHC5TgyMmHpicTgEqDN0Q96Xo8K6tLXPnmNOHXCgs0=
github.com/yuin/goldmark v1.3.2/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark-meta v1.0.0 h1:ScsatUIT2gFS6azqzLGUjgOnELsBOxMXerM3ogdJhAM=
github.com/yuin/goldmark-meta v1.0.0/go.mod h1:zsNNOrZ4nLuyHAJeLQEZcQat8dm70SmB2kHbls092Gc=
github.com/tliron/glsp v0.1.1 h1:GNNgUX9p1Q9MoPQooJoZ0+WaLL03EkhcKZUYJAtiNqs=
github.com/tliron/glsp v0.1.1/go.mod h1:RVyVKeY3U+Nlc3DRklUiaegNsQyjzNTEool6YWh1v7g=
github.com/tliron/kutil v0.1.59 h1:ReZ/o2EB0TsoTsmGIFNRUnbLyHvuFmSI5TKO8VZgnFk=
github.com/tliron/kutil v0.1.59/go.mod h1:jzWwDRumthKR8qHIquhHrErveSzZeFImNacoKfEjYkM=
github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0=
github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
github.com/zchee/color/v2 v2.0.6 h1:+mD95jTXou3Bi8+ZWn3SOEDts36SNROILd9JId7VI9A=
github.com/zchee/color/v2 v2.0.6/go.mod h1:mtte+U+f1/0xODbqR9J+TfcTjd86MMv6KNmpnC8MiXk=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
github.com/zk-org/pretty v0.2.4 h1:pxf2E61IDO1I9cSNVoESQqK/y129Xophlp6XOH59130=
github.com/zk-org/pretty v0.2.4/go.mod h1:GIPC7TRqGw0VDdumZQ6JP21n0xCsXBd+rnVHeyEjN10=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190614084037-d442b75600c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7 h1:iGu644GcxtEcrInvDsQRCwJjtCIOlT2V7IRt6ah2Whw=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/djherbis/times.v1 v1.2.0 h1:UCvDKl1L/fmBygl2Y7hubXCnY7t4Yj46ZrBFNUipFbM=
gopkg.in/djherbis/times.v1 v1.2.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o=
gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
k8s.io/apiextensions-apiserver v0.20.4/go.mod h1:Hzebis/9c6Io5yzHp24Vg4XOkTp1ViMwKP/6gmpsfA4=
k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0=
k8s.io/code-generator v0.20.4/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg=
k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.6.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

@ -6,10 +6,10 @@ import (
"strings"
"github.com/kballard/go-shellquote"
"github.com/mickael-menu/zk/internal/util/errors"
executil "github.com/mickael-menu/zk/internal/util/exec"
"github.com/mickael-menu/zk/internal/util/opt"
osutil "github.com/mickael-menu/zk/internal/util/os"
"github.com/zk-org/zk/internal/util/errors"
executil "github.com/zk-org/zk/internal/util/exec"
"github.com/zk-org/zk/internal/util/opt"
osutil "github.com/zk-org/zk/internal/util/os"
)
// Editor represents an external editor able to edit the notes.
@ -37,7 +37,7 @@ func (e *Editor) Open(paths ...string) error {
// /dev/tty is restored as stdin, in case the user used a pipe to feed
// initial note content to `zk new`. Without this, Vim doesn't work
// properly in this case.
// See https://github.com/mickael-menu/zk/issues/4
// See https://github.com/zk-org/zk/issues/4
cmd := executil.CommandFromString(e.editor + " " + shellquote.Join(paths...) + " </dev/tty")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout

@ -4,8 +4,8 @@ import (
"os"
"testing"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/util/opt"
"github.com/zk-org/zk/internal/util/test/assert"
)
func TestEditorUsesZkEditorFirst(t *testing.T) {

@ -1,12 +1,11 @@
package fs
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/mickael-menu/zk/internal/util"
"github.com/zk-org/zk/internal/util"
)
// FileStorage implements the port core.FileStorage.
@ -79,11 +78,7 @@ func (fs *FileStorage) FileExists(path string) (bool, error) {
func (fs *FileStorage) DirExists(path string) (bool, error) {
fi, err := fs.fileInfo(path)
if err != nil {
return false, err
} else {
return fi != nil && (*fi).Mode().IsDir(), nil
}
return !os.IsNotExist(err) && fi != nil && (*fi).Mode().IsDir(), nil
}
func (fs *FileStorage) fileInfo(path string) (*os.FileInfo, error) {
@ -118,7 +113,7 @@ func (fs *FileStorage) IsDescendantOf(dir string, path string) (bool, error) {
}
func (fs *FileStorage) Read(path string) ([]byte, error) {
return ioutil.ReadFile(path)
return os.ReadFile(path)
}
func (fs *FileStorage) Write(path string, content []byte) error {

@ -8,9 +8,10 @@ import (
"strings"
"sync"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
stringsutil "github.com/mickael-menu/zk/internal/util/strings"
"github.com/kballard/go-shellquote"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/opt"
stringsutil "github.com/zk-org/zk/internal/util/strings"
)
// ErrCancelled is returned when the user cancelled fzf.
@ -26,6 +27,8 @@ var (
type Opts struct {
// Preview command executed by fzf when hovering a line.
PreviewCmd opt.String
// Optionally provide additional arguments, taken from the config `fzf-options` property.
Options opt.String
// Amount of space between two non-empty fields.
Padding int
// Delimiter used by fzf between fields.
@ -72,21 +75,18 @@ func New(opts Opts) (*Fzf, error) {
opts.Delimiter = "\x01"
}
// Hard-coded fzf options that are required by zk.
args := []string{
"--delimiter", opts.Delimiter,
"--tiebreak", "begin",
"--ansi",
"--exact",
"--tabstop", "4",
"--height", "100%",
"--layout", "reverse",
//"--info", "inline",
// Make sure the path and titles are always visible
"--no-hscroll",
// Don't highlight search terms
"--color", "hl:-1,hl+:-1",
"--preview-window", "wrap",
"--delimiter", opts.Delimiter,
}
// Additional options.
additionalArgs, err := shellquote.Split(opts.Options.String())
if err != nil {
return nil, errors.Wrapf(err, "can't split the fzf-options: %s", opts.Options.String())
}
args = append(args, additionalArgs...)
header := ""
binds := []string{}

@ -4,18 +4,21 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/mickael-menu/zk/internal/adapter/term"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/opt"
stringsutil "github.com/mickael-menu/zk/internal/util/strings"
"github.com/zk-org/zk/internal/adapter/term"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/opt"
stringsutil "github.com/zk-org/zk/internal/util/strings"
)
// NoteFilter uses fzf to filter interactively a set of notes.
type NoteFilter struct {
opts NoteFilterOpts
fs core.FileStorage
terminal *term.Terminal
opts NoteFilterOpts
fs core.FileStorage
terminal *term.Terminal
templateLoader core.TemplateLoader
}
// NoteFilterOpts holds the configuration for the fzf notes filtering.
@ -28,6 +31,12 @@ type NoteFilterOpts struct {
Interactive bool
// Indicates whether fzf is opened for every query, even if empty.
AlwaysFilter bool
// Format for a single line, taken from the config `fzf-line` property.
LineTemplate opt.String
// Optionally provide additional arguments, taken from the config `fzf-options` property.
FzfOptions opt.String
// Key binding for the new action.
NewBinding opt.String
// Preview command to run when selecting a note.
PreviewCmd opt.String
// When non null, a "create new note from query" binding will be added to
@ -37,11 +46,12 @@ type NoteFilterOpts struct {
NotebookDir string
}
func NewNoteFilter(opts NoteFilterOpts, fs core.FileStorage, terminal *term.Terminal) *NoteFilter {
func NewNoteFilter(opts NoteFilterOpts, fs core.FileStorage, terminal *term.Terminal, templateLoader core.TemplateLoader) *NoteFilter {
return &NoteFilter{
opts: opts,
fs: fs,
terminal: terminal,
opts: opts,
fs: fs,
terminal: terminal,
templateLoader: templateLoader,
}
}
@ -55,9 +65,19 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
return notes, nil
}
lineTemplate, err := f.templateLoader.LoadTemplate(f.opts.LineTemplate.OrString(defaultLineTemplate).String())
if err != nil {
return selectedNotes, err
}
for _, note := range notes {
absPaths = append(absPaths, filepath.Join(f.opts.NotebookDir, note.Path))
relPaths = append(relPaths, note.Path)
absPath := filepath.Join(f.opts.NotebookDir, note.Path)
absPaths = append(absPaths, absPath)
if relPath, err := f.fs.Rel(absPath); err == nil {
relPaths = append(relPaths, relPath)
} else {
relPaths = append(relPaths, note.Path)
}
}
zkBin, err := os.Executable()
@ -73,16 +93,20 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
suffix = " in " + dir.Name + "/"
}
bindings = append(bindings, Binding{
Keys: "Ctrl-N",
Description: "create a note with the query as title" + suffix,
Action: fmt.Sprintf("abort+execute(%s new %s --title {q} < /dev/tty > /dev/tty)", zkBin, dir.Path),
})
newBinding := f.opts.NewBinding.OrString("Ctrl-E").String()
if newBinding != "" {
bindings = append(bindings, Binding{
Keys: newBinding,
Description: "create a note with the query as title" + suffix,
Action: fmt.Sprintf(`abort+execute("%s" new "%s" --title {q} < /dev/tty > /dev/tty)`, zkBin, dir.Path),
})
}
}
previewCmd := f.opts.PreviewCmd.OrString("cat {-1}").Unwrap()
fzf, err := New(Opts{
Options: f.opts.FzfOptions.OrString(defaultOptions),
PreviewCmd: opt.NewNotEmptyString(previewCmd),
Padding: 2,
Bindings: bindings,
@ -92,15 +116,36 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
}
for i, note := range notes {
title := note.Title
if title == "" {
title = relPaths[i]
context := lineRenderContext{
Filename: note.Filename(),
FilenameStem: note.FilenameStem(),
Path: note.Path,
AbsPath: absPaths[i],
RelPath: relPaths[i],
Title: note.Title,
TitleOrPath: note.Title,
Body: stringsutil.JoinLines(note.Body),
RawContent: stringsutil.JoinLines(note.RawContent),
WordCount: note.WordCount,
Tags: note.Tags,
Metadata: note.Metadata,
Created: note.Created,
Modified: note.Modified,
Checksum: note.Checksum,
}
fzf.Add([]string{
f.terminal.MustStyle(title, core.StyleYellow),
f.terminal.MustStyle(stringsutil.JoinLines(note.Body), core.StyleUnderstate),
f.terminal.MustStyle(absPaths[i], core.StyleUnderstate),
})
if context.TitleOrPath == "" {
context.TitleOrPath = note.Path
}
line, err := lineTemplate.Render(context)
if err != nil {
return selectedNotes, err
}
// The absolute path is appended at the end of the line to be used in
// the preview command.
absPathField := f.terminal.MustStyle(context.AbsPath, core.StyleUnderstate)
fzf.Add([]string{line, absPathField})
}
selection, err := fzf.Selection()
@ -119,3 +164,35 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
return selectedNotes, nil
}
var defaultLineTemplate = `{{style "title" title-or-path}} {{style "understate" body}} {{style "understate" (json metadata)}}`
// defaultOptions are the default fzf options used when filtering notes.
var defaultOptions = strings.Join([]string{
"--tiebreak begin", // Prefer matches located at the beginning of the line
"--exact", // Look for exact matches instead of fuzzy ones by default
"--tabstop 4", // Length of tab characters
"--height 100%", // Height of the list relative to the terminal window
"--layout reverse", // Display the input field at the top
"--no-hscroll", // Make sure the path and titles are always visible
"--color hl:-1,hl+:-1", // Don't highlight search terms
"--preview-window wrap", // Enable line wrapping in the preview window
}, " ")
type lineRenderContext struct {
Filename string
FilenameStem string `handlebars:"filename-stem"`
Path string
AbsPath string `handlebars:"abs-path"`
RelPath string `handlebars:"rel-path"`
Title string
TitleOrPath string `handlebars:"title-or-path"`
Body string
RawContent string `handlebars:"raw-content"`
WordCount int `handlebars:"word-count"`
Tags []string
Metadata map[string]interface{}
Created time.Time
Modified time.Time
Checksum string
}

@ -6,20 +6,23 @@ import (
"path/filepath"
"github.com/aymerick/raymond"
"github.com/mickael-menu/zk/internal/adapter/handlebars/helpers"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/paths"
"github.com/zk-org/zk/internal/adapter/handlebars/helpers"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/paths"
)
func Init(supportsUTF8 bool, logger util.Logger) {
helpers.RegisterConcat()
helpers.RegisterDate(logger)
helpers.RegisterFormatDate(logger)
helpers.RegisterJoin()
helpers.RegisterJSON(logger)
helpers.RegisterList(supportsUTF8)
helpers.RegisterPrepend(logger)
helpers.RegisterShell(logger)
helpers.RegisterSubstring()
}
// Template renders a parsed handlebars template.

@ -7,12 +7,12 @@ import (
"testing"
"time"
"github.com/mickael-menu/zk/internal/adapter/handlebars/helpers"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/fixtures"
"github.com/mickael-menu/zk/internal/util/paths"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/adapter/handlebars/helpers"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/fixtures"
"github.com/zk-org/zk/internal/util/paths"
"github.com/zk-org/zk/internal/util/test/assert"
)
func init() {
@ -137,6 +137,14 @@ func TestConcatHelper(t *testing.T) {
testString(t, "{{concat '> ' 'A quote'}}", nil, "> A quote")
}
func TestSubstringHelper(t *testing.T) {
testString(t, "{{substring '' 2 4}}", nil, "")
testString(t, "{{substring 'A full quote' 2 4}}", nil, "full")
testString(t, "{{substring 'A full quote' 40 4}}", nil, "")
testString(t, "{{substring 'A full quote' -5 5}}", nil, "quote")
testString(t, "{{substring 'A full quote' -5 6}}", nil, "quote")
}
func TestJoinHelper(t *testing.T) {
test := func(items []string, expected string) {
context := map[string]interface{}{"items": items}
@ -149,6 +157,28 @@ func TestJoinHelper(t *testing.T) {
test([]string{"Item 1", "Item 2", "Item 3"}, "Item 1-Item 2-Item 3")
}
type testJSONObject struct {
Foo string
Missing string `json:"missing,omitempty"`
List []string `json:"stringList"`
}
func TestJSONHelper(t *testing.T) {
test := func(value interface{}, expected string) {
context := map[string]interface{}{"value": value}
testString(t, "{{json value}}", context, expected)
}
test(`foo"bar"`, `"foo\"bar\""`)
test([]string{"foo", "bar"}, `["foo","bar"]`)
test(map[string]string{"foo": "bar"}, `{"foo":"bar"}`)
test(map[string]string{"foo": "bar"}, `{"foo":"bar"}`)
test(testJSONObject{
Foo: "baz",
List: []string{"foo", "bar"},
}, `{"Foo":"baz","stringList":["foo","bar"]}`)
}
func TestPrependHelper(t *testing.T) {
// inline
testString(t, "{{prepend '> ' 'A quote'}}", nil, "> A quote")
@ -196,19 +226,131 @@ func TestSlugHelper(t *testing.T) {
)
}
func TestFormatDateHelper(t *testing.T) {
context := map[string]interface{}{"now": time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)}
testString(t, "{{format-date now}}", context, "2009-11-17")
testString(t, "{{format-date now 'short'}}", context, "11/17/2009")
testString(t, "{{format-date now 'medium'}}", context, "Nov 17, 2009")
testString(t, "{{format-date now 'long'}}", context, "November 17, 2009")
testString(t, "{{format-date now 'full'}}", context, "Tuesday, November 17, 2009")
testString(t, "{{format-date now 'year'}}", context, "2009")
testString(t, "{{format-date now 'time'}}", context, "20:34")
testString(t, "{{format-date now 'timestamp'}}", context, "200911172034")
testString(t, "{{format-date now 'timestamp-unix'}}", context, "1258490098")
testString(t, "{{format-date now 'cust: %Y-%m'}}", context, "cust: 2009-11")
}
func TestFormatDateHelperElapsedYear(t *testing.T) {
year := time.Now().UTC().Year() - 14
context := map[string]interface{}{"now": time.Date(year, 11, 17, 20, 34, 58, 651387237, time.UTC)}
testString(t, "{{format-date now 'elapsed'}}", context, "14 years ago")
}
func TestFormatDateHelperElapsedViaTimeMultiplication(t *testing.T) {
// test for time being provided in via multiplications on seconds, minutes
// and hours, as expected by github.com/rvflash/elapsed
cases := []struct {
elapsed time.Duration
want string
}{
{
elapsed: -12 * time.Second,
want: "not yet",
},
{
elapsed: time.Second,
want: "just now",
},
{
elapsed: 59 * time.Second,
want: "just now",
},
{
elapsed: 60 * time.Second,
want: "1 minute ago",
},
{
elapsed: 1 * time.Minute,
want: "1 minute ago",
},
{
elapsed: 2 * time.Minute,
want: "2 minutes ago",
},
{
elapsed: 62 * time.Minute,
want: "1 hour ago",
},
{
elapsed: time.Hour,
want: "1 hour ago",
},
{
elapsed: 2 * time.Hour,
want: "2 hours ago",
},
{
elapsed: 24 * time.Hour,
want: "yesterday",
},
{
elapsed: 4 * 24 * time.Hour,
want: "4 days ago",
},
{
elapsed: 7 * 24 * time.Hour,
want: "1 week ago",
},
{
elapsed: 8 * 24 * time.Hour,
want: "2 weeks ago",
},
{
elapsed: 18 * 24 * time.Hour,
want: "3 weeks ago",
},
{
elapsed: 30 * 24 * time.Hour,
want: "1 month ago",
},
{
elapsed: 31 * 24 * time.Hour,
want: "2 months ago",
},
{
elapsed: 60 * 24 * time.Hour,
want: "2 months ago",
},
{
elapsed: 61 * 24 * time.Hour,
want: "3 months ago",
},
{
elapsed: 330 * 24 * time.Hour,
want: "11 months ago",
},
{
elapsed: 331 * 24 * time.Hour,
want: "1 year ago",
},
{
elapsed: 366 * 24 * time.Hour,
want: "2 years ago",
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d_%s", i, tc.want), func(t *testing.T) {
templateContext := map[string]interface{}{"now": time.Now().Add(-tc.elapsed)}
testString(t, "{{format-date now 'elapsed'}}", templateContext, tc.want)
})
}
}
func TestDateHelper(t *testing.T) {
context := map[string]interface{}{"now": time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)}
testString(t, "{{date now}}", context, "2009-11-17")
testString(t, "{{date now 'short'}}", context, "11/17/2009")
testString(t, "{{date now 'medium'}}", context, "Nov 17, 2009")
testString(t, "{{date now 'long'}}", context, "November 17, 2009")
testString(t, "{{date now 'full'}}", context, "Tuesday, November 17, 2009")
testString(t, "{{date now 'year'}}", context, "2009")
testString(t, "{{date now 'time'}}", context, "20:34")
testString(t, "{{date now 'timestamp'}}", context, "200911172034")
testString(t, "{{date now 'timestamp-unix'}}", context, "1258490098")
testString(t, "{{date now 'cust: %Y-%m'}}", context, "cust: 2009-11")
testString(t, "{{date now 'elapsed'}}", context, "12 years ago")
testString(t, "{{format-date (date \"2009-11-17T20:34:58\") 'timestamp'}}", context, "200911172034")
}
func TestShellHelper(t *testing.T) {
@ -251,8 +393,8 @@ func testLoader(opts LoaderOpts) *Loader {
loader.RegisterHelper("style", helpers.NewStyleHelper(opts.Styler, &util.NullLogger))
loader.RegisterHelper("slug", helpers.NewSlugHelper("en", &util.NullLogger))
formatter := func(path, title string) (string, error) {
return path + " - " + title, nil
formatter := func(context core.LinkFormatterContext) (string, error) {
return context.Path + " - " + context.Title, nil
}
loader.RegisterHelper("format-link", helpers.NewLinkHelper(formatter, &util.NullLogger))

@ -1,24 +1,51 @@
package helpers
import (
"os"
"time"
"github.com/aymerick/raymond"
"github.com/lestrrat-go/strftime"
"github.com/mickael-menu/zk/internal/util"
"github.com/zk-org/zk/internal/util"
dateutil "github.com/zk-org/zk/internal/util/date"
"github.com/pkg/errors"
"github.com/rvflash/elapsed"
)
// RegisterDate registers the {{date}} template helpers which format a given date.
// RegisterDate registers the {{date}} template helper to use the `naturaldate` package to generate time.Time based on language strings.
// This can be used in combination with the `format-date` helper to generate dates in the user's language.
// {{format-date (date "last week") "timestamp"}}
func RegisterDate(logger util.Logger) {
raymond.RegisterHelper("date", func(arg1 interface{}, arg2 interface{}) time.Time {
var t time.Time
switch date := arg1.(type) {
case string:
t, err := dateutil.TimeFromNatural(date)
if err != nil {
logger.Err(errors.Wrap(err, "the {{date}} template helper failed to parse the date"))
}
return t
case time.Time:
logger.Println("the {{date}} template helper was renamed to {{format-date}}, please update your configuration")
os.Exit(1)
return t
default:
logger.Println("the {{date}} template helper expects a natural human date as a string for its only argument")
return t
}
})
}
// RegisterFormatDate registers the {{format-date}} template helpers which format a given date.
//
// It supports various styles: short, medium, long, full, year, time,
// timestamp, timestamp-unix or a custom strftime format.
//
// {{date now}} -> 2009-11-17
// {{date now "medium"}} -> Nov 17, 2009
// {{date now "%Y-%m"}} -> 2009-11
func RegisterDate(logger util.Logger) {
raymond.RegisterHelper("date", func(date time.Time, arg interface{}) string {
// {{format-date now}} -> 2009-11-17
// {{format-date now "medium"}} -> Nov 17, 2009
// {{format-date now "%Y-%m"}} -> 2009-11
func RegisterFormatDate(logger util.Logger) {
raymond.RegisterHelper("format-date", func(date time.Time, arg interface{}) string {
format := "%Y-%m-%d"
if arg, ok := arg.(string); ok {
@ -31,7 +58,7 @@ func RegisterDate(logger util.Logger) {
} else {
res, err := strftime.Format(format, date, strftime.WithUnixSeconds('s'))
if err != nil {
logger.Printf("the {{date}} template helper failed to format the date: %v", err)
logger.Printf("the {{format-date}} template helper failed to format the date: %v", err)
return ""
}
return res

@ -0,0 +1,22 @@
package helpers
import (
"encoding/json"
"github.com/aymerick/raymond"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
)
// RegisterJSON registers a {{json}} template helper which serializes its
// parameter to a JSON value.
func RegisterJSON(logger util.Logger) {
raymond.RegisterHelper("json", func(arg interface{}) string {
jsonBytes, err := json.Marshal(arg)
if err != nil {
logger.Err(errors.Wrapf(err, "%v: not a serializable argument for {{json}}", arg))
return ""
}
return string(jsonBytes)
})
}

@ -1,20 +1,26 @@
package helpers
import (
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
)
// NewLinkHelper creates a new template helper to generate an internal link
// using a LinkFormatter.
//
// {{link "path/to/note.md" "An interesting subject"}} -> (depends on the LinkFormatter)
// {{format-link "path/to/note.md" "An interesting subject"}} -> (depends on the LinkFormatter)
// [[path/to/note]]
// [An interesting subject](path/to/note)
func NewLinkHelper(formatter core.LinkFormatter, logger util.Logger) interface{} {
return func(path string, opt interface{}) string {
title, _ := opt.(string)
link, err := formatter(path, title)
link, err := formatter(core.LinkFormatterContext{
Path: path,
RelPath: path,
AbsPath: path,
Title: title,
Metadata: map[string]interface{}{},
})
if err != nil {
logger.Err(err)
return ""

@ -2,8 +2,8 @@ package helpers
import (
"github.com/aymerick/raymond"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/strings"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/strings"
)
// RegisterPrepend registers a {{prepend}} template helper which prepend a

@ -4,8 +4,8 @@ import (
"strings"
"github.com/aymerick/raymond"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/exec"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/exec"
)
// RegisterShell registers the {{sh}} template helper, which runs shell commands.

@ -3,7 +3,7 @@ package helpers
import (
"github.com/aymerick/raymond"
"github.com/gosimple/slug"
"github.com/mickael-menu/zk/internal/util"
"github.com/zk-org/zk/internal/util"
)
// NewSlugHelper creates a new template helper to slugify text.

@ -4,8 +4,8 @@ import (
"strings"
"github.com/aymerick/raymond"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
)
// NewStyleHelper creates a new template helper which stylizes the text input

@ -0,0 +1,31 @@
package helpers
import (
"github.com/aymerick/raymond"
)
// RegisterSubstring registers a {{substring}} template helper which extracts a
// substring given a starting index and a length.
//
// {{substring 'A full quote' 2 4}} -> "full"
// {{substring 'A full quote' -5 5}} -> "quote"
//
func RegisterSubstring() {
raymond.RegisterHelper("substring", func(str string, index int, length int) string {
if index < 0 {
index = len(str) + index
}
if index >= len(str) {
return ""
}
end := min(index+length, len(str))
return str[index:end]
})
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

@ -0,0 +1,27 @@
package lsp
import (
"fmt"
"github.com/zk-org/zk/internal/core"
)
const cmdIndex = "zk.index"
func executeCommandIndex(notebook *core.Notebook, args []interface{}) (interface{}, error) {
opts := core.NoteIndexOpts{}
if len(args) == 2 {
options, ok := args[1].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("zk.index expects a dictionary of options as second argument, got: %v", args[1])
}
if forceOption, ok := options["force"]; ok {
opts.Force = toBool(forceOption)
}
if verboseOption, ok := options["verbose"]; ok {
opts.Verbose = toBool(verboseOption)
}
}
return notebook.Index(opts)
}

@ -0,0 +1,64 @@
package lsp
import (
"fmt"
"path/filepath"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
const cmdLink = "zk.link"
type cmdLinkOpts struct {
Path *string `json:"path"`
Location *protocol.Location `json:"location"`
Title *string `json:"title"`
}
func executeCommandLink(notebook *core.Notebook, documents *documentStore, context *glsp.Context, args []interface{}) (interface{}, error) {
var opts cmdLinkOpts
if len(args) > 1 {
arg, ok := args[1].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdLink, args[1])
}
err := unmarshalJSON(arg, &opts)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s args, got: %v", cmdLink, arg)
}
}
if opts.Path == nil {
return nil, errors.New("'path' not provided")
}
note, err := notebook.FindByHref(*opts.Path, false)
if err != nil {
return nil, err
}
if note == nil {
return nil, errors.New("Requested note to link to not found!")
}
info := &linkInfo{
note: note,
location: opts.Location,
title: opts.Title,
}
err = linkNote(notebook, documents, context, info)
if err != nil {
return nil, err
}
return map[string]interface{}{
"path": filepath.Join(notebook.Path, note.Path),
}, nil
}

@ -0,0 +1,162 @@
package lsp
import (
"fmt"
"path/filepath"
"time"
"github.com/zk-org/zk/internal/cli"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
strutil "github.com/zk-org/zk/internal/util/strings"
)
const cmdList = "zk.list"
type cmdListOpts struct {
Select []string `json:"select"`
cli.Filtering
}
func executeCommandList(logger util.Logger, notebook *core.Notebook, args []interface{}) (interface{}, error) {
var opts cmdListOpts
if len(args) > 1 {
arg, ok := args[1].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdTagList, args[1])
}
err := unmarshalJSON(arg, &opts)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s args, got: %v", cmdTagList, arg)
}
}
if len(opts.Select) == 0 {
return nil, fmt.Errorf("%s expects a `select` option with the list of fields to return", cmdTagList)
}
var selection = newListSelection(opts.Select)
findOpts, err := opts.NewNoteFindOpts(notebook)
if err != nil {
return nil, err
}
notes, err := notebook.FindNotes(findOpts)
if err != nil {
return nil, err
}
listNotes := []listNote{}
for _, note := range notes {
listNotes = append(listNotes, newListNote(note, selection, notebook.Path))
}
return listNotes, nil
}
type listSelection struct {
Filename bool
FilenameStem bool
Path bool
AbsPath bool
Title bool
Lead bool
Body bool
Snippets bool
RawContent bool
WordCount bool
Tags bool
Metadata bool
Created bool
Modified bool
Checksum bool
}
func newListSelection(fields []string) listSelection {
return listSelection{
Filename: strutil.Contains(fields, "filename"),
FilenameStem: strutil.Contains(fields, "filenameStem"),
Path: strutil.Contains(fields, "path"),
AbsPath: strutil.Contains(fields, "absPath"),
Title: strutil.Contains(fields, "title"),
Lead: strutil.Contains(fields, "lead"),
Body: strutil.Contains(fields, "body"),
Snippets: strutil.Contains(fields, "snippets"),
RawContent: strutil.Contains(fields, "rawContent"),
WordCount: strutil.Contains(fields, "wordCount"),
Tags: strutil.Contains(fields, "tags"),
Metadata: strutil.Contains(fields, "metadata"),
Created: strutil.Contains(fields, "created"),
Modified: strutil.Contains(fields, "modified"),
Checksum: strutil.Contains(fields, "checksum"),
}
}
func newListNote(note core.ContextualNote, selection listSelection, basePath string) listNote {
var res listNote
if selection.Filename {
res.Filename = note.Filename()
}
if selection.FilenameStem {
res.FilenameStem = note.FilenameStem()
}
if selection.Path {
res.Path = note.Path
}
if selection.AbsPath {
res.AbsPath = filepath.Join(basePath, note.Path)
}
if selection.Title {
res.Title = note.Title
}
if selection.Lead {
res.Lead = note.Lead
}
if selection.Body {
res.Body = note.Body
}
if selection.Snippets {
res.Snippets = note.Snippets
}
if selection.RawContent {
res.RawContent = note.RawContent
}
if selection.WordCount {
res.WordCount = note.WordCount
}
if selection.Tags {
res.Tags = note.Tags
}
if selection.Metadata {
res.Metadata = note.Metadata
}
if selection.Created {
res.Created = &note.Created
}
if selection.Modified {
res.Modified = &note.Modified
}
if selection.Checksum {
res.Checksum = note.Checksum
}
return res
}
type listNote struct {
Filename string `json:"filename,omitempty"`
FilenameStem string `json:"filenameStem,omitempty"`
Path string `json:"path,omitempty"`
AbsPath string `json:"absPath,omitempty"`
Title string `json:"title,omitempty"`
Lead string `json:"lead,omitempty"`
Body string `json:"body,omitempty"`
Snippets []string `json:"snippets,omitempty"`
RawContent string `json:"rawContent,omitempty"`
WordCount int `json:"wordCount,omitempty"`
Tags []string `json:"tags,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Created *time.Time `json:"created,omitempty"`
Modified *time.Time `json:"modified,omitempty"`
Checksum string `json:"checksum,omitempty"`
}

@ -0,0 +1,112 @@
package lsp
import (
"fmt"
"path/filepath"
"github.com/zk-org/zk/internal/core"
dateutil "github.com/zk-org/zk/internal/util/date"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/opt"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
const cmdNew = "zk.new"
type cmdNewOpts struct {
Title string `json:"title"`
Content string `json:"content"`
Dir string `json:"dir"`
Group string `json:"group"`
Template string `json:"template"`
Extra map[string]string `json:"extra"`
Date string `json:"date"`
Edit jsonBoolean `json:"edit"`
DryRun jsonBoolean `json:"dryRun"`
InsertLinkAtLocation *protocol.Location `json:"insertLinkAtLocation"`
InsertContentAtLocation *protocol.Location `json:"insertContentAtLocation"`
}
func executeCommandNew(notebook *core.Notebook, documents *documentStore, context *glsp.Context, args []interface{}) (interface{}, error) {
var opts cmdNewOpts
if len(args) > 1 {
arg, ok := args[1].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdNew, args[1])
}
err := unmarshalJSON(arg, &opts)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s args, got: %v", cmdNew, arg)
}
}
date, err := dateutil.TimeFromNatural(opts.Date)
if err != nil {
return nil, errors.Wrapf(err, "%s, failed to parse the `date` option", opts.Date)
}
note, err := notebook.NewNote(core.NewNoteOpts{
Title: opt.NewNotEmptyString(opts.Title),
Content: opts.Content,
Directory: opt.NewNotEmptyString(opts.Dir),
Group: opt.NewNotEmptyString(opts.Group),
Template: opt.NewNotEmptyString(opts.Template),
Extra: opts.Extra,
DryRun: bool(opts.DryRun),
Date: date,
})
if err != nil {
var noteExists core.ErrNoteExists
if !errors.As(err, &noteExists) {
return nil, err
}
note, err = notebook.FindNote(core.NoteFindOpts{
IncludeHrefs: []string{noteExists.Name},
})
if err != nil {
return nil, err
}
}
if note == nil {
return nil, errors.New("zk.new could not generate a new note")
}
if opts.InsertContentAtLocation != nil {
go context.Call(protocol.ServerWorkspaceApplyEdit, protocol.ApplyWorkspaceEditParams{
Edit: protocol.WorkspaceEdit{
Changes: map[string][]protocol.TextEdit{
opts.InsertContentAtLocation.URI: {{Range: opts.InsertContentAtLocation.Range, NewText: note.RawContent}},
},
},
}, nil)
}
if !opts.DryRun && opts.InsertLinkAtLocation != nil {
minNote := note.AsMinimalNote()
info := &linkInfo{
note: &minNote,
location: opts.InsertLinkAtLocation,
title: &opts.Title,
}
err := linkNote(notebook, documents, context, info)
if err != nil {
return nil, err
}
}
absPath := filepath.Join(notebook.Path, note.Path)
if !opts.DryRun && opts.Edit {
go context.Call(protocol.ServerWindowShowDocument, protocol.ShowDocumentParams{
URI: pathToURI(absPath),
TakeFocus: boolPtr(true),
}, nil)
}
return map[string]interface{}{
"path": absPath,
"content": note.RawContent,
}, nil
}

@ -0,0 +1,39 @@
package lsp
import (
"fmt"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
)
const cmdTagList = "zk.tag.list"
type cmdTagListOpts struct {
Sort []string `json:"sort"`
}
func executeCommandTagList(logger util.Logger, notebook *core.Notebook, args []interface{}) (interface{}, error) {
var opts cmdTagListOpts
if len(args) > 1 {
arg, ok := args[1].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("%s expects a dictionary of options as second argument, got: %v", cmdTagList, args[1])
}
err := unmarshalJSON(arg, &opts)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s args, got: %v", cmdTagList, arg)
}
}
var sorters []core.CollectionSorter
var err error
if opts.Sort != nil {
sorters, err = core.CollectionSortersFromStrings(opts.Sort)
if err != nil {
return nil, err
}
}
return notebook.FindCollections(core.CollectionKindTag, sorters)
}

@ -0,0 +1,66 @@
package lsp
import (
"path/filepath"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/paths"
)
// completionTemplates holds templates to render the various elements of an LSP
// completion item.
type completionTemplates struct {
Label core.Template
FilterText core.Template
Detail core.Template
}
func newCompletionTemplates(loader core.TemplateLoader, templates core.LSPCompletionTemplates) (result completionTemplates, err error) {
if !templates.Label.IsNull() {
result.Label, err = loader.LoadTemplate(*templates.Label.Value)
}
if !templates.FilterText.IsNull() {
result.FilterText, err = loader.LoadTemplate(*templates.FilterText.Value)
}
if !templates.Detail.IsNull() {
result.Detail, err = loader.LoadTemplate(*templates.Detail.Value)
}
return
}
type completionItemRenderContext struct {
ID int64
Filename string
FilenameStem string `handlebars:"filename-stem"`
Path string
AbsPath string `handlebars:"abs-path"`
RelPath string `handlebars:"rel-path"`
Title string
TitleOrPath string `handlebars:"title-or-path"`
Metadata map[string]interface{}
}
func newCompletionItemRenderContext(note core.MinimalNote, notebookDir string, currentDir string) (completionItemRenderContext, error) {
absPath := filepath.Join(notebookDir, note.Path)
relPath, err := filepath.Rel(currentDir, absPath)
if err != nil {
return completionItemRenderContext{}, err
}
context := completionItemRenderContext{
ID: int64(note.ID),
Filename: filepath.Base(note.Path),
FilenameStem: paths.FilenameStem(note.Path),
Path: note.Path,
AbsPath: absPath,
RelPath: relPath,
Title: note.Title,
TitleOrPath: note.Title,
Metadata: note.Metadata,
}
if context.TitleOrPath == "" {
context.TitleOrPath = note.Path
}
return context, nil
}

@ -2,19 +2,83 @@ package lsp
import (
"net/url"
"path/filepath"
"regexp"
"strings"
"unicode/utf16"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
"github.com/tliron/kutil/logging"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
strutil "github.com/zk-org/zk/internal/util/strings"
)
// documentStore holds opened documents.
type documentStore struct {
documents map[string]*document
fs core.FileStorage
logger util.Logger
}
func newDocumentStore(fs core.FileStorage, logger util.Logger) *documentStore {
return &documentStore{
documents: map[string]*document{},
fs: fs,
logger: logger,
}
}
func (s *documentStore) DidOpen(params protocol.DidOpenTextDocumentParams, notify glsp.NotifyFunc) (*document, error) {
langID := params.TextDocument.LanguageID
if langID != "markdown" && langID != "vimwiki" && langID != "pandoc" {
return nil, nil
}
uri := params.TextDocument.URI
path, err := s.normalizePath(uri)
if err != nil {
return nil, err
}
doc := &document{
URI: uri,
Path: path,
Content: params.TextDocument.Text,
}
s.documents[path] = doc
return doc, nil
}
func (s *documentStore) Close(uri protocol.DocumentUri) {
delete(s.documents, uri)
}
func (s *documentStore) Get(pathOrURI string) (*document, bool) {
path, err := s.normalizePath(pathOrURI)
if err != nil {
s.logger.Err(err)
return nil, false
}
d, ok := s.documents[path]
return d, ok
}
func (s *documentStore) normalizePath(pathOrUri string) (string, error) {
path, err := uriToPath(pathOrUri)
if err != nil {
return "", errors.Wrapf(err, "unable to parse URI: %s", pathOrUri)
}
return s.fs.Canonical(path), nil
}
// document represents an opened file.
type document struct {
Path string
Content string
Log logging.Logger
lines []string
URI protocol.DocumentUri
Path string
NeedsRefreshDiagnostics bool
Content string
lines []string
}
// ApplyChanges updates the content of the document from LSP textDocument/didChange events.
@ -32,25 +96,19 @@ func (d *document) ApplyChanges(changes []interface{}) {
d.lines = nil
}
var nonEmptyString = regexp.MustCompile(`\S+`)
// WordAt returns the word found at the given location.
// Credit https://github.com/aca/neuron-language-server/blob/450a7cff71c14e291ee85ff8a0614fa9d4dd5145/utils.go#L13
func (d *document) WordAt(pos protocol.Position) string {
line, ok := d.GetLine(int(pos.Line))
if !ok {
return ""
}
return strutil.WordAt(line, int(pos.Character))
}
charIdx := int(pos.Character)
wordIdxs := nonEmptyString.FindAllStringIndex(line, -1)
for _, wordIdx := range wordIdxs {
if wordIdx[0] <= charIdx && charIdx <= wordIdx[1] {
return line[wordIdx[0]:wordIdx[1]]
}
}
return ""
// ContentAtRange returns the document text at given range.
func (d *document) ContentAtRange(rng protocol.Range) string {
startIndex, endIndex := rng.IndexesIn(d.Content)
return d.Content[startIndex:endIndex]
}
// GetLine returns the line at the given index.
@ -74,34 +132,46 @@ func (d *document) GetLines() []string {
// LookBehind returns the n characters before the given position, on the same line.
func (d *document) LookBehind(pos protocol.Position, length int) string {
line, ok := d.GetLine(int(pos.Line))
utf16Bytes := utf16.Encode([]rune(line))
if !ok {
return ""
}
charIdx := int(pos.Character)
if length > charIdx {
return line[0:charIdx]
return string(utf16.Decode(utf16Bytes[0:charIdx]))
}
return line[(charIdx - length):charIdx]
return string(utf16.Decode(utf16Bytes[(charIdx - length):charIdx]))
}
// LookForward returns the n characters after the given position, on the same line.
func (d *document) LookForward(pos protocol.Position, length int) string {
line, ok := d.GetLine(int(pos.Line))
utf16Bytes := utf16.Encode([]rune(line))
if !ok {
return ""
}
lineLength := len(line)
lineLength := len(utf16Bytes)
charIdx := int(pos.Character)
if lineLength <= charIdx+length {
return line[charIdx:]
return string(utf16.Decode(utf16Bytes[charIdx:]))
}
return line[charIdx:(charIdx + length)]
return string(utf16.Decode(utf16Bytes[charIdx:(charIdx + length)]))
}
var wikiLinkRegex = regexp.MustCompile(`\[?\[\[(.+?)(?:\|(.+?))?\]\]`)
var markdownLinkRegex = regexp.MustCompile(`\[([^\]]+?[^\\])\]\((.+?[^\\])\)`)
// LinkFromRoot returns a Link to this document from the root of the given
// notebook.
func (d *document) LinkFromRoot(nb *core.Notebook) (*documentLink, error) {
href, err := nb.RelPath(d.Path)
if err != nil {
return nil, err
}
return &documentLink{
Href: href,
RelativeToDir: nb.Path,
}, nil
}
// DocumentLinkAt returns the internal or external link found in the document
// at the given position.
@ -120,6 +190,66 @@ func (d *document) DocumentLinkAt(pos protocol.Position) (*documentLink, error)
return nil, nil
}
// Recursive function to check whether a link is within inline code.
func linkWithinInlineCode(strBuffer string, linkStart, linkEnd int, insideInline bool) bool {
if backtickId := strings.Index(strBuffer, "`"); backtickId >= 0 && backtickId < linkEnd {
return linkWithinInlineCode(strBuffer[backtickId+1:],
linkStart-backtickId-1, linkEnd-backtickId-1, !insideInline)
} else {
return insideInline
}
}
var wikiLinkRegex = regexp.MustCompile(`\[?\[\[(.+?)(?: *\| *(.+?))?\]\]`)
var markdownLinkRegex = regexp.MustCompile(`\[([^\]]+?[^\\])\]\((.+?[^\\])\)`)
var fileURIregex = regexp.MustCompile(`file:///`)
var fencedStartRegex = regexp.MustCompile(`^(` + "```" + `|~~~).*`)
var fencedEndRegex = regexp.MustCompile(`^(` + "```" + `|~~~)\s*`)
var indentedRegex = regexp.MustCompile(`^(\s{4}|\t).+`)
var insideInline = false
var insideFenced = false
var insideIndented = false
var currentCodeBlockStart = -1
// check whether the current line in document is within a fenced or indented
// code block
func isLineWithinCodeBlock(lines []string, lineIndex int, line string) bool {
// if line is already within code fences or indented code block
if insideFenced {
if fencedEndRegex.FindStringIndex(line) != nil &&
lines[currentCodeBlockStart][:3] == line[:3] {
// Fenced code block ends with this line
insideFenced = false
currentCodeBlockStart = -1
}
return true
} else if insideIndented {
if indentedRegex.FindStringIndex(line) == nil && len(line) > 0 {
// Indeted code block ends with this line
insideIndented = false
currentCodeBlockStart = -1
} else {
return true
}
} else {
// Check whether the current line is the start of a code fence or
// indented code block
if fencedStartRegex.FindStringIndex(line) != nil {
insideFenced = true
currentCodeBlockStart = lineIndex
return true
} else if indentedRegex.FindStringIndex(line) != nil &&
(lineIndex > 0 && len(lines[lineIndex-1]) == 0 || lineIndex == 0) {
insideIndented = true
currentCodeBlockStart = lineIndex
return true
}
}
return false
}
// DocumentLinks returns all the internal and external links found in the
// document.
func (d *document) DocumentLinks() ([]documentLink, error) {
@ -128,13 +258,22 @@ func (d *document) DocumentLinks() ([]documentLink, error) {
lines := d.GetLines()
for lineIndex, line := range lines {
appendLink := func(href string, start, end int) {
if isLineWithinCodeBlock(lines, lineIndex, line) {
continue
}
appendLink := func(href string, start, end int, hasTitle bool, isWikiLink bool) {
if href == "" {
return
}
// Go regexes work with bytes, but the LSP client expects character indexes.
start = strutil.ByteIndexToRuneIndex(line, start)
end = strutil.ByteIndexToRuneIndex(line, end)
links = append(links, documentLink{
Href: href,
Href: href,
RelativeToDir: filepath.Dir(d.Path),
Range: protocol.Range{
Start: protocol.Position{
Line: protocol.UInteger(lineIndex),
@ -145,28 +284,94 @@ func (d *document) DocumentLinks() ([]documentLink, error) {
Character: protocol.UInteger(end),
},
},
HasTitle: hasTitle,
IsWikiLink: isWikiLink,
})
}
// extract link paths from [title](path) patterns
// note: match[0:1] is the entire match, match[2:3] is the contents of
// brackets, match[4:5] is contents of parentheses
for _, match := range markdownLinkRegex.FindAllStringSubmatchIndex(line, -1) {
// Ignore when inside backticks: `[title](file)`
if linkWithinInlineCode(line, match[0], match[1], insideInline) {
continue
}
// Ignore embedded images ![title](file.png)
if match[0] > 0 && line[match[0]-1] == '!' {
continue
}
// ignore tripple dash file URIs [title](file:///foo.go)
if match[5]-match[4] >= 8 {
linkURL := line[match[4]:match[5]]
fileURIresult := linkURL[:8]
if fileURIregex.MatchString(fileURIresult) {
continue
}
}
href := line[match[4]:match[5]]
// Valid Markdown links are percent-encoded.
// Decode the href if it's percent-encoded
if decodedHref, err := url.PathUnescape(href); err == nil {
href = decodedHref
}
appendLink(href, match[0], match[1])
appendLink(href, match[0], match[1], false, false)
}
for _, match := range wikiLinkRegex.FindAllStringSubmatchIndex(line, -1) {
// Ignore when inside backticks: `[[filename]]`
if linkWithinInlineCode(line, match[0], match[1], insideInline) {
continue
}
href := line[match[2]:match[3]]
appendLink(href, match[0], match[1])
hasTitle := match[4] != -1
appendLink(href, match[0], match[1], hasTitle, true)
}
if strings.Count(line, "`")%2 == 1 {
insideInline = !insideInline
}
}
return links, nil
}
// IsTagPosition returns whether the given caret position is inside a tag (YAML frontmatter, #hashtag, etc.).
func (d *document) IsTagPosition(position protocol.Position, noteContentParser core.NoteContentParser) bool {
lines := strutil.CopyList(d.GetLines())
lineIdx := int(position.Line)
charIdx := int(position.Character)
line := lines[lineIdx]
// https://github.com/zk-org/zk/issues/144#issuecomment-1006108485
line = line[:charIdx] + "ZK_PLACEHOLDER" + line[charIdx:]
lines[lineIdx] = line
targetWord := strutil.WordAt(line, charIdx)
if targetWord == "" {
return false
}
if string(targetWord[0]) == "#" {
targetWord = targetWord[1:]
}
content := strings.Join(lines, "\n")
note, err := noteContentParser.ParseNoteContent(content)
if err != nil {
return false
}
return strutil.Contains(note.Tags, targetWord)
}
type documentLink struct {
Href string
Range protocol.Range
Href string
RelativeToDir string
Range protocol.Range
// HasTitle indicates whether this link has a title information. For
// example [[filename]] doesn't but [[filename|title]] does.
HasTitle bool
// IsWikiLink indicates whether this link is a [[WikiLink]] instead of a
// regular Markdown link.
IsWikiLink bool
}

@ -1,16 +1,18 @@
package lsp
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
strutil "github.com/mickael-menu/zk/internal/util/strings"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/opt"
strutil "github.com/zk-org/zk/internal/util/strings"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
glspserv "github.com/tliron/glsp/server"
@ -20,21 +22,25 @@ import (
// Server holds the state of the Language Server.
type Server struct {
server *glspserv.Server
notebooks *core.NotebookStore
documents map[protocol.DocumentUri]*document
fs core.FileStorage
logger util.Logger
server *glspserv.Server
notebooks *core.NotebookStore
documents *documentStore
noteContentParser core.NoteContentParser
templateLoader core.TemplateLoader
fs core.FileStorage
logger util.Logger
useAdditionalTextEdits opt.Bool
}
// ServerOpts holds the options to create a new Server.
type ServerOpts struct {
Name string
Version string
LogFile opt.String
Logger *util.ProxyLogger
Notebooks *core.NotebookStore
FS core.FileStorage
Name string
Version string
LogFile opt.String
Logger *util.ProxyLogger
Notebooks *core.NotebookStore
TemplateLoader core.TemplateLoader
FS core.FileStorage
}
// NewServer creates a new Server instance.
@ -45,20 +51,23 @@ func NewServer(opts ServerOpts) *Server {
logging.Configure(10, opts.LogFile.Value)
}
workspace := newWorkspace()
handler := protocol.Handler{}
server := &Server{
server: glspserv.NewServer(&handler, opts.Name, debug),
notebooks: opts.Notebooks,
documents: map[string]*document{},
fs: fs,
}
glspServer := glspserv.NewServer(&handler, opts.Name, debug)
// Redirect zk's logger to GLSP's to avoid breaking the JSON-RPC protocol
// with unwanted output.
if opts.Logger != nil {
opts.Logger.Logger = newGlspLogger(server.server.Log)
server.logger = opts.Logger
opts.Logger.Logger = newGlspLogger(glspServer.Log)
}
server := &Server{
server: glspServer,
notebooks: opts.Notebooks,
documents: newDocumentStore(fs, opts.Logger),
templateLoader: opts.TemplateLoader,
fs: fs,
logger: opts.Logger,
useAdditionalTextEdits: opt.NullBool,
}
var clientCapabilities protocol.ClientCapabilities
@ -66,24 +75,25 @@ func NewServer(opts ServerOpts) *Server {
handler.Initialize = func(context *glsp.Context, params *protocol.InitializeParams) (interface{}, error) {
clientCapabilities = params.Capabilities
if len(params.WorkspaceFolders) > 0 {
for _, f := range params.WorkspaceFolders {
workspace.addFolder(f.URI)
}
} else if params.RootURI != nil {
workspace.addFolder(*params.RootURI)
} else if params.RootPath != nil {
workspace.addFolder(*params.RootPath)
}
// To see the logs with coc.nvim, run :CocCommand workspace.showOutput
// https://github.com/neoclide/coc.nvim/wiki/Debug-language-server#using-output-channel
if params.Trace != nil {
protocol.SetTraceValue(*params.Trace)
}
if params.ClientInfo != nil {
if params.ClientInfo.Name == "Visual Studio Code" {
// Visual Studio Code doesn't seem to support inl
// VSCode doesn't support deleting the trigger characters with
// the main TextEdit. We'll use additional text edits instead.
server.useAdditionalTextEdits = opt.True
}
}
capabilities := handler.CreateServerCapabilities()
capabilities.HoverProvider = true
capabilities.DefinitionProvider = true
capabilities.CodeActionProvider = true
change := protocol.TextDocumentSyncKindIncremental
capabilities.TextDocumentSync = protocol.TextDocumentSyncOptions{
@ -95,14 +105,22 @@ func NewServer(opts ServerOpts) *Server {
ResolveProvider: boolPtr(true),
}
triggerChars := []string{"[", "#", ":"}
triggerChars := []string{"(", "[", "#", ":"}
capabilities.ExecuteCommandProvider = &protocol.ExecuteCommandOptions{
Commands: []string{
cmdIndex,
cmdNew,
cmdList,
cmdTagList,
},
}
capabilities.CompletionProvider = &protocol.CompletionOptions{
TriggerCharacters: triggerChars,
ResolveProvider: boolPtr(true),
}
capabilities.DefinitionProvider = boolPtr(true)
capabilities.ReferencesProvider = &protocol.ReferenceOptions{}
return protocol.InitializeResult{
Capabilities: capabilities,
@ -127,59 +145,52 @@ func NewServer(opts ServerOpts) *Server {
return nil
}
handler.WorkspaceDidChangeWorkspaceFolders = func(context *glsp.Context, params *protocol.DidChangeWorkspaceFoldersParams) error {
for _, f := range params.Event.Added {
workspace.addFolder(f.URI)
}
for _, f := range params.Event.Removed {
workspace.removeFolder(f.URI)
}
return nil
}
handler.TextDocumentDidOpen = func(context *glsp.Context, params *protocol.DidOpenTextDocumentParams) error {
langID := params.TextDocument.LanguageID
if langID != "markdown" && langID != "vimwiki" {
return nil
doc, err := server.documents.DidOpen(*params, context.Notify)
if err != nil {
return err
}
path := fs.Canonical(strings.TrimPrefix(params.TextDocument.URI, "file://"))
server.documents[params.TextDocument.URI] = &document{
Path: path,
Content: params.TextDocument.Text,
Log: server.server.Log,
if doc != nil {
server.refreshDiagnosticsOfDocument(doc, context.Notify, false)
}
return nil
}
handler.TextDocumentDidChange = func(context *glsp.Context, params *protocol.DidChangeTextDocumentParams) error {
doc, ok := server.documents[params.TextDocument.URI]
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil
}
doc.ApplyChanges(params.ContentChanges)
server.refreshDiagnosticsOfDocument(doc, context.Notify, true)
return nil
}
handler.TextDocumentDidClose = func(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
delete(server.documents, params.TextDocument.URI)
server.documents.Close(params.TextDocument.URI)
return nil
}
handler.TextDocumentDidSave = func(context *glsp.Context, params *protocol.DidSaveTextDocumentParams) error {
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil
}
notebook, err := server.notebookOf(doc)
if err != nil {
server.logger.Err(err)
return nil
}
_, err = notebook.Index(core.NoteIndexOpts{})
server.logger.Err(err)
return nil
}
handler.TextDocumentCompletion = func(context *glsp.Context, params *protocol.CompletionParams) (interface{}, error) {
triggerChar := params.Context.TriggerCharacter
if params.Context.TriggerKind != protocol.CompletionTriggerKindTriggerCharacter || triggerChar == nil {
return nil, nil
}
doc, ok := server.documents[params.TextDocument.URI]
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
@ -189,27 +200,16 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
switch *triggerChar {
case "#":
if notebook.Config.Format.Markdown.Hashtags {
return server.buildTagCompletionList(notebook, "#")
}
case ":":
if notebook.Config.Format.Markdown.ColonTags {
return server.buildTagCompletionList(notebook, ":")
}
case "[":
if doc.LookBehind(params.Position, 2) == "[[" {
return server.buildLinkCompletionList(doc, notebook, params)
}
if params.Context != nil && params.Context.TriggerKind == protocol.CompletionTriggerKindInvoked {
return server.buildInvokedCompletionList(notebook, doc, params.Position)
} else {
return server.buildTriggerCompletionList(notebook, doc, params.Position)
}
return nil, nil
}
handler.CompletionItemResolve = func(context *glsp.Context, params *protocol.CompletionItem) (*protocol.CompletionItem, error) {
if path, ok := params.Data.(string); ok {
content, err := ioutil.ReadFile(path)
content, err := os.ReadFile(path)
if err != nil {
return params, err
}
@ -223,7 +223,7 @@ func NewServer(opts ServerOpts) *Server {
}
handler.TextDocumentHover = func(context *glsp.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
doc, ok := server.documents[params.TextDocument.URI]
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
@ -238,13 +238,19 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
target, err := server.targetForHref(link.Href, doc, notebook)
if err != nil || target == "" || strutil.IsURL(target) {
target, err := server.noteForLink(*link, notebook)
if err != nil || target == nil {
return nil, err
}
path, err := uriToPath(target.URI)
if err != nil {
server.logger.Printf("unable to parse URI: %v", err)
return nil, err
}
path = fs.Canonical(path)
target = strings.TrimPrefix(target, "file://")
contents, err := ioutil.ReadFile(target)
contents, err := os.ReadFile(path)
if err != nil {
return nil, err
}
@ -258,7 +264,7 @@ func NewServer(opts ServerOpts) *Server {
}
handler.TextDocumentDocumentLink = func(context *glsp.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
doc, ok := server.documents[params.TextDocument.URI]
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
@ -275,22 +281,31 @@ func NewServer(opts ServerOpts) *Server {
documentLinks := []protocol.DocumentLink{}
for _, link := range links {
target, err := server.targetForHref(link.Href, doc, notebook)
if target == "" || err != nil {
continue
var target string
if strutil.IsURL(link.Href) {
// External link
target = link.Href
} else {
// Internal note link
targetNote, err := server.noteForLink(link, notebook)
if targetNote != nil && err == nil {
target = targetNote.URI
}
}
documentLinks = append(documentLinks, protocol.DocumentLink{
Range: link.Range,
Target: &target,
})
if target != "" {
documentLinks = append(documentLinks, protocol.DocumentLink{
Range: link.Range,
Target: &target,
})
}
}
return documentLinks, err
}
handler.TextDocumentDefinition = func(context *glsp.Context, params *protocol.DefinitionParams) (interface{}, error) {
doc, ok := server.documents[params.TextDocument.URI]
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
@ -305,8 +320,8 @@ func NewServer(opts ServerOpts) *Server {
return nil, err
}
target, err := server.targetForHref(link.Href, doc, notebook)
if link == nil || target == "" || err != nil {
target, err := server.noteForLink(*link, notebook)
if link == nil || target == nil || err != nil {
return nil, err
}
@ -315,51 +330,362 @@ func NewServer(opts ServerOpts) *Server {
if false && isTrue(clientCapabilities.TextDocument.Definition.LinkSupport) {
return protocol.LocationLink{
OriginSelectionRange: &link.Range,
TargetURI: target,
TargetURI: target.URI,
}, nil
} else {
return protocol.Location{
URI: target,
URI: target.URI,
}, nil
}
}
handler.WorkspaceExecuteCommand = func(context *glsp.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
openNotebook := func() (*core.Notebook, error) {
args := params.Arguments
if len(args) == 0 {
return nil, fmt.Errorf("%s expects a notebook path as first argument", params.Command)
}
path, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("%s expects a notebook path as first argument, got: %v", params.Command, args[0])
}
return server.notebooks.Open(path)
}
switch params.Command {
case cmdIndex:
nb, err := openNotebook()
if err != nil {
return nil, err
}
return executeCommandIndex(nb, params.Arguments)
case cmdNew:
nb, err := openNotebook()
if err != nil {
return nil, err
}
return executeCommandNew(nb, server.documents, context, params.Arguments)
case cmdLink:
nb, err := openNotebook()
if err != nil {
return nil, err
}
return executeCommandLink(nb, server.documents, context, params.Arguments)
case cmdList:
nb, err := openNotebook()
if err != nil {
return nil, err
}
return executeCommandList(server.logger, nb, params.Arguments)
case cmdTagList:
nb, err := openNotebook()
if err != nil {
return nil, err
}
return executeCommandTagList(server.logger, nb, params.Arguments)
default:
return nil, fmt.Errorf("unknown zk LSP command: %s", params.Command)
}
}
handler.TextDocumentCodeAction = func(context *glsp.Context, params *protocol.CodeActionParams) (interface{}, error) {
if isRangeEmpty(params.Range) {
return nil, nil
}
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
wd := filepath.Dir(doc.Path)
actions := []protocol.CodeAction{}
addAction := func(dir string, actionTitle string) error {
opts := cmdNewOpts{
Title: doc.ContentAtRange(params.Range),
Dir: dir,
InsertLinkAtLocation: &protocol.Location{
URI: params.TextDocument.URI,
Range: params.Range,
},
}
var jsonOpts map[string]interface{}
err := unmarshalJSON(opts, &jsonOpts)
if err != nil {
return err
}
actions = append(actions, protocol.CodeAction{
Title: actionTitle,
Kind: stringPtr(protocol.CodeActionKindRefactor),
Command: &protocol.Command{
Title: actionTitle,
Command: cmdNew,
Arguments: []interface{}{wd, jsonOpts},
},
})
return nil
}
addAction(wd, "New note in current directory")
addAction("", "New note in top directory")
return actions, nil
}
handler.TextDocumentReferences = func(context *glsp.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) {
doc, ok := server.documents.Get(params.TextDocument.URI)
if !ok {
return nil, nil
}
notebook, err := server.notebookOf(doc)
if err != nil {
return nil, err
}
link, err := doc.DocumentLinkAt(params.Position)
if err != nil {
return nil, err
}
if link == nil {
link, err = doc.LinkFromRoot(notebook)
if err != nil {
return nil, err
}
}
target, err := server.noteForLink(*link, notebook)
if link == nil || target == nil || err != nil {
return nil, err
}
opts := core.NoteFindOpts{
LinkTo: &core.LinkFilter{Hrefs: []string{target.Path}},
}
notes, err := notebook.FindNotes(opts)
if err != nil {
return nil, err
}
var locations []protocol.Location
for _, note := range notes {
pos := strings.Index(note.RawContent, target.Path[0:len(target.Path)-3])
var line uint32 = 0
if pos < 0 {
line = 0
} else {
linePos := strings.Count(note.RawContent[0:pos], "\n")
line = uint32(linePos)
}
locations = append(locations, protocol.Location{
URI: pathToURI(filepath.Join(notebook.Path, note.Path)),
Range: protocol.Range{
Start: protocol.Position{
Line: line,
Character: 0,
},
End: protocol.Position{
Line: line,
Character: 0,
},
},
})
}
return locations, nil
}
return server
}
// Run starts the Language Server in stdio mode.
func (s *Server) Run() error {
return errors.Wrap(s.server.RunStdio(), "lsp")
}
func (s *Server) notebookOf(doc *document) (*core.Notebook, error) {
return s.notebooks.Open(doc.Path)
}
// targetForHref returns the LSP documentUri for the note at the given HREF.
func (s *Server) targetForHref(href string, doc *document, notebook *core.Notebook) (string, error) {
// noteForLink returns the Note object for the note targeted by the given link.
//
// Match by order of precedence:
// 1. Prefix of relative path
// 2. Find any occurrence of the href in a note path (substring)
// 3. Match the href as a term in the note titles
func (s *Server) noteForLink(link documentLink, notebook *core.Notebook) (*Note, error) {
note, err := s.noteForHref(link.Href, link.RelativeToDir, notebook)
if note == nil && err == nil && link.IsWikiLink {
// Try to find a partial href match.
note, err = notebook.FindByHref(link.Href, true)
}
if note == nil || err != nil {
return nil, err
}
joined_path := filepath.Join(notebook.Path, note.Path)
return &Note{*note, pathToURI(joined_path)}, nil
}
// noteForHref returns the Note object for the note targeted by the given HREF
// relative to relativeToDir.
func (s *Server) noteForHref(href string, relativeToDir string, notebook *core.Notebook) (*core.MinimalNote, error) {
if strutil.IsURL(href) {
return href, nil
} else {
path := filepath.Clean(filepath.Join(filepath.Dir(doc.Path), href))
path, err := filepath.Rel(notebook.Path, path)
if err != nil {
return "", errors.Wrapf(err, "failed to resolve href: %s", href)
return nil, nil
}
path := href
if relativeToDir != "" {
path = filepath.Clean(filepath.Join(relativeToDir, path))
}
path, err := filepath.Rel(notebook.Path, path)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve href: %s", href)
}
note, err := notebook.FindByHref(path, false)
if err != nil {
s.logger.Printf("findByHref(%s): %s", href, err.Error())
}
return note, err
}
type Note struct {
core.MinimalNote
URI protocol.DocumentUri
}
func (s *Server) refreshDiagnosticsOfDocument(doc *document, notify glsp.NotifyFunc, delay bool) {
if doc.NeedsRefreshDiagnostics { // Already refreshing
return
}
notebook, err := s.notebookOf(doc)
if err != nil {
s.logger.Err(err)
return
}
diagConfig := notebook.Config.LSP.Diagnostics
if diagConfig.WikiTitle == core.LSPDiagnosticNone && diagConfig.DeadLink == core.LSPDiagnosticNone {
// No diagnostic enabled.
return
}
doc.NeedsRefreshDiagnostics = true
go func() {
if delay {
time.Sleep(1 * time.Second)
}
note, err := notebook.FindByHref(path)
doc.NeedsRefreshDiagnostics = false
diagnostics := []protocol.Diagnostic{}
links, err := doc.DocumentLinks()
if err != nil {
s.logger.Printf("findByHref(%s): %s", href, err.Error())
return "", err
s.logger.Err(err)
return
}
if note == nil {
return "", nil
for _, link := range links {
if strutil.IsURL(link.Href) {
continue
}
target, err := s.noteForLink(link, notebook)
if err != nil {
s.logger.Err(err)
continue
}
var severity protocol.DiagnosticSeverity
var message string
if target == nil {
if diagConfig.DeadLink == core.LSPDiagnosticNone {
continue
}
severity = protocol.DiagnosticSeverity(diagConfig.DeadLink)
message = "not found"
} else {
if diagConfig.WikiTitle == core.LSPDiagnosticNone {
continue
}
severity = protocol.DiagnosticSeverity(diagConfig.WikiTitle)
message = target.Title
}
diagnostics = append(diagnostics, protocol.Diagnostic{
Range: link.Range,
Severity: &severity,
Source: stringPtr("zk"),
Message: message,
})
}
return "file://" + filepath.Join(notebook.Path, note.Path), nil
go notify(protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{
URI: doc.URI,
Diagnostics: diagnostics,
})
}()
}
// buildInvokedCompletionList builds the completion item response for a
// completion started automatically when typing an identifier, or manually.
func (s *Server) buildInvokedCompletionList(notebook *core.Notebook, doc *document, position protocol.Position) ([]protocol.CompletionItem, error) {
currentWord := doc.WordAt(position)
if strings.HasPrefix(doc.LookBehind(position, len(currentWord)+2), "[[") {
return s.buildLinkCompletionList(notebook, doc, position)
}
if doc.IsTagPosition(position, notebook.Parser) {
return s.buildTagCompletionList(notebook, doc.WordAt(position))
}
return nil, nil
}
// Run starts the Language Server in stdio mode.
func (s *Server) Run() error {
return errors.Wrap(s.server.RunStdio(), "lsp")
// buildTriggerCompletionList builds the completion item response for a
// completion started with a trigger character.
func (s *Server) buildTriggerCompletionList(notebook *core.Notebook, doc *document, position protocol.Position) ([]protocol.CompletionItem, error) {
// We don't use the context because clients might not send it. Instead,
// we'll look for trigger patterns in the document.
switch doc.LookBehind(position, 3) {
case "]((":
return s.buildLinkCompletionList(notebook, doc, position)
}
switch doc.LookBehind(position, 2) {
case "[[":
return s.buildLinkCompletionList(notebook, doc, position)
}
switch doc.LookBehind(position, 1) {
case "#":
if notebook.Config.Format.Markdown.Hashtags {
return s.buildTagCompletionList(notebook, "#")
}
case ":":
if notebook.Config.Format.Markdown.ColonTags {
return s.buildTagCompletionList(notebook, ":")
}
}
return nil, nil
}
func (s *Server) buildTagCompletionList(notebook *core.Notebook, triggerChar string) ([]protocol.CompletionItem, error) {
tags, err := notebook.FindCollections(core.CollectionKindTag)
func (s *Server) buildTagCompletionList(notebook *core.Notebook, prefix string) ([]protocol.CompletionItem, error) {
tags, err := notebook.FindCollections(core.CollectionKindTag, nil)
if err != nil {
return nil, err
}
@ -368,7 +694,7 @@ func (s *Server) buildTagCompletionList(notebook *core.Notebook, triggerChar str
for _, tag := range tags {
items = append(items, protocol.CompletionItem{
Label: tag.Name,
InsertText: s.buildInsertForTag(tag.Name, triggerChar, notebook.Config),
InsertText: s.buildInsertForTag(tag.Name, prefix, notebook.Config),
Detail: stringPtr(fmt.Sprintf("%d %s", tag.NoteCount, strutil.Pluralize("note", tag.NoteCount))),
})
}
@ -376,8 +702,8 @@ func (s *Server) buildTagCompletionList(notebook *core.Notebook, triggerChar str
return items, nil
}
func (s *Server) buildInsertForTag(name string, triggerChar string, config core.Config) *string {
switch triggerChar {
func (s *Server) buildInsertForTag(name string, prefix string, config core.Config) *string {
switch prefix {
case ":":
name += ":"
case "#":
@ -392,8 +718,13 @@ func (s *Server) buildInsertForTag(name string, triggerChar string, config core.
return &name
}
func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook, params *protocol.CompletionParams) ([]protocol.CompletionItem, error) {
linkFormatter, err := notebook.NewLinkFormatter()
func (s *Server) buildLinkCompletionList(notebook *core.Notebook, doc *document, position protocol.Position) ([]protocol.CompletionItem, error) {
linkFormatter, err := newLinkFormatter(notebook, doc, position)
if err != nil {
return nil, err
}
templates, err := newCompletionTemplates(s.templateLoader, notebook.Config.LSP.Completion.Note)
if err != nil {
return nil, err
}
@ -405,7 +736,7 @@ func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook,
var items []protocol.CompletionItem
for _, note := range notes {
item, err := s.newCompletionItem(notebook, note, doc, params.Position, linkFormatter)
item, err := s.newCompletionItem(notebook, note, doc, position, linkFormatter, templates)
if err != nil {
s.logger.Err(err)
continue
@ -417,67 +748,135 @@ func (s *Server) buildLinkCompletionList(doc *document, notebook *core.Notebook,
return items, nil
}
func (s *Server) newCompletionItem(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter) (item protocol.CompletionItem, err error) {
func newLinkFormatter(notebook *core.Notebook, doc *document, position protocol.Position) (core.LinkFormatter, error) {
if doc.LookBehind(position, 3) == "]((" {
return core.NewMarkdownLinkFormatter(notebook.Config.Format.Markdown, true)
} else {
return notebook.NewLinkFormatter()
}
}
func (s *Server) newCompletionItem(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter, templates completionTemplates) (protocol.CompletionItem, error) {
kind := protocol.CompletionItemKindReference
item.Kind = &kind
item.Data = filepath.Join(notebook.Path, note.Path)
item := protocol.CompletionItem{
Kind: &kind,
Data: filepath.Join(notebook.Path, note.Path),
}
if note.Title != "" {
item.Label = note.Title
templateContext, err := newCompletionItemRenderContext(note, notebook.Path, doc.Path)
if err != nil {
return item, err
}
if templates.Label != nil {
item.Label, err = templates.Label.Render(templateContext)
if err != nil {
return item, err
}
} else {
item.Label = note.Title
}
// Fallback on the note path to never have empty labels.
if item.Label == "" {
item.Label = note.Path
}
// Add the path to the filter text to be able to complete by it.
item.FilterText = stringPtr(item.Label + " " + note.Path)
if templates.FilterText != nil {
filterText, err := templates.FilterText.Render(templateContext)
if err != nil {
return item, err
}
item.FilterText = &filterText
}
if item.FilterText == nil || *item.FilterText == "" {
// Add the path to the filter text to be able to complete by it.
item.FilterText = stringPtr(item.Label + " " + note.Path)
}
if templates.Detail != nil {
detail, err := templates.Detail.Render(templateContext)
if err != nil {
return item, err
}
item.Detail = &detail
}
item.TextEdit, err = s.newTextEditForLink(notebook, note, doc, pos, linkFormatter)
if err != nil {
err = errors.Wrapf(err, "failed to build TextEdit for note at %s", note.Path)
return
return item, err
}
addTextEdits := []protocol.TextEdit{}
if s.useAdditionalTextEditsWithNotebook(notebook) {
addTextEdits := []protocol.TextEdit{}
// Some LSP clients (e.g. VSCode) don't support deleting the trigger
// characters with the main TextEdit. So let's add an additional
// TextEdit for that.
addTextEdits = append(addTextEdits, protocol.TextEdit{
NewText: "",
Range: rangeFromPosition(pos, -2, 0),
})
startOffset := -2
if doc.LookBehind(pos, 2) != "[[" {
currentWord := doc.WordAt(pos)
startOffset = -2 - len(currentWord)
}
item.AdditionalTextEdits = addTextEdits
// Some LSP clients (e.g. VSCode) don't support deleting the trigger
// characters with the main TextEdit. So let's add an additional
// TextEdit for that.
addTextEdits = append(addTextEdits, protocol.TextEdit{
NewText: "",
Range: rangeFromPosition(pos, startOffset, 0),
})
item.AdditionalTextEdits = addTextEdits
}
return item, nil
}
func (s *Server) newTextEditForLink(notebook *core.Notebook, note core.MinimalNote, doc *document, pos protocol.Position, linkFormatter core.LinkFormatter) (interface{}, error) {
path := filepath.Join(notebook.Path, note.Path)
path = s.fs.Canonical(path)
path, err := filepath.Rel(filepath.Dir(doc.Path), path)
path := core.NotebookPath{
Path: note.Path,
BasePath: notebook.Path,
WorkingDir: filepath.Dir(doc.Path),
}
context, err := core.NewLinkFormatterContext(path, note.Title, note.Metadata)
if err != nil {
path = note.Path
return nil, err
}
link, err := linkFormatter(path, note.Title)
link, err := linkFormatter(context)
if err != nil {
return nil, err
}
// Overwrite [[ trigger directly if the additional text edits are disabled.
startOffset := 0
if !s.useAdditionalTextEditsWithNotebook(notebook) {
if doc.LookBehind(pos, 2) == "[[" {
startOffset = -2
} else {
currentWord := doc.WordAt(pos)
startOffset = -2 - len(currentWord)
}
}
// Some LSP clients (e.g. VSCode) auto-pair brackets, so we need to
// remove the closing ]] after the completion.
// remove the closing ]] or )) after the completion.
endOffset := 0
if doc.LookForward(pos, 2) == "]]" {
suffix := doc.LookForward(pos, 2)
if suffix == "]]" || suffix == "))" {
endOffset = 2
}
return protocol.TextEdit{
NewText: link,
Range: rangeFromPosition(pos, 0, endOffset),
Range: rangeFromPosition(pos, startOffset, endOffset),
}, nil
}
func (s *Server) useAdditionalTextEditsWithNotebook(nb *core.Notebook) bool {
return nb.Config.LSP.Completion.UseAdditionalTextEdits.
Or(s.useAdditionalTextEdits).
OrBool(false).
Unwrap()
}
func positionInRange(content string, rng protocol.Range, pos protocol.Position) bool {
start, end := rng.IndexesIn(content)
i := pos.IndexIn(content)
@ -501,6 +900,10 @@ func rangeFromPosition(pos protocol.Position, startOffset, endOffset int) protoc
}
}
func isRangeEmpty(pos protocol.Range) bool {
return pos.Start == pos.End
}
func boolPtr(v bool) *bool {
b := v
return &b
@ -518,3 +921,16 @@ func stringPtr(v string) *string {
s := v
return &s
}
func unmarshalJSON(obj interface{}, v interface{}) error {
js, err := json.Marshal(obj)
if err != nil {
return err
}
return json.Unmarshal(js, v)
}
func toBool(obj interface{}) bool {
s := strings.ToLower(fmt.Sprint(obj))
return s == "true" || s == "1"
}

@ -0,0 +1,118 @@
package lsp
import (
"fmt"
"net/url"
"path/filepath"
"runtime"
"strings"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func pathToURI(path string) string {
u := &url.URL{
Scheme: "file",
Path: path,
}
return u.String()
}
func uriToPath(uri string) (string, error) {
s := strings.ReplaceAll(uri, "%5C", "/")
parsed, err := url.Parse(s)
if err != nil {
return "", err
}
if parsed.Scheme != "file" {
return "", errors.New("URI was not a file:// URI")
}
if runtime.GOOS == "windows" {
// In Windows "file:///c:/tmp/foo.md" is parsed to "/c:/tmp/foo.md".
// Strip the first character to get a valid path.
if strings.Contains(parsed.Path[1:], ":") {
// url.Parse() behaves differently with "file:///c:/..." and "file://c:/..."
return parsed.Path[1:], nil
} else {
// if the windows drive is not included in Path it will be in Host
return parsed.Host + "/" + parsed.Path[1:], nil
}
}
return parsed.Path, nil
}
// jsonBoolean can be unmarshalled from integers or strings.
// Neovim cannot send a boolean easily, so it's useful to support integers too.
type jsonBoolean bool
func (b *jsonBoolean) UnmarshalJSON(data []byte) error {
s := string(data)
if s == "1" || s == "true" {
*b = true
} else if s == "0" || s == "false" {
*b = false
} else {
return fmt.Errorf("%s: failed to unmarshal as boolean", s)
}
return nil
}
type linkInfo struct {
note *core.MinimalNote
location *protocol.Location
title *string
}
func linkNote(notebook *core.Notebook, documents *documentStore, context *glsp.Context, info *linkInfo) error {
if info.location == nil {
return errors.New("'location' not provided")
}
// Get current document to edit
doc, ok := documents.Get(info.location.URI)
if !ok {
return fmt.Errorf("Cannot insert link in '%s'", info.location.URI)
}
formatter, err := notebook.NewLinkFormatter()
if err != nil {
return err
}
path := core.NotebookPath{
Path: info.note.Path,
BasePath: notebook.Path,
WorkingDir: filepath.Dir(doc.Path),
}
var title *string
title = info.title
if title == nil {
title = &info.note.Title
}
formatterContext, err := core.NewLinkFormatterContext(path, *title, info.note.Metadata)
if err != nil {
return err
}
link, err := formatter(formatterContext)
if err != nil {
return err
}
go context.Call(protocol.ServerWorkspaceApplyEdit, protocol.ApplyWorkspaceEditParams{
Edit: protocol.WorkspaceEdit{
Changes: map[string][]protocol.TextEdit{
info.location.URI: {{Range: info.location.Range, NewText: link}},
},
},
}, nil)
return nil
}

@ -1,28 +0,0 @@
package lsp
import "strings"
type workspace struct {
folders []string
}
func newWorkspace() *workspace {
return &workspace{
folders: []string{},
}
}
func (w *workspace) addFolder(folder string) {
folder = strings.TrimPrefix(folder, "file://")
w.folders = append(w.folders, folder)
}
func (w *workspace) removeFolder(folder string) {
folder = strings.TrimPrefix(folder, "file://")
for i, f := range w.folders {
if f == folder {
w.folders = append(w.folders[:i], w.folders[i+1:]...)
break
}
}
}

@ -6,7 +6,6 @@ import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
@ -14,7 +13,7 @@ import (
// Tags represents a list of inline tags in a Markdown document.
type Tags struct {
gast.BaseInline
ast.BaseInline
// Tags in this list.
Tags []string
}
@ -22,13 +21,13 @@ type Tags struct {
func (n *Tags) Dump(source []byte, level int) {
m := map[string]string{}
m["Tags"] = strings.Join(n.Tags, ", ")
gast.DumpHelper(n, source, level, m, nil)
ast.DumpHelper(n, source, level, m, nil)
}
// KindTags is a NodeKind of the Tags node.
var KindTags = gast.NewNodeKind("Tags")
var KindTags = ast.NewNodeKind("Tags")
func (n *Tags) Kind() gast.NodeKind {
func (n *Tags) Kind() ast.NodeKind {
return KindTags
}
@ -106,7 +105,11 @@ func (p *hashtagParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
}
}
for i, char := range string(line[1:]) {
for i, char := range string(line) {
if i == 0 {
// Skip the first character, as it is #
continue
}
if parsingMultiWordTag {
multiWordTagEndPos = i
} else {
@ -164,7 +167,7 @@ func (p *hashtagParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
block.Advance(endPos)
return &Tags{
BaseInline: gast.BaseInline{},
BaseInline: ast.BaseInline{},
Tags: []string{tag},
}
}
@ -222,7 +225,7 @@ func (p *colontagParser) Parse(parent ast.Node, block text.Reader, pc parser.Con
} else if char == ':' {
tag = strings.TrimSpace(tag)
if len(tag) == 0 {
if !isValidTag(tag) {
break
}
tags = append(tags, tag)
@ -244,7 +247,7 @@ func (p *colontagParser) Parse(parent ast.Node, block text.Reader, pc parser.Con
block.Advance(endPos)
return &Tags{
BaseInline: gast.BaseInline{},
BaseInline: ast.BaseInline{},
Tags: tags,
}
}
@ -256,3 +259,19 @@ func isValidTagChar(r rune, excluded rune) bool {
r == '&' || r == '+' || r == '=' || r == ':' ||
r == '#')
}
func isValidTag(tag string) bool {
if len(tag) == 0 {
return false
}
// Prevent Markdown table syntax to be parsed a a colon tag, e.g. |:---:|
// https://github.com/zk-org/zk/issues/185
for _, c := range tag {
if c != '-' {
return true
}
}
return false
}

@ -3,7 +3,7 @@ package extensions
import (
"strings"
"github.com/mickael-menu/zk/internal/core"
"github.com/zk-org/zk/internal/core"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
@ -18,6 +18,11 @@ var WikiLinkExt = &wikiLink{}
type wikiLink struct{}
// WikiLink represents a wiki link found in a Markdown document.
type WikiLink struct {
ast.Link
}
func (w *wikiLink) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithInlineParsers(
@ -51,7 +56,7 @@ func (p *wlParser) Parse(parent ast.Node, block text.Reader, pc parser.Context)
endPos = 0 // Last position of the link in the line
)
appendChar := func(c byte) {
appendRune := func(c rune) {
if parsingLabel {
label += string(c)
} else {
@ -59,7 +64,7 @@ func (p *wlParser) Parse(parent ast.Node, block text.Reader, pc parser.Context)
}
}
for i, char := range line {
for i, char := range string(line) {
endPos = i
if closed {
@ -116,11 +121,11 @@ func (p *wlParser) Parse(parent ast.Node, block text.Reader, pc parser.Context)
// We add them to the HREF and reset the count.
if closerCharCount > 0 {
for i := 0; i < closerCharCount; i++ {
appendChar(']')
appendRune(']')
}
closerCharCount = 0
}
appendChar(char)
appendRune(char)
}
if !closed || len(href) == 0 {
@ -135,7 +140,7 @@ func (p *wlParser) Parse(parent ast.Node, block text.Reader, pc parser.Context)
label = href
}
link := ast.NewLink()
link := &WikiLink{Link: *ast.NewLink()}
link.Destination = []byte(href)
// Title will be parsed as the link's rel by the Markdown parser.
link.Title = []byte(rel)

@ -3,14 +3,16 @@ package markdown
import (
"bufio"
"fmt"
"net/url"
"regexp"
"strings"
"github.com/mickael-menu/zk/internal/adapter/markdown/extensions"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/opt"
strutil "github.com/mickael-menu/zk/internal/util/strings"
"github.com/mickael-menu/zk/internal/util/yaml"
"github.com/zk-org/zk/internal/adapter/markdown/extensions"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/opt"
strutil "github.com/zk-org/zk/internal/util/strings"
"github.com/zk-org/zk/internal/util/yaml"
"github.com/mvdan/xurls"
"github.com/yuin/goldmark"
meta "github.com/yuin/goldmark-meta"
@ -22,7 +24,8 @@ import (
// Parser parses the content of Markdown notes.
type Parser struct {
md goldmark.Markdown
md goldmark.Markdown
logger util.Logger
}
type ParserOpts struct {
@ -35,7 +38,7 @@ type ParserOpts struct {
}
// NewParser creates a new Markdown Parser.
func NewParser(options ParserOpts) *Parser {
func NewParser(options ParserOpts, logger util.Logger) *Parser {
return &Parser{
md: goldmark.New(
goldmark.WithExtensions(
@ -57,11 +60,12 @@ func NewParser(options ParserOpts) *Parser {
},
),
),
logger: logger,
}
}
// Parse implements core.NoteParser.
func (p *Parser) Parse(content string) (*core.ParsedNote, error) {
// ParseNoteContent implements core.NoteContentParser.
func (p *Parser) ParseNoteContent(content string) (*core.NoteContent, error) {
bytes := []byte(content)
context := parser.NewContext()
@ -70,7 +74,7 @@ func (p *Parser) Parse(content string) (*core.ParsedNote, error) {
parser.WithContext(context),
)
links, err := parseLinks(root, bytes)
links, err := p.parseLinks(root, bytes)
if err != nil {
return nil, err
}
@ -91,7 +95,7 @@ func (p *Parser) Parse(content string) (*core.ParsedNote, error) {
return nil, err
}
return &core.ParsedNote{
return &core.NoteContent{
Title: title,
Body: body,
Lead: parseLead(body),
@ -208,19 +212,21 @@ func parseTags(frontmatter frontmatter, root ast.Node, source []byte) ([]string,
}
// parseLinks extracts outbound links from the note.
func parseLinks(root ast.Node, source []byte) ([]core.Link, error) {
func (p *Parser) parseLinks(root ast.Node, source []byte) ([]core.Link, error) {
links := make([]core.Link, 0)
err := ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
switch link := n.(type) {
case *ast.Link:
href := string(link.Destination)
href, err := url.PathUnescape(string(link.Destination))
p.logger.Err(err)
if href != "" {
snippet, snStart, snEnd := extractLines(n, source)
links = append(links, core.Link{
Title: string(link.Text(source)),
Href: href,
Type: core.LinkTypeMarkdown,
Rels: core.LinkRels(strings.Fields(string(link.Title))...),
IsExternal: strutil.IsURL(href),
Snippet: snippet,
@ -235,6 +241,7 @@ func parseLinks(root ast.Node, source []byte) ([]core.Link, error) {
links = append(links, core.Link{
Title: string(link.Label(source)),
Href: href,
Type: core.LinkTypeImplicit,
Rels: []core.LinkRelation{},
IsExternal: true,
Snippet: snippet,
@ -242,6 +249,22 @@ func parseLinks(root ast.Node, source []byte) ([]core.Link, error) {
SnippetEnd: snEnd,
})
}
case *extensions.WikiLink:
href := string(link.Destination)
if href != "" {
snippet, snStart, snEnd := extractLines(n, source)
links = append(links, core.Link{
Title: string(link.Text(source)),
Href: href,
Type: core.LinkTypeWikiLink,
Rels: core.LinkRels(strings.Fields(string(link.Title))...),
IsExternal: strutil.IsURL(href),
Snippet: snippet,
SnippetStart: snStart,
SnippetEnd: snEnd,
})
}
}
}
return ast.WalkContinue, nil

@ -3,9 +3,10 @@ package markdown
import (
"testing"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/opt"
"github.com/zk-org/zk/internal/util/test/assert"
)
func TestParseTitle(t *testing.T) {
@ -187,6 +188,10 @@ func TestParseHashtags(t *testing.T) {
test("##invalid also#invalid", []string{})
// Bear's multi multi-word tags are disabled
test("#multi word# end", []string{"multi"})
// Single character
// See https://github.com/zk-org/zk/issues/118
test("#a", []string{"a"})
}
func TestParseWordtags(t *testing.T) {
@ -364,6 +369,8 @@ An https://inline-link.com and http://another-inline-link.com.
A [[Wiki link]] is surrounded by [[2-brackets | two brackets]].
[[lien accentué]]
It can contain [[esca]\]ped \[chara\\cters]].
A [[[Folgezettel link]]] is surrounded by three brackets.
@ -379,6 +386,7 @@ Neuron links with titles: [[trailing|Trailing link]]# #[[leading | Leading link
{
Title: "link",
Href: "heading",
Type: core.LinkTypeMarkdown,
Rels: []core.LinkRelation{},
IsExternal: false,
Snippet: "Heading with a [link](heading)",
@ -388,6 +396,7 @@ Neuron links with titles: [[trailing|Trailing link]]# #[[leading | Leading link
{
Title: "multiple links",
Href: "stripped-formatting",
Type: core.LinkTypeMarkdown,
Rels: []core.LinkRelation{},
IsExternal: false,
Snippet: `Paragraph containing [multiple **links**](stripped-formatting), here's one [relative](../other).
@ -398,6 +407,7 @@ A link can have [one relation](one "rel-1") or [several relations](several "rel-
{
Title: "relative",
Href: "../other",
Type: core.LinkTypeMarkdown,
Rels: []core.LinkRelation{},
IsExternal: false,
Snippet: `Paragraph containing [multiple **links**](stripped-formatting), here's one [relative](../other).
@ -408,6 +418,7 @@ A link can have [one relation](one "rel-1") or [several relations](several "rel-
{
Title: "one relation",
Href: "one",
Type: core.LinkTypeMarkdown,
Rels: core.LinkRels("rel-1"),
IsExternal: false,
Snippet: `Paragraph containing [multiple **links**](stripped-formatting), here's one [relative](../other).
@ -418,6 +429,7 @@ A link can have [one relation](one "rel-1") or [several relations](several "rel-
{
Title: "several relations",
Href: "several",
Type: core.LinkTypeMarkdown,
Rels: core.LinkRels("rel-1", "rel-2"),
IsExternal: false,
Snippet: `Paragraph containing [multiple **links**](stripped-formatting), here's one [relative](../other).
@ -428,6 +440,7 @@ A link can have [one relation](one "rel-1") or [several relations](several "rel-
{
Title: "https://inline-link.com",
Href: "https://inline-link.com",
Type: core.LinkTypeImplicit,
IsExternal: true,
Rels: []core.LinkRelation{},
Snippet: "An https://inline-link.com and http://another-inline-link.com.",
@ -437,6 +450,7 @@ A link can have [one relation](one "rel-1") or [several relations](several "rel-
{
Title: "http://another-inline-link.com",
Href: "http://another-inline-link.com",
Type: core.LinkTypeImplicit,
IsExternal: true,
Rels: []core.LinkRelation{},
Snippet: "An https://inline-link.com and http://another-inline-link.com.",
@ -446,6 +460,7 @@ A link can have [one relation](one "rel-1") or [several relations](several "rel-
{
Title: "Wiki link",
Href: "Wiki link",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: []core.LinkRelation{},
Snippet: "A [[Wiki link]] is surrounded by [[2-brackets | two brackets]].",
@ -455,83 +470,129 @@ A link can have [one relation](one "rel-1") or [several relations](several "rel-
{
Title: "two brackets",
Href: "2-brackets",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: []core.LinkRelation{},
Snippet: "A [[Wiki link]] is surrounded by [[2-brackets | two brackets]].",
SnippetStart: 288,
SnippetEnd: 351,
},
{
Title: "lien accentué",
Href: "lien accentué",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: []core.LinkRelation{},
Snippet: "[[lien accentué]]",
SnippetStart: 353,
SnippetEnd: 371,
},
{
Title: `esca]]ped [chara\cters`,
Href: `esca]]ped [chara\cters`,
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: []core.LinkRelation{},
Snippet: `It can contain [[esca]\]ped \[chara\\cters]].`,
SnippetStart: 353,
SnippetEnd: 398,
SnippetStart: 373,
SnippetEnd: 418,
},
{
Title: "Folgezettel link",
Href: "Folgezettel link",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: core.LinkRels("down"),
Snippet: "A [[[Folgezettel link]]] is surrounded by three brackets.",
SnippetStart: 400,
SnippetEnd: 457,
SnippetStart: 420,
SnippetEnd: 477,
},
{
Title: "trailing hash",
Href: "trailing hash",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: core.LinkRels("down"),
Snippet: "Neuron also supports a [[trailing hash]]# for Folgezettel links.",
SnippetStart: 459,
SnippetEnd: 523,
SnippetStart: 479,
SnippetEnd: 543,
},
{
Title: "leading hash",
Href: "leading hash",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: core.LinkRels("up"),
Snippet: "A #[[leading hash]] is used for #uplinks.",
SnippetStart: 525,
SnippetEnd: 566,
SnippetStart: 545,
SnippetEnd: 586,
},
{
Title: "Trailing link",
Href: "trailing",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: core.LinkRels("down"),
Snippet: "Neuron links with titles: [[trailing|Trailing link]]# #[[leading | Leading link]]",
SnippetStart: 568,
SnippetEnd: 650,
SnippetStart: 588,
SnippetEnd: 670,
},
{
Title: "Leading link",
Href: "leading",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: core.LinkRels("up"),
Snippet: "Neuron links with titles: [[trailing|Trailing link]]# #[[leading | Leading link]]",
SnippetStart: 568,
SnippetEnd: 650,
SnippetStart: 588,
SnippetEnd: 670,
},
{
Title: "External links",
Href: "http://example.com",
Type: core.LinkTypeMarkdown,
Rels: []core.LinkRelation{},
IsExternal: true,
Snippet: `[External links](http://example.com) are marked [as such](ftp://domain).`,
SnippetStart: 652,
SnippetEnd: 724,
SnippetStart: 672,
SnippetEnd: 744,
},
{
Title: "as such",
Href: "ftp://domain",
Type: core.LinkTypeMarkdown,
Rels: []core.LinkRelation{},
IsExternal: true,
Snippet: `[External links](http://example.com) are marked [as such](ftp://domain).`,
SnippetStart: 652,
SnippetEnd: 724,
SnippetStart: 672,
SnippetEnd: 744,
},
})
// Markdown links are decoded, but not WikiLinks.
// i.e. https://github.com/zk-org/zk/issues/86
test("[foo%20bar](202110031652%20foo%20bar)", []core.Link{
{
Title: "foo%20bar",
Href: "202110031652 foo bar",
Type: core.LinkTypeMarkdown,
IsExternal: false,
Rels: []core.LinkRelation{},
Snippet: "[foo%20bar](202110031652%20foo%20bar)",
SnippetStart: 0,
SnippetEnd: 37,
},
})
test("[[202110031652%20foo%20bar]]", []core.Link{
{
Title: "202110031652%20foo%20bar",
Href: "202110031652%20foo%20bar",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: []core.LinkRelation{},
Snippet: "[[202110031652%20foo%20bar]]",
SnippetStart: 0,
SnippetEnd: 28,
},
})
}
@ -564,7 +625,7 @@ Paragraph
})
}
func parse(t *testing.T, source string) core.ParsedNote {
func parse(t *testing.T, source string) core.NoteContent {
return parseWithOptions(t, source, ParserOpts{
HashtagEnabled: true,
MultiWordTagEnabled: true,
@ -572,8 +633,8 @@ func parse(t *testing.T, source string) core.ParsedNote {
})
}
func parseWithOptions(t *testing.T, source string, options ParserOpts) core.ParsedNote {
content, err := NewParser(options).Parse(source)
func parseWithOptions(t *testing.T, source string, options ParserOpts) core.NoteContent {
content, err := NewParser(options, &util.NullLogger).ParseNoteContent(source)
assert.Nil(t, err)
return *content
}

@ -3,10 +3,11 @@ package sqlite
import (
"database/sql"
"fmt"
"strings"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
)
// CollectionDAO persists collections (e.g. tags) in the SQLite database.
@ -17,7 +18,6 @@ type CollectionDAO struct {
// Prepared SQL statements
createCollectionStmt *LazyStmt
findCollectionStmt *LazyStmt
findAllCollectionsStmt *LazyStmt
findAssociationStmt *LazyStmt
createAssociationStmt *LazyStmt
removeAssociationsStmt *LazyStmt
@ -42,16 +42,6 @@ func NewCollectionDAO(tx Transaction, logger util.Logger) *CollectionDAO {
WHERE kind = ? AND name = ?
`),
// Find all collections.
findAllCollectionsStmt: tx.PrepareLazy(`
SELECT c.name, COUNT(nc.id) as count
FROM collections c
INNER JOIN notes_collections nc ON nc.collection_id = c.id
WHERE kind = ?
GROUP BY c.id
ORDER BY c.name
`),
// Returns whether a note and a collection are associated.
findAssociationStmt: tx.PrepareLazy(`
SELECT id FROM notes_collections
@ -87,8 +77,25 @@ func (d *CollectionDAO) FindOrCreate(kind core.CollectionKind, name string) (cor
}
}
func (d *CollectionDAO) FindAll(kind core.CollectionKind) ([]core.Collection, error) {
rows, err := d.findAllCollectionsStmt.Query(kind)
func (d *CollectionDAO) FindAll(kind core.CollectionKind, sorters []core.CollectionSorter) ([]core.Collection, error) {
query := `
SELECT c.id, c.name, COUNT(nc.id) as count
FROM collections c
INNER JOIN notes_collections nc ON nc.collection_id = c.id
WHERE kind = ?
GROUP BY c.id
`
orderTerms := []string{}
if sorters != nil {
for _, sorter := range sorters {
orderTerms = append(orderTerms, collectionOrderTerm(sorter))
}
}
orderTerms = append(orderTerms, `c.name ASC`)
query += "ORDER BY " + strings.Join(orderTerms, ", ") + "\n"
rows, err := d.tx.Query(query, kind)
if err != nil {
return []core.Collection{}, err
}
@ -97,14 +104,16 @@ func (d *CollectionDAO) FindAll(kind core.CollectionKind) ([]core.Collection, er
collections := []core.Collection{}
for rows.Next() {
var id sql.NullInt64
var name string
var count int
err := rows.Scan(&name, &count)
err := rows.Scan(&id, &name, &count)
if err != nil {
return collections, err
}
collections = append(collections, core.Collection{
ID: core.CollectionID(id.Int64),
Kind: kind,
Name: name,
NoteCount: count,
@ -114,6 +123,22 @@ func (d *CollectionDAO) FindAll(kind core.CollectionKind) ([]core.Collection, er
return collections, nil
}
func collectionOrderTerm(sorter core.CollectionSorter) string {
order := " ASC"
if !sorter.Ascending {
order = " DESC"
}
switch sorter.Field {
case core.CollectionSortName:
return "c.name COLLATE NOCASE" + order
case core.CollectionSortNoteCount:
return "count" + order
default:
panic(fmt.Sprintf("%v: unknown core.CollectionSortField", sorter.Field))
}
}
func (d *CollectionDAO) findCollection(kind core.CollectionKind, name string) (core.CollectionID, error) {
wrap := errors.Wrapperf("failed to get %s named %s", kind, name)

@ -3,9 +3,9 @@ package sqlite
import (
"testing"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/test/assert"
)
func TestCollectionDAOFindOrCreate(t *testing.T) {
@ -35,18 +35,51 @@ func TestCollectionDAOFindOrCreate(t *testing.T) {
func TestCollectionDaoFindAll(t *testing.T) {
testCollectionDAO(t, func(tx Transaction, dao *CollectionDAO) {
// Finds none
cs, err := dao.FindAll("missing")
cs, err := dao.FindAll("missing", nil)
assert.Nil(t, err)
assert.Equal(t, len(cs), 0)
// Finds existing
cs, err = dao.FindAll("tag")
cs, err = dao.FindAll("tag", nil)
assert.Nil(t, err)
assert.Equal(t, cs, []core.Collection{
{Kind: "tag", Name: "adventure", NoteCount: 2},
{Kind: "tag", Name: "fantasy", NoteCount: 1},
{Kind: "tag", Name: "fiction", NoteCount: 1},
{Kind: "tag", Name: "history", NoteCount: 1},
{ID: 2, Kind: "tag", Name: "adventure", NoteCount: 2},
{ID: 4, Kind: "tag", Name: "fantasy", NoteCount: 1},
{ID: 1, Kind: "tag", Name: "fiction", NoteCount: 1},
{ID: 5, Kind: "tag", Name: "history", NoteCount: 1},
{ID: 7, Kind: "tag", Name: "science", NoteCount: 3},
})
})
}
func TestCollectionDaoFindAllSortedByName(t *testing.T) {
testCollectionDAO(t, func(tx Transaction, dao *CollectionDAO) {
cs, err := dao.FindAll("tag", []core.CollectionSorter{
{Field: core.CollectionSortName, Ascending: false},
})
assert.Nil(t, err)
assert.Equal(t, cs, []core.Collection{
{ID: 7, Kind: "tag", Name: "science", NoteCount: 3},
{ID: 5, Kind: "tag", Name: "history", NoteCount: 1},
{ID: 1, Kind: "tag", Name: "fiction", NoteCount: 1},
{ID: 4, Kind: "tag", Name: "fantasy", NoteCount: 1},
{ID: 2, Kind: "tag", Name: "adventure", NoteCount: 2},
})
})
}
func TestCollectionDaoFindAllSortedByNoteCount(t *testing.T) {
testCollectionDAO(t, func(tx Transaction, dao *CollectionDAO) {
cs, err := dao.FindAll("tag", []core.CollectionSorter{
{Field: core.CollectionSortNoteCount, Ascending: false},
})
assert.Nil(t, err)
assert.Equal(t, cs, []core.Collection{
{ID: 7, Kind: "tag", Name: "science", NoteCount: 3},
{ID: 2, Kind: "tag", Name: "adventure", NoteCount: 2},
{ID: 4, Kind: "tag", Name: "fantasy", NoteCount: 1},
{ID: 1, Kind: "tag", Name: "fiction", NoteCount: 1},
{ID: 5, Kind: "tag", Name: "history", NoteCount: 1},
})
})
}

@ -2,10 +2,12 @@ package sqlite
import (
"database/sql"
"fmt"
"regexp"
sqlite "github.com/mattn/go-sqlite3"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util/errors"
)
func init() {
@ -15,6 +17,9 @@ func init() {
if err := conn.RegisterFunc("mention_query", buildMentionQuery, true); err != nil {
return err
}
if err := conn.RegisterFunc("regexp", regexp.MatchString, true); err != nil {
return err
}
return nil
},
})
@ -75,133 +80,159 @@ func (db *DB) migrate() error {
return err
}
needsReindexing := false
if version <= 0 {
err = tx.ExecStmts([]string{
// Notes
`CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
path TEXT NOT NULL,
sortable_path TEXT NOT NULL,
title TEXT DEFAULT('') NOT NULL,
lead TEXT DEFAULT('') NOT NULL,
body TEXT DEFAULT('') NOT NULL,
raw_content TEXT DEFAULT('') NOT NULL,
word_count INTEGER DEFAULT(0) NOT NULL,
checksum TEXT NOT NULL,
created DATETIME DEFAULT(CURRENT_TIMESTAMP) NOT NULL,
modified DATETIME DEFAULT(CURRENT_TIMESTAMP) NOT NULL,
UNIQUE(path)
)`,
`CREATE INDEX IF NOT EXISTS index_notes_checksum ON notes (checksum)`,
`CREATE INDEX IF NOT EXISTS index_notes_path ON notes (path)`,
// Links
`CREATE TABLE IF NOT EXISTS links (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
source_id INTEGER NOT NULL REFERENCES notes(id)
ON DELETE CASCADE,
target_id INTEGER REFERENCES notes(id)
ON DELETE SET NULL,
title TEXT DEFAULT('') NOT NULL,
href TEXT NOT NULL,
external INT DEFAULT(0) NOT NULL,
rels TEXT DEFAULT('') NOT NULL,
snippet TEXT DEFAULT('') NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS index_links_source_id_target_id ON links (source_id, target_id)`,
// FTS index
`CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
path, title, body,
content = notes,
content_rowid = id,
tokenize = "porter unicode61 remove_diacritics 1 tokenchars '''&/'"
)`,
// Triggers to keep the FTS index up to date.
`CREATE TRIGGER IF NOT EXISTS trigger_notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, path, title, body) VALUES (new.id, new.path, new.title, new.body);
END`,
`CREATE TRIGGER IF NOT EXISTS trigger_notes_ad AFTER DELETE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, path, title, body) VALUES('delete', old.id, old.path, old.title, old.body);
END`,
`CREATE TRIGGER IF NOT EXISTS trigger_notes_au AFTER UPDATE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, path, title, body) VALUES('delete', old.id, old.path, old.title, old.body);
INSERT INTO notes_fts(rowid, path, title, body) VALUES (new.id, new.path, new.title, new.body);
END`,
`PRAGMA user_version = 1`,
})
if err != nil {
return err
}
migrations := []struct {
SQL []string
NeedsReindexing bool
}{
{ // 1
SQL: []string{
// Notes
`CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
path TEXT NOT NULL,
sortable_path TEXT NOT NULL,
title TEXT DEFAULT('') NOT NULL,
lead TEXT DEFAULT('') NOT NULL,
body TEXT DEFAULT('') NOT NULL,
raw_content TEXT DEFAULT('') NOT NULL,
word_count INTEGER DEFAULT(0) NOT NULL,
checksum TEXT NOT NULL,
created DATETIME DEFAULT(CURRENT_TIMESTAMP) NOT NULL,
modified DATETIME DEFAULT(CURRENT_TIMESTAMP) NOT NULL,
UNIQUE(path)
)`,
`CREATE INDEX IF NOT EXISTS index_notes_checksum ON notes (checksum)`,
`CREATE INDEX IF NOT EXISTS index_notes_path ON notes (path)`,
// Links
`CREATE TABLE IF NOT EXISTS links (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
source_id INTEGER NOT NULL REFERENCES notes(id)
ON DELETE CASCADE,
target_id INTEGER REFERENCES notes(id)
ON DELETE SET NULL,
title TEXT DEFAULT('') NOT NULL,
href TEXT NOT NULL,
external INT DEFAULT(0) NOT NULL,
rels TEXT DEFAULT('') NOT NULL,
snippet TEXT DEFAULT('') NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS index_links_source_id_target_id ON links (source_id, target_id)`,
// FTS index
`CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
path, title, body,
content = notes,
content_rowid = id,
tokenize = "porter unicode61 remove_diacritics 1 tokenchars '''&/'"
)`,
// Triggers to keep the FTS index up to date.
`CREATE TRIGGER IF NOT EXISTS trigger_notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, path, title, body) VALUES (new.id, new.path, new.title, new.body);
END`,
`CREATE TRIGGER IF NOT EXISTS trigger_notes_ad AFTER DELETE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, path, title, body) VALUES('delete', old.id, old.path, old.title, old.body);
END`,
`CREATE TRIGGER IF NOT EXISTS trigger_notes_au AFTER UPDATE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, path, title, body) VALUES('delete', old.id, old.path, old.title, old.body);
INSERT INTO notes_fts(rowid, path, title, body) VALUES (new.id, new.path, new.title, new.body);
END`,
},
},
{ // 2
SQL: []string{
// Collections
`CREATE TABLE IF NOT EXISTS collections (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
kind TEXT NO NULL,
name TEXT NOT NULL,
UNIQUE(kind, name)
)`,
`CREATE INDEX IF NOT EXISTS index_collections ON collections (kind, name)`,
// Note-Collection association
`CREATE TABLE IF NOT EXISTS notes_collections (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
note_id INTEGER NOT NULL REFERENCES notes(id)
ON DELETE CASCADE,
collection_id INTEGER NOT NULL REFERENCES collections(id)
ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS index_notes_collections ON notes_collections (note_id, collection_id)`,
// View of notes with their associated metadata (e.g. tags), for simpler queries.
`CREATE VIEW notes_with_metadata AS
SELECT n.*, GROUP_CONCAT(c.name, '` + "\x01" + `') AS tags
FROM notes n
LEFT JOIN notes_collections nc ON nc.note_id = n.id
LEFT JOIN collections c ON nc.collection_id = c.id AND c.kind = '` + string(core.CollectionKindTag) + `'
GROUP BY n.id`,
},
},
{ // 3
SQL: []string{
// Add a `metadata` column to `notes`
`ALTER TABLE notes ADD COLUMN metadata TEXT DEFAULT('{}') NOT NULL`,
// Add snippet's start and end offsets to `links`
`ALTER TABLE links ADD COLUMN snippet_start INTEGER DEFAULT(0) NOT NULL`,
`ALTER TABLE links ADD COLUMN snippet_end INTEGER DEFAULT(0) NOT NULL`,
},
NeedsReindexing: true,
},
{ // 4
SQL: []string{
// Metadata
`CREATE TABLE IF NOT EXISTS metadata (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NO NULL
)`,
},
},
{ // 5
SQL: []string{
// Add a `type` column to `links`
`ALTER TABLE links ADD COLUMN type TEXT DEFAULT('') NOT NULL`,
},
NeedsReindexing: true,
},
{ // 6
SQL: []string{
// View of links with the source and target notes metadata, for simpler queries.
`CREATE VIEW resolved_links AS
SELECT l.*, s.path AS source_path, s.title AS source_title, t.path AS target_path, t.title AS target_title
FROM links l
LEFT JOIN notes s ON l.source_id = s.id
LEFT JOIN notes t ON l.target_id = t.id`,
},
},
{ // 7
SQL: []string{},
// https://github.com/zk-org/zk/issues/170#issuecomment-1107848441
NeedsReindexing: true,
},
}
if version <= 1 {
err = tx.ExecStmts([]string{
// Collections
`CREATE TABLE IF NOT EXISTS collections (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
kind TEXT NO NULL,
name TEXT NOT NULL,
UNIQUE(kind, name)
)`,
`CREATE INDEX IF NOT EXISTS index_collections ON collections (kind, name)`,
// Note-Collection association
`CREATE TABLE IF NOT EXISTS notes_collections (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
note_id INTEGER NOT NULL REFERENCES notes(id)
ON DELETE CASCADE,
collection_id INTEGER NOT NULL REFERENCES collections(id)
ON DELETE CASCADE
)`,
`CREATE INDEX IF NOT EXISTS index_notes_collections ON notes_collections (note_id, collection_id)`,
// View of notes with their associated metadata (e.g. tags), for simpler queries.
`CREATE VIEW notes_with_metadata AS
SELECT n.*, GROUP_CONCAT(c.name, '` + "\x01" + `') AS tags
FROM notes n
LEFT JOIN notes_collections nc ON nc.note_id = n.id
LEFT JOIN collections c ON nc.collection_id = c.id AND c.kind = '` + string(core.CollectionKindTag) + `'
GROUP BY n.id`,
`PRAGMA user_version = 2`,
})
if err != nil {
return err
}
}
if version <= 2 {
err = tx.ExecStmts([]string{
// Add a `metadata` column to `notes`
`ALTER TABLE notes ADD COLUMN metadata TEXT DEFAULT('{}') NOT NULL`,
// Add snippet's start and end offsets to `links`
`ALTER TABLE links ADD COLUMN snippet_start INTEGER DEFAULT(0) NOT NULL`,
`ALTER TABLE links ADD COLUMN snippet_end INTEGER DEFAULT(0) NOT NULL`,
needsReindexing := false
`PRAGMA user_version = 3`,
})
if err != nil {
return err
for i, migration := range migrations {
if version > i {
continue
}
needsReindexing = true
}
if version <= 3 {
err = tx.ExecStmts([]string{
// Metadata
`CREATE TABLE IF NOT EXISTS metadata (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NO NULL
)`,
})
stmts := append(migration.SQL, fmt.Sprintf("PRAGMA user_version = %d", i+1))
err = tx.ExecStmts(stmts)
if err != nil {
return err
}
needsReindexing = needsReindexing || migration.NeedsReindexing
}
if needsReindexing {

@ -3,8 +3,8 @@ package sqlite
import (
"testing"
"github.com/mickael-menu/zk/internal/util/fixtures"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/util/fixtures"
"github.com/zk-org/zk/internal/util/test/assert"
)
func TestOpen(t *testing.T) {
@ -27,7 +27,7 @@ func TestMigrateFrom0(t *testing.T) {
var version int
err := tx.QueryRow("PRAGMA user_version").Scan(&version)
assert.Nil(t, err)
assert.Equal(t, version, 3)
assert.Equal(t, version, 7)
_, err = tx.Exec(`
INSERT INTO notes (path, sortable_path, title, body, word_count, checksum)

@ -0,0 +1,171 @@
package sqlite
import (
"database/sql"
"fmt"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
)
// LinkDAO persists links in the SQLite database.
type LinkDAO struct {
tx Transaction
logger util.Logger
// Prepared SQL statements
addLinkStmt *LazyStmt
removeLinksStmt *LazyStmt
updateTargetIDStmt *LazyStmt
}
// NewLinkDAO creates a new instance of a DAO working on the given database
// transaction.
func NewLinkDAO(tx Transaction, logger util.Logger) *LinkDAO {
return &LinkDAO{
tx: tx,
logger: logger,
// Add a new link.
addLinkStmt: tx.PrepareLazy(`
INSERT INTO links (source_id, target_id, title, href, type, external, rels, snippet, snippet_start, snippet_end)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`),
// Remove all the outbound links of a note.
removeLinksStmt: tx.PrepareLazy(`
DELETE FROM links
WHERE source_id = ?
`),
updateTargetIDStmt: tx.PrepareLazy(`
UPDATE links
SET target_id = ?
WHERE id = ?
`),
}
}
// Add inserts all the outbound links of the given note.
func (d *LinkDAO) Add(links []core.ResolvedLink) error {
for _, link := range links {
sourceID := noteIDToSQL(link.SourceID)
targetID := noteIDToSQL(link.TargetID)
_, err := d.addLinkStmt.Exec(sourceID, targetID, link.Title, link.Href, link.Type, link.IsExternal, joinLinkRels(link.Rels), link.Snippet, link.SnippetStart, link.SnippetEnd)
if err != nil {
return err
}
}
return nil
}
// RemoveAll removes all the outbound links of the given note.
func (d *LinkDAO) RemoveAll(id core.NoteID) error {
_, err := d.removeLinksStmt.Exec(noteIDToSQL(id))
return err
}
// SetTargetID updates the target note of a link.
func (d *LinkDAO) SetTargetID(id core.LinkID, targetID core.NoteID) error {
_, err := d.updateTargetIDStmt.Exec(noteIDToSQL(targetID), linkIDToSQL(id))
return err
}
// joinLinkRels will concatenate a list of rels into a SQLite ready string.
// Each rel is delimited by \x01 for easy matching in queries.
func joinLinkRels(rels []core.LinkRelation) string {
if len(rels) == 0 {
return ""
}
delimiter := "\x01"
res := delimiter
for _, rel := range rels {
res += string(rel) + delimiter
}
return res
}
// FindInternal returns all the links internal to the notebook.
func (d *LinkDAO) FindInternal() ([]core.ResolvedLink, error) {
return d.findWhere("external = 0")
}
// FindBetweenNotes returns all the links existing between the given notes.
func (d *LinkDAO) FindBetweenNotes(ids []core.NoteID) ([]core.ResolvedLink, error) {
idsString := joinNoteIDs(ids, ",")
return d.findWhere(fmt.Sprintf("source_id IN (%s) AND target_id IN (%s)", idsString, idsString))
}
// findWhere returns all the links, filtered by the given where query.
func (d *LinkDAO) findWhere(where string) ([]core.ResolvedLink, error) {
links := make([]core.ResolvedLink, 0)
query := `
SELECT id, source_id, source_path, target_id, target_path, title, href, type, external, rels, snippet, snippet_start, snippet_end
FROM resolved_links
`
if where != "" {
query += "\nWHERE " + where
}
rows, err := d.tx.Query(query)
if err != nil {
return links, err
}
defer rows.Close()
for rows.Next() {
link, err := d.scanLink(rows)
if err != nil {
d.logger.Err(err)
continue
}
if link != nil {
links = append(links, *link)
}
}
return links, nil
}
func (d *LinkDAO) scanLink(row RowScanner) (*core.ResolvedLink, error) {
var (
id, sourceID, snippetStart, snippetEnd int
targetID sql.NullInt64
sourcePath, title, href, linkType, snippet string
external bool
targetPath, rels sql.NullString
)
err := row.Scan(
&id, &sourceID, &sourcePath, &targetID, &targetPath, &title, &href,
&linkType, &external, &rels, &snippet, &snippetStart, &snippetEnd,
)
switch {
case err == sql.ErrNoRows:
return nil, nil
case err != nil:
return nil, err
default:
return &core.ResolvedLink{
ID: core.LinkID(id),
SourceID: core.NoteID(sourceID),
SourcePath: sourcePath,
TargetID: core.NoteID(targetID.Int64),
TargetPath: targetPath.String,
Link: core.Link{
Title: title,
Href: href,
Type: core.LinkType(linkType),
IsExternal: external,
Rels: core.LinkRels(parseListFromNullString(rels)...),
Snippet: snippet,
SnippetStart: snippetStart,
SnippetEnd: snippetEnd,
},
}, nil
}
}

@ -0,0 +1,53 @@
package sqlite
import (
"fmt"
"testing"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/test/assert"
)
func testLinkDAO(t *testing.T, callback func(tx Transaction, dao *LinkDAO)) {
testTransaction(t, func(tx Transaction) {
callback(tx, NewLinkDAO(tx, &util.NullLogger))
})
}
type linkRow struct {
SourceId core.NoteID
TargetId *core.NoteID
Href, Type, Title, Rels, Snippet string
SnippetStart, SnippetEnd int
IsExternal bool
}
func queryLinkRows(t *testing.T, q RowQuerier, where string) []linkRow {
links := make([]linkRow, 0)
rows, err := q.Query(fmt.Sprintf(`
SELECT source_id, target_id, title, href, type, external, rels, snippet, snippet_start, snippet_end
FROM links
WHERE %v
ORDER BY id
`, where))
assert.Nil(t, err)
for rows.Next() {
var row linkRow
var sourceId int64
var targetId *int64
err = rows.Scan(&sourceId, &targetId, &row.Title, &row.Href, &row.Type, &row.IsExternal, &row.Rels, &row.Snippet, &row.SnippetStart, &row.SnippetEnd)
assert.Nil(t, err)
row.SourceId = core.NoteID(sourceId)
if targetId != nil {
row.TargetId = idPointer(*targetId)
}
links = append(links, row)
}
rows.Close()
assert.Nil(t, rows.Err())
return links
}

@ -3,7 +3,7 @@ package sqlite
import (
"database/sql"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/errors"
)
// Known metadata keys.

@ -3,7 +3,7 @@ package sqlite
import (
"testing"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/util/test/assert"
)
func TestMetadataDAOGetUnknown(t *testing.T) {

@ -5,18 +5,15 @@ import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/fts5"
"github.com/mickael-menu/zk/internal/util/icu"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/paths"
strutil "github.com/mickael-menu/zk/internal/util/strings"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/fts5"
"github.com/zk-org/zk/internal/util/paths"
strutil "github.com/zk-org/zk/internal/util/strings"
)
// NoteDAO persists notes in the SQLite database.
@ -30,11 +27,8 @@ type NoteDAO struct {
updateStmt *LazyStmt
removeStmt *LazyStmt
findIdByPathStmt *LazyStmt
findIdByPathPrefixStmt *LazyStmt
findIdsByPathRegexStmt *LazyStmt
findByIdStmt *LazyStmt
addLinkStmt *LazyStmt
setLinksTargetStmt *LazyStmt
removeLinksStmt *LazyStmt
}
// NewNoteDAO creates a new instance of a DAO working on the given database
@ -75,10 +69,12 @@ func NewNoteDAO(tx Transaction, logger util.Logger) *NoteDAO {
WHERE path = ?
`),
// Find a note ID from a prefix of its path.
findIdByPathPrefixStmt: tx.PrepareLazy(`
// Find note IDs from a regex matching their path.
findIdsByPathRegexStmt: tx.PrepareLazy(`
SELECT id FROM notes
WHERE path LIKE ? || '%'
WHERE path REGEXP ?
-- To find the best match possible, we sort by path length.
-- See https://github.com/zk-org/zk/issues/23
ORDER BY LENGTH(path) ASC
`),
@ -88,26 +84,6 @@ func NewNoteDAO(tx Transaction, logger util.Logger) *NoteDAO {
FROM notes_with_metadata
WHERE id = ?
`),
// Add a new link.
addLinkStmt: tx.PrepareLazy(`
INSERT INTO links (source_id, target_id, title, href, external, rels, snippet, snippet_start, snippet_end)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`),
// Set links matching a given href and missing a target ID to the given
// target ID.
setLinksTargetStmt: tx.PrepareLazy(`
UPDATE links
SET target_id = ?
WHERE target_id IS NULL AND external = 0 AND ? LIKE href || '%'
`),
// Remove all the outbound links of a note.
removeLinksStmt: tx.PrepareLazy(`
DELETE FROM links
WHERE source_id = ?
`),
}
}
@ -173,14 +149,12 @@ func (d *NoteDAO) Add(note core.Note) (core.NoteID, error) {
return 0, err
}
id := core.NoteID(lastId)
err = d.addLinks(id, note)
return id, err
return core.NoteID(lastId), err
}
// Update modifies an existing note.
func (d *NoteDAO) Update(note core.Note) (core.NoteID, error) {
id, err := d.findIdByPath(note.Path)
id, err := d.FindIdByPath(note.Path)
if err != nil {
return 0, err
}
@ -193,16 +167,6 @@ func (d *NoteDAO) Update(note core.Note) (core.NoteID, error) {
note.Title, note.Lead, note.Body, note.RawContent, note.WordCount,
metadata, note.Checksum, note.Modified, note.Path,
)
if err != nil {
return id, err
}
_, err = d.removeLinksStmt.Exec(d.idToSql(id))
if err != nil {
return id, err
}
err = d.addLinks(id, note)
return id, err
}
@ -217,41 +181,9 @@ func (d *NoteDAO) metadataToJSON(note core.Note) string {
return string(json)
}
// addLinks inserts all the outbound links of the given note.
func (d *NoteDAO) addLinks(id core.NoteID, note core.Note) error {
for _, link := range note.Links {
targetId, err := d.findIdByPathPrefix(link.Href)
if err != nil {
return err
}
_, err = d.addLinkStmt.Exec(id, d.idToSql(targetId), link.Title, link.Href, link.IsExternal, joinLinkRels(link.Rels), link.Snippet, link.SnippetStart, link.SnippetEnd)
if err != nil {
return err
}
}
_, err := d.setLinksTargetStmt.Exec(int64(id), note.Path)
return err
}
// joinLinkRels will concatenate a list of rels into a SQLite ready string.
// Each rel is delimited by \x01 for easy matching in queries.
func joinLinkRels(rels []core.LinkRelation) string {
if len(rels) == 0 {
return ""
}
delimiter := "\x01"
res := delimiter
for _, rel := range rels {
res += string(rel) + delimiter
}
return res
}
// Remove deletes the note with the given path from the index.
func (d *NoteDAO) Remove(path string) error {
id, err := d.findIdByPath(path)
id, err := d.FindIdByPath(path)
if err != nil {
return err
}
@ -263,7 +195,7 @@ func (d *NoteDAO) Remove(path string) error {
return err
}
func (d *NoteDAO) findIdByPath(path string) (core.NoteID, error) {
func (d *NoteDAO) FindIdByPath(path string) (core.NoteID, error) {
row, err := d.findIdByPathStmt.QueryRow(path)
if err != nil {
return core.NoteID(0), err
@ -271,35 +203,49 @@ func (d *NoteDAO) findIdByPath(path string) (core.NoteID, error) {
return idForRow(row)
}
func (d *NoteDAO) findIdsByPathPrefixes(paths []string) ([]core.NoteID, error) {
ids := make([]core.NoteID, 0)
for _, path := range paths {
id, err := d.findIdByPathPrefix(path)
func idForRow(row *sql.Row) (core.NoteID, error) {
var id sql.NullInt64
err := row.Scan(&id)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return core.NoteID(id.Int64), nil
}
}
func (d *NoteDAO) findIdsByPathRegex(regex string) ([]core.NoteID, error) {
ids := []core.NoteID{}
rows, err := d.findIdsByPathRegexStmt.Query(regex)
if err != nil {
return ids, err
}
defer rows.Close()
for rows.Next() {
var id sql.NullInt64
err := rows.Scan(&id)
if err != nil {
return ids, err
}
if id.IsValid() {
ids = append(ids, id)
}
}
if len(ids) == 0 {
return ids, fmt.Errorf("could not find notes at: " + strings.Join(paths, ", "))
ids = append(ids, core.NoteID(id.Int64))
}
return ids, nil
}
func (d *NoteDAO) findIdByPathPrefix(path string) (core.NoteID, error) {
row, err := d.findIdByPathPrefixStmt.QueryRow(path)
func (d *NoteDAO) findIdWithStmt(stmt *LazyStmt, args ...interface{}) (core.NoteID, error) {
row, err := stmt.QueryRow(args...)
if err != nil {
return 0, err
return core.NoteID(0), err
}
return idForRow(row)
}
func idForRow(row *sql.Row) (core.NoteID, error) {
var id sql.NullInt64
err := row.Scan(&id)
err = row.Scan(&id)
switch {
case err == sql.ErrNoRows:
@ -311,6 +257,54 @@ func idForRow(row *sql.Row) (core.NoteID, error) {
}
}
func (d *NoteDAO) FindIdByHref(href string, allowPartialHref bool) (core.NoteID, error) {
ids, err := d.FindIdsByHref(href, allowPartialHref)
if len(ids) == 0 || err != nil {
return 0, err
}
return ids[0], nil
}
func (d *NoteDAO) findIdsByHrefs(hrefs []string, allowPartialHrefs bool) ([]core.NoteID, error) {
ids := make([]core.NoteID, 0)
for _, href := range hrefs {
cids, err := d.FindIdsByHref(href, allowPartialHrefs)
if err != nil {
return ids, err
}
ids = append(ids, cids...)
}
return ids, nil
}
// FIXME: This logic is duplicated in NoteIndex.linkMatchesPath(). Maybe there's a way to share it using a custom SQLite function?
func (d *NoteDAO) FindIdsByHref(href string, allowPartialHref bool) ([]core.NoteID, error) {
// Remove any anchor at the end of the HREF, since it's most likely
// matching a sub-section in the note.
href = strings.SplitN(href, "#", 2)[0]
href = regexp.QuoteMeta(href)
if allowPartialHref {
ids, err := d.findIdsByPathRegex("^(.*/)?[^/]*" + href + "[^/]*$")
if len(ids) > 0 || err != nil {
return ids, err
}
ids, err = d.findIdsByPathRegex(".*" + href + ".*")
if len(ids) > 0 || err != nil {
return ids, err
}
}
ids, err := d.findIdsByPathRegex("^(?:" + href + "[^/]*|" + href + "/.+)$")
if len(ids) > 0 || err != nil {
return ids, err
}
return []core.NoteID{}, nil
}
func (d *NoteDAO) FindMinimal(opts core.NoteFindOpts) ([]core.MinimalNote, error) {
notes := make([]core.MinimalNote, 0)
@ -319,7 +313,7 @@ func (d *NoteDAO) FindMinimal(opts core.NoteFindOpts) ([]core.MinimalNote, error
return notes, err
}
rows, err := d.findRows(opts, true)
rows, err := d.findRows(opts, noteSelectionMinimal)
if err != nil {
return notes, err
}
@ -339,27 +333,6 @@ func (d *NoteDAO) FindMinimal(opts core.NoteFindOpts) ([]core.MinimalNote, error
return notes, nil
}
func (d *NoteDAO) scanMinimalNote(row RowScanner) (*core.MinimalNote, error) {
var (
id int
path, title string
)
err := row.Scan(&id, &path, &title)
switch {
case err == sql.ErrNoRows:
return nil, nil
case err != nil:
return nil, err
default:
return &core.MinimalNote{
ID: core.NoteID(id),
Path: path,
Title: title,
}, nil
}
}
// Find returns all the notes matching the given criteria.
func (d *NoteDAO) Find(opts core.NoteFindOpts) ([]core.ContextualNote, error) {
notes := make([]core.ContextualNote, 0)
@ -369,7 +342,7 @@ func (d *NoteDAO) Find(opts core.NoteFindOpts) ([]core.ContextualNote, error) {
return notes, err
}
rows, err := d.findRows(opts, false)
rows, err := d.findRows(opts, noteSelectionFull)
if err != nil {
return notes, err
}
@ -389,57 +362,13 @@ func (d *NoteDAO) Find(opts core.NoteFindOpts) ([]core.ContextualNote, error) {
return notes, nil
}
func (d *NoteDAO) scanNote(row RowScanner) (*core.ContextualNote, error) {
var (
id, wordCount int
title, lead, body, rawContent string
snippets, tags sql.NullString
path, metadataJSON, checksum string
created, modified time.Time
)
err := row.Scan(
&id, &path, &title, &lead, &body, &rawContent, &wordCount,
&created, &modified, &metadataJSON, &checksum, &tags, &snippets,
)
switch {
case err == sql.ErrNoRows:
return nil, nil
case err != nil:
return nil, err
default:
metadata, err := unmarshalMetadata(metadataJSON)
if err != nil {
d.logger.Err(errors.Wrap(err, path))
}
return &core.ContextualNote{
Snippets: parseListFromNullString(snippets),
Note: core.Note{
ID: core.NoteID(id),
Path: path,
Title: title,
Lead: lead,
Body: body,
RawContent: rawContent,
WordCount: wordCount,
Links: []core.Link{},
Tags: parseListFromNullString(tags),
Metadata: metadata,
Created: created,
Modified: modified,
Checksum: checksum,
},
}, nil
}
}
// parseListFromNullString splits a 0-separated string.
func parseListFromNullString(str sql.NullString) []string {
list := []string{}
if str.Valid && str.String != "" {
list = strings.Split(str.String, "\x01")
list = strutil.RemoveDuplicates(list)
list = strutil.RemoveBlank(list)
}
return list
}
@ -450,23 +379,24 @@ func (d *NoteDAO) expandMentionsIntoMatch(opts core.NoteFindOpts) (core.NoteFind
if opts.Mention == nil {
return opts, nil
}
if opts.ExactMatch {
return opts, fmt.Errorf("--exact-match and --mention cannot be used together")
if opts.MatchStrategy != core.MatchStrategyFts {
return opts, fmt.Errorf("--mention can only be used with --match-strategy=fts")
}
// Find the IDs for the mentioned paths.
ids, err := d.findIdsByPathPrefixes(opts.Mention)
ids, err := d.findIdsByHrefs(opts.Mention, true /* allowPartialHrefs */)
if err != nil {
return opts, err
}
if len(ids) == 0 {
return opts, fmt.Errorf("could not find notes at: " + strings.Join(opts.Mention, ", "))
}
// Exclude the mentioned notes from the results.
for _, id := range ids {
opts = opts.ExcludingID(id)
}
opts = opts.ExcludingIDs(ids)
// Find their titles.
titlesQuery := "SELECT title, metadata FROM notes WHERE id IN (" + d.joinIds(ids, ",") + ")"
titlesQuery := "SELECT title, metadata FROM notes WHERE id IN (" + joinNoteIDs(ids, ",") + ")"
rows, err := d.tx.Query(titlesQuery)
if err != nil {
return opts, err
@ -490,14 +420,21 @@ func (d *NoteDAO) expandMentionsIntoMatch(opts core.NoteFindOpts) (core.NoteFind
}
// Expand the mention queries in the match predicate.
match := opts.Match.String()
match += " " + strings.Join(mentionQueries, " OR ")
opts.Match = opt.NewString(match)
opts.Match = append(opts.Match, " ("+strings.Join(mentionQueries, " OR ")+") ")
return opts, nil
}
func (d *NoteDAO) findRows(opts core.NoteFindOpts, minimal bool) (*sql.Rows, error) {
// noteSelection represents the amount of column selected with findRows.
type noteSelection int
const (
noteSelectionID noteSelection = iota + 1
noteSelectionMinimal
noteSelectionFull
)
func (d *NoteDAO) findRows(opts core.NoteFindOpts, selection noteSelection) (*sql.Rows, error) {
snippetCol := `n.lead`
joinClauses := []string{}
whereExprs := []string{}
@ -508,42 +445,44 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, minimal bool) (*sql.Rows, err
transitiveClosure := false
maxDistance := 0
setupLinkFilter := func(paths []string, direction int, negate, recursive bool) error {
ids, err := d.findIdsByPathPrefixes(paths)
setupLinkFilter := func(tableAlias string, hrefs []string, direction int, negate, recursive bool) error {
ids, err := d.findIdsByHrefs(hrefs, true /* allowPartialHrefs */)
if err != nil {
return err
}
if len(ids) == 0 {
return nil
return fmt.Errorf("could not find notes at: " + strings.Join(hrefs, ", "))
}
idsList := "(" + d.joinIds(ids, ",") + ")"
idsList := "(" + joinNoteIDs(ids, ",") + ")"
linksSrc := "links"
if recursive {
transitiveClosure = true
linksSrc = "transitive_closure"
additionalOrderTerms = append(additionalOrderTerms, tableAlias+".distance")
}
if !negate {
if direction != 0 {
snippetCol = "GROUP_CONCAT(REPLACE(l.snippet, l.title, '<zk:match>' || l.title || '</zk:match>'), '\x01')"
snippetCol = fmt.Sprintf("GROUP_CONCAT(REPLACE(%s.snippet, %[1]s.title, '<zk:match>' || %[1]s.title || '</zk:match>'), '\x01')", tableAlias)
}
joinOns := make([]string, 0)
if direction <= 0 {
joinOns = append(joinOns, fmt.Sprintf(
"(n.id = l.target_id AND l.source_id IN %s)", idsList,
"(n.id = %[1]s.target_id AND %[1]s.source_id IN %[2]s)", tableAlias, idsList,
))
}
if direction >= 0 {
joinOns = append(joinOns, fmt.Sprintf(
"(n.id = l.source_id AND l.target_id IN %s)", idsList,
"(n.id = %[1]s.source_id AND %[1]s.target_id IN %[2]s)", tableAlias, idsList,
))
}
joinClauses = append(joinClauses, fmt.Sprintf(
"LEFT JOIN %s l ON %s",
"LEFT JOIN %[2]s %[1]s ON %[3]s",
tableAlias,
linksSrc,
strings.Join(joinOns, " OR "),
))
@ -577,35 +516,44 @@ func (d *NoteDAO) findRows(opts core.NoteFindOpts, minimal bool) (*sql.Rows, err
return nil
}
if !opts.Match.IsNull() {
if opts.ExactMatch {
whereExprs = append(whereExprs, `n.raw_content LIKE '%' || ? || '%' ESCAPE '\'`)
args = append(args, escapeLikeTerm(opts.Match.String(), '\\'))
} else {
if 0 < len(opts.Match) {
switch opts.MatchStrategy {
case core.MatchStrategyExact:
for _, match := range opts.Match {
whereExprs = append(whereExprs, `n.raw_content LIKE '%' || ? || '%' ESCAPE '\'`)
args = append(args, escapeLikeTerm(match, '\\'))
}
case core.MatchStrategyFts:
snippetCol = `snippet(fts_match.notes_fts, 2, '<zk:match>', '</zk:match>', '…', 20)`
joinClauses = append(joinClauses, "JOIN notes_fts fts_match ON n.id = fts_match.rowid")
additionalOrderTerms = append(additionalOrderTerms, `bm25(fts_match.notes_fts, 1000.0, 500.0, 1.0)`)
whereExprs = append(whereExprs, "fts_match.notes_fts MATCH ?")
args = append(args, fts5.ConvertQuery(opts.Match.String()))
for _, match := range opts.Match {
whereExprs = append(whereExprs, "fts_match.notes_fts MATCH ?")
args = append(args, fts5.ConvertQuery(match))
}
case core.MatchStrategyRe:
for _, match := range opts.Match {
whereExprs = append(whereExprs, "n.raw_content REGEXP ?")
args = append(args, match)
}
break
}
}
if opts.IncludePaths != nil {
regexes := make([]string, 0)
for _, path := range opts.IncludePaths {
regexes = append(regexes, "n.path REGEXP ?")
args = append(args, pathRegex(path))
if opts.IncludeHrefs != nil {
ids, err := d.findIdsByHrefs(opts.IncludeHrefs, opts.AllowPartialHrefs)
if err != nil {
return nil, err
}
whereExprs = append(whereExprs, strings.Join(regexes, " OR "))
opts = opts.IncludingIDs(ids)
}
if opts.ExcludePaths != nil {
regexes := make([]string, 0)
for _, path := range opts.ExcludePaths {
regexes = append(regexes, "n.path NOT REGEXP ?")
args = append(args, pathRegex(path))
if opts.ExcludeHrefs != nil {
ids, err := d.findIdsByHrefs(opts.ExcludeHrefs, opts.AllowPartialHrefs)
if err != nil {
return nil, err
}
whereExprs = append(whereExprs, strings.Join(regexes, " AND "))
opts = opts.ExcludingIDs(ids)
}
if opts.Tags != nil {
@ -657,24 +605,25 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
}
if opts.MentionedBy != nil {
ids, err := d.findIdsByPathPrefixes(opts.MentionedBy)
ids, err := d.findIdsByHrefs(opts.MentionedBy, true /* allowPartialHrefs */)
if err != nil {
return nil, err
}
if len(ids) == 0 {
return nil, fmt.Errorf("could not find notes at: " + strings.Join(opts.MentionedBy, ", "))
}
// Exclude the mentioning notes from the results.
for _, id := range ids {
opts = opts.ExcludingID(id)
}
opts = opts.ExcludingIDs(ids)
snippetCol = `snippet(nsrc.notes_fts, 2, '<zk:match>', '</zk:match>', '…', 20)`
joinClauses = append(joinClauses, "JOIN notes_fts nsrc ON nsrc.rowid IN ("+d.joinIds(ids, ",")+") AND nsrc.notes_fts MATCH mention_query(n.title, n.metadata)")
joinClauses = append(joinClauses, "JOIN notes_fts nsrc ON nsrc.rowid IN ("+joinNoteIDs(ids, ",")+") AND nsrc.notes_fts MATCH mention_query(n.title, n.metadata)")
}
if opts.LinkedBy != nil {
filter := opts.LinkedBy
maxDistance = filter.MaxDistance
err := setupLinkFilter(filter.Paths, -1, filter.Negate, filter.Recursive)
err := setupLinkFilter("l_by", filter.Hrefs, -1, filter.Negate, filter.Recursive)
if err != nil {
return nil, err
}
@ -683,7 +632,7 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
if opts.LinkTo != nil {
filter := opts.LinkTo
maxDistance = filter.MaxDistance
err := setupLinkFilter(filter.Paths, 1, filter.Negate, filter.Recursive)
err := setupLinkFilter("l_to", filter.Hrefs, 1, filter.Negate, filter.Recursive)
if err != nil {
return nil, err
}
@ -691,11 +640,11 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
if opts.Related != nil {
maxDistance = 2
err := setupLinkFilter(opts.Related, 0, false, true)
err := setupLinkFilter("l_rel", opts.Related, 0, false, true)
if err != nil {
return nil, err
}
groupBy += " HAVING MIN(l.distance) = 2"
groupBy += " HAVING MIN(l_rel.distance) = 2"
}
if opts.Orphan {
@ -724,8 +673,12 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
args = append(args, opts.ModifiedEnd)
}
if opts.IncludeIDs != nil {
whereExprs = append(whereExprs, "n.id IN ("+joinNoteIDs(opts.IncludeIDs, ",")+")")
}
if opts.ExcludeIDs != nil {
whereExprs = append(whereExprs, "n.id NOT IN ("+d.joinIds(opts.ExcludeIDs, ",")+")")
whereExprs = append(whereExprs, "n.id NOT IN ("+joinNoteIDs(opts.ExcludeIDs, ",")+")")
}
orderTerms := []string{}
@ -739,8 +692,6 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
// Credit to https://inviqa.com/blog/storing-graphs-database-sql-meets-social-network
if transitiveClosure {
orderTerms = append([]string{"l.distance"}, orderTerms...)
query += `WITH RECURSIVE transitive_closure(source_id, target_id, title, snippet, distance, path) AS (
SELECT source_id, target_id, title, snippet,
1 AS distance,
@ -767,9 +718,12 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
query += "\n)\n"
}
query += "SELECT n.id, n.path, n.title"
if !minimal {
query += fmt.Sprintf(", n.lead, n.body, n.raw_content, n.word_count, n.created, n.modified, n.metadata, n.checksum, n.tags, %s AS snippet", snippetCol)
query += "SELECT n.id"
if selection != noteSelectionID {
query += ", n.path, n.title, n.metadata"
if selection != noteSelectionMinimal {
query += fmt.Sprintf(", n.lead, n.body, n.raw_content, n.word_count, n.created, n.modified, n.checksum, n.tags, %s AS snippet", snippetCol)
}
}
query += "\nFROM notes_with_metadata n\n"
@ -798,6 +752,91 @@ WHERE collection_id IN (SELECT id FROM collections t WHERE kind = '%s' AND (%s))
return d.tx.Query(query, args...)
}
func (d *NoteDAO) scanNoteID(row RowScanner) (core.NoteID, error) {
var id int
err := row.Scan(&id)
switch {
case err == sql.ErrNoRows:
return 0, nil
case err != nil:
return 0, err
default:
return core.NoteID(id), nil
}
}
func (d *NoteDAO) scanMinimalNote(row RowScanner) (*core.MinimalNote, error) {
var (
id int
path, title, metadataJSON string
)
err := row.Scan(&id, &path, &title, &metadataJSON)
switch {
case err == sql.ErrNoRows:
return nil, nil
case err != nil:
return nil, err
default:
metadata, err := unmarshalMetadata(metadataJSON)
if err != nil {
d.logger.Err(errors.Wrap(err, path))
}
return &core.MinimalNote{
ID: core.NoteID(id),
Path: path,
Title: title,
Metadata: metadata,
}, nil
}
}
func (d *NoteDAO) scanNote(row RowScanner) (*core.ContextualNote, error) {
var (
id, wordCount int
title, lead, body, rawContent string
snippets, tags sql.NullString
path, metadataJSON, checksum string
created, modified time.Time
)
err := row.Scan(
&id, &path, &title, &metadataJSON, &lead, &body, &rawContent,
&wordCount, &created, &modified, &checksum, &tags, &snippets,
)
switch {
case err == sql.ErrNoRows:
return nil, nil
case err != nil:
return nil, err
default:
metadata, err := unmarshalMetadata(metadataJSON)
if err != nil {
d.logger.Err(errors.Wrap(err, path))
}
return &core.ContextualNote{
Snippets: parseListFromNullString(snippets),
Note: core.Note{
ID: core.NoteID(id),
Path: path,
Title: title,
Lead: lead,
Body: body,
RawContent: rawContent,
WordCount: wordCount,
Links: []core.Link{},
Tags: parseListFromNullString(tags),
Metadata: metadata,
Created: created,
Modified: modified,
Checksum: checksum,
},
}, nil
}
}
func orderTerm(sorter core.NoteSorter) string {
order := " ASC"
if !sorter.Ascending {
@ -817,42 +856,11 @@ func orderTerm(sorter core.NoteSorter) string {
return "n.title" + order
case core.NoteSortWordCount:
return "n.word_count" + order
case core.NoteSortPathLength:
return "LENGTH(path)" + order
default:
panic(fmt.Sprintf("%v: unknown core.NoteSortField", sorter.Field))
}
}
// pathRegex returns an ICU regex to match the files in the folder at given
// `path`, or any file having `path` for prefix.
func pathRegex(path string) string {
path = icu.EscapePattern(path)
return path + "[^/]*|" + path + "/.+"
}
func (d *NoteDAO) idToSql(id core.NoteID) sql.NullInt64 {
if id.IsValid() {
return sql.NullInt64{Int64: int64(id), Valid: true}
} else {
return sql.NullInt64{}
}
}
func (d *NoteDAO) joinIds(ids []core.NoteID, delimiter string) string {
strs := make([]string, 0)
for _, i := range ids {
strs = append(strs, strconv.FormatInt(int64(i), 10))
}
return strings.Join(strs, delimiter)
}
func unmarshalMetadata(metadataJSON string) (metadata map[string]interface{}, err error) {
err = json.Unmarshal([]byte(metadataJSON), &metadata)
err = errors.Wrapf(err, "cannot parse note metadata from JSON: %s", metadataJSON)
return
}
// buildMentionQuery creates an FTS5 predicate to match the given note's title
// (or aliases from the metadata) in the content of another note.
//
@ -894,7 +902,3 @@ func buildMentionQuery(title, metadataJSON string) string {
return "(" + strings.Join(titles, " OR ") + ")"
}
type RowScanner interface {
Scan(dest ...interface{}) error
}

@ -6,11 +6,11 @@ import (
"testing"
"time"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/paths"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/opt"
"github.com/zk-org/zk/internal/util/paths"
"github.com/zk-org/zk/internal/util/test/assert"
)
func TestNoteDAOIndexed(t *testing.T) {
@ -135,108 +135,6 @@ func TestNoteDAOAdd(t *testing.T) {
})
}
func TestNoteDAOAddWithLinks(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
id, err := dao.Add(core.Note{
Path: "log/added.md",
Links: []core.Link{
{
Title: "Same dir",
Href: "log/2021-01-04",
Rels: core.LinkRels("rel-1", "rel-2"),
},
{
Title: "Relative",
Href: "f39c8",
Snippet: "[Relative](f39c8) link",
SnippetStart: 50,
SnippetEnd: 100,
},
{
Title: "Second is added",
Href: "f39c8",
Rels: core.LinkRels("second"),
},
{
Title: "Unknown",
Href: "unknown",
},
{
Title: "URL",
Href: "http://example.com",
IsExternal: true,
Snippet: "External [URL](http://example.com)",
},
},
})
assert.Nil(t, err)
rows := queryLinkRows(t, tx, fmt.Sprintf("source_id = %d", id))
assert.Equal(t, rows, []linkRow{
{
SourceId: id,
TargetId: idPointer(2),
Title: "Same dir",
Href: "log/2021-01-04",
Rels: "\x01rel-1\x01rel-2\x01",
},
{
SourceId: id,
TargetId: idPointer(4),
Title: "Relative",
Href: "f39c8",
Rels: "",
Snippet: "[Relative](f39c8) link",
SnippetStart: 50,
SnippetEnd: 100,
},
{
SourceId: id,
TargetId: idPointer(4),
Title: "Second is added",
Href: "f39c8",
Rels: "\x01second\x01",
},
{
SourceId: id,
TargetId: nil,
Title: "Unknown",
Href: "unknown",
Rels: "",
},
{
SourceId: id,
TargetId: nil,
Title: "URL",
Href: "http://example.com",
IsExternal: true,
Rels: "",
Snippet: "External [URL](http://example.com)",
},
})
})
}
func TestNoteDAOAddFillsLinksMissingTargetId(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
id, err := dao.Add(core.Note{
Path: "missing_target.md",
})
assert.Nil(t, err)
rows := queryLinkRows(t, tx, fmt.Sprintf("target_id = %d", id))
assert.Equal(t, rows, []linkRow{
{
SourceId: 3,
TargetId: &id,
Title: "Missing target",
Href: "missing",
Snippet: "There's a Missing target",
},
})
})
}
// Check that we can't add a duplicate note with an existing path.
func TestNoteDAOAddExistingNote(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
@ -288,69 +186,6 @@ func TestNoteDAOUpdateUnknown(t *testing.T) {
})
}
func TestNoteDAOUpdateWithLinks(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
links := queryLinkRows(t, tx, "source_id = 1")
assert.Equal(t, links, []linkRow{
{
SourceId: 1,
TargetId: idPointer(2),
Title: "An internal link",
Href: "log/2021-01-04.md",
Snippet: "[[An internal link]]",
},
{
SourceId: 1,
TargetId: nil,
Title: "An external link",
Href: "https://domain.com",
IsExternal: true,
Snippet: "[[An external link]]",
},
})
_, err := dao.Update(core.Note{
Path: "log/2021-01-03.md",
Links: []core.Link{
{
Title: "A new link",
Href: "index",
IsExternal: false,
Rels: core.LinkRels("rel"),
Snippet: "[[A new link]]",
},
{
Title: "An external link",
Href: "https://domain.com",
IsExternal: true,
Snippet: "[[An external link]]",
},
},
})
assert.Nil(t, err)
links = queryLinkRows(t, tx, "source_id = 1")
assert.Equal(t, links, []linkRow{
{
SourceId: 1,
TargetId: idPointer(3),
Title: "A new link",
Href: "index",
Rels: "\x01rel\x01",
Snippet: "[[A new link]]",
},
{
SourceId: 1,
TargetId: nil,
Title: "An external link",
Href: "https://domain.com",
IsExternal: true,
Snippet: "[[An external link]]",
},
})
})
}
func TestNoteDAORemove(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
_, err := queryNoteRow(tx, `path = "ref/test/a.md"`)
@ -391,19 +226,85 @@ func TestNoteDAORemoveCascadeLinks(t *testing.T) {
})
}
func TestNoteDAOFindIdsByHref(t *testing.T) {
test := func(href string, allowPartialHref bool, expected []core.NoteID) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
actual, err := dao.FindIdsByHref(href, allowPartialHref)
assert.Nil(t, err)
assert.Equal(t, actual, expected)
})
}
test("test", false, []core.NoteID{})
test("test", true, []core.NoteID{6, 5, 8})
// Filename takes precedence over the rest of the path.
// See https://github.com/zk-org/zk/issues/111
test("ref", true, []core.NoteID{8})
}
func TestNoteDAOFindIncludingHrefs(t *testing.T) {
test := func(href string, allowPartialHref bool, expected []string) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludeHrefs: []string{href},
AllowPartialHrefs: allowPartialHref,
},
expected,
)
}
test("test", false, []string{})
test("test", true, []string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md"})
// Filename takes precedence over the rest of the path.
// See https://github.com/zk-org/zk/issues/111
test("ref", true, []string{"ref/test/ref.md"})
}
func TestNoteDAOFindExcludingHrefs(t *testing.T) {
test := func(href string, allowPartialHref bool, expected []string) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
ExcludeHrefs: []string{href},
AllowPartialHrefs: allowPartialHref,
},
expected,
)
}
test("test", false, []string{"ref/test/ref.md", "ref/test/b.md",
"f39c8.md", "ref/test/a.md", "log/2021-01-03.md", "log/2021-02-04.md",
"index.md", "log/2021-01-04.md"})
test("test", true, []string{"f39c8.md", "log/2021-01-03.md",
"log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
// Filename takes precedence over the rest of the path.
// See https://github.com/zk-org/zk/issues/111
test("ref", true, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md",
"log/2021-01-03.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
}
func TestNoteDAOFindMinimalAll(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
notes, err := dao.FindMinimal(core.NoteFindOpts{})
assert.Nil(t, err)
assert.Equal(t, notes, []core.MinimalNote{
{ID: 5, Path: "ref/test/b.md", Title: "A nested note"},
{ID: 4, Path: "f39c8.md", Title: "An interesting note"},
{ID: 6, Path: "ref/test/a.md", Title: "Another nested note"},
{ID: 1, Path: "log/2021-01-03.md", Title: "Daily note"},
{ID: 7, Path: "log/2021-02-04.md", Title: "February 4, 2021"},
{ID: 3, Path: "index.md", Title: "Index"},
{ID: 2, Path: "log/2021-01-04.md", Title: "January 4, 2021"},
{ID: 8, Path: "ref/test/ref.md", Title: "", Metadata: map[string]interface{}{}},
{ID: 5, Path: "ref/test/b.md", Title: "A nested note", Metadata: map[string]interface{}{}},
{ID: 4, Path: "f39c8.md", Title: "An interesting note", Metadata: map[string]interface{}{}},
{ID: 6, Path: "ref/test/a.md", Title: "Another nested note", Metadata: map[string]interface{}{
"alias": "a.md",
}},
{ID: 1, Path: "log/2021-01-03.md", Title: "Daily note", Metadata: map[string]interface{}{
"author": "Dom",
}},
{ID: 7, Path: "log/2021-02-04.md", Title: "February 4, 2021", Metadata: map[string]interface{}{}},
{ID: 3, Path: "index.md", Title: "Index", Metadata: map[string]interface{}{
"aliases": []interface{}{"First page"},
}},
{ID: 2, Path: "log/2021-01-04.md", Title: "January 4, 2021", Metadata: map[string]interface{}{}},
})
})
}
@ -411,29 +312,35 @@ func TestNoteDAOFindMinimalAll(t *testing.T) {
func TestNoteDAOFindMinimalWithFilter(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
notes, err := dao.FindMinimal(core.NoteFindOpts{
Match: opt.NewString("daily | index"),
Sorters: []core.NoteSorter{{Field: core.NoteSortWordCount, Ascending: true}},
Limit: 3,
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
Sorters: []core.NoteSorter{{Field: core.NoteSortWordCount, Ascending: true}},
Limit: 3,
})
assert.Nil(t, err)
assert.Equal(t, notes, []core.MinimalNote{
{ID: 1, Path: "log/2021-01-03.md", Title: "Daily note"},
{ID: 3, Path: "index.md", Title: "Index"},
{ID: 7, Path: "log/2021-02-04.md", Title: "February 4, 2021"},
{ID: 1, Path: "log/2021-01-03.md", Title: "Daily note", Metadata: map[string]interface{}{
"author": "Dom",
}},
{ID: 3, Path: "index.md", Title: "Index", Metadata: map[string]interface{}{
"aliases": []interface{}{"First page"},
}},
{ID: 7, Path: "log/2021-02-04.md", Title: "February 4, 2021", Metadata: map[string]interface{}{}},
})
})
}
func TestNoteDAOFindAll(t *testing.T) {
testNoteDAOFindPaths(t, core.NoteFindOpts{}, []string{
"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md",
"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md",
"log/2021-02-04.md", "index.md", "log/2021-01-04.md",
})
}
func TestNoteDAOFindLimit(t *testing.T) {
testNoteDAOFindPaths(t, core.NoteFindOpts{Limit: 2}, []string{
testNoteDAOFindPaths(t, core.NoteFindOpts{Limit: 3}, []string{
"ref/test/ref.md",
"ref/test/b.md",
"f39c8.md",
})
@ -453,14 +360,17 @@ func TestNoteDAOFindTag(t *testing.T) {
test([]string{"fiction | adventure | fantasy"}, []string{"ref/test/b.md", "f39c8.md", "log/2021-01-03.md"})
test([]string{"fiction | history", "adventure"}, []string{"ref/test/b.md", "log/2021-01-03.md"})
test([]string{"fiction", "unknown"}, []string{})
test([]string{"-fiction"}, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
test([]string{"NOT fiction"}, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
test([]string{"NOTfiction"}, []string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
test([]string{"-fiction"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
test([]string{"NOT fiction"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
test([]string{"NOTfiction"}, []string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"})
}
func TestNoteDAOFindMatch(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{Match: opt.NewString("daily | index")},
core.NoteFindOpts{
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
},
[]core.ContextualNote{
{
Note: core.Note{
@ -542,10 +452,26 @@ func TestNoteDAOFindMatch(t *testing.T) {
)
}
func TestNoteDAOFindMatchWithMultiMatch(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: []string{"daily | index", "second"},
MatchStrategy: core.MatchStrategyFts,
Sorters: []core.NoteSorter{
{Field: core.NoteSortPath, Ascending: false},
},
},
[]string{
"log/2021-01-04.md",
},
)
}
func TestNoteDAOFindMatchWithSort(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: opt.NewString("daily | index"),
Match: []string{"daily | index"},
MatchStrategy: core.MatchStrategyFts,
Sorters: []core.NoteSorter{
{Field: core.NoteSortPath, Ascending: false},
},
@ -563,8 +489,8 @@ func TestNoteDAOFindExactMatch(t *testing.T) {
test := func(match string, expected []string) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Match: opt.NewString(match),
ExactMatch: true,
Match: []string{match},
MatchStrategy: core.MatchStrategyExact,
},
expected,
)
@ -576,20 +502,34 @@ func TestNoteDAOFindExactMatch(t *testing.T) {
test(`[exact% ch\ar_acters]`, []string{"ref/test/a.md"})
}
func TestNoteDAOFindExactMatchCannotBeUsedWithMention(t *testing.T) {
func TestNoteDAOFindMentionRequiresFtsMatchStrategy(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
_, err := dao.Find(core.NoteFindOpts{
MatchStrategy: core.MatchStrategyExact,
Mention: []string{"mention"},
})
assert.Err(t, err, "--mention can only be used with --match-strategy=fts")
})
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
_, err := dao.Find(core.NoteFindOpts{
ExactMatch: true,
Mention: []string{"mention"},
MatchStrategy: core.MatchStrategyRe,
Mention: []string{"mention"},
})
assert.Err(t, err, "--exact-match and --mention cannot be used together")
assert.Err(t, err, "--mention can only be used with --match-strategy=fts")
})
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
_, err := dao.Find(core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
Mention: []string{"mention"},
})
assert.Err(t, err, "could not find notes at: mention")
})
}
func TestNoteDAOFindInPathAbsoluteFile(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludePaths: []string{"log/2021-01-03.md"},
IncludeHrefs: []string{"log/2021-01-03.md"},
},
[]string{"log/2021-01-03.md"},
)
@ -599,7 +539,7 @@ func TestNoteDAOFindInPathAbsoluteFile(t *testing.T) {
func TestNoteDAOFindInPathWithFilePrefix(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludePaths: []string{"log/2021-01"},
IncludeHrefs: []string{"log/2021-01"},
},
[]string{"log/2021-01-03.md", "log/2021-01-04.md"},
)
@ -609,13 +549,15 @@ func TestNoteDAOFindInPathWithFilePrefix(t *testing.T) {
func TestNoteDAOFindInPathRequiresCompleteDirName(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludePaths: []string{"lo"},
IncludeHrefs: []string{"lo"},
AllowPartialHrefs: false,
},
[]string{},
)
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludePaths: []string{"log"},
IncludeHrefs: []string{"log"},
AllowPartialHrefs: false,
},
[]string{"log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md"},
)
@ -625,25 +567,25 @@ func TestNoteDAOFindInPathRequiresCompleteDirName(t *testing.T) {
func TestNoteDAOFindInMultiplePaths(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
IncludePaths: []string{"ref", "index.md"},
IncludeHrefs: []string{"ref", "index.md"},
},
[]string{"ref/test/b.md", "ref/test/a.md", "index.md"},
[]string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md"},
)
}
func TestNoteDAOFindExcludingPath(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
ExcludePaths: []string{"log"},
ExcludeHrefs: []string{"log"},
},
[]string{"ref/test/b.md", "f39c8.md", "ref/test/a.md", "index.md"},
[]string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "index.md"},
)
}
func TestNoteDAOFindExcludingMultiplePaths(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
ExcludePaths: []string{"ref", "log/2021-01"},
ExcludeHrefs: []string{"ref", "log/2021-01"},
},
[]string{"f39c8.md", "log/2021-02-04.md", "index.md"},
)
@ -651,7 +593,10 @@ func TestNoteDAOFindExcludingMultiplePaths(t *testing.T) {
func TestNoteDAOFindMentions(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{Mention: []string{"log/2021-01-03.md", "index.md"}},
core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
Mention: []string{"log/2021-01-03.md", "index.md"},
},
[]core.ContextualNote{
{
Note: core.Note{
@ -663,7 +608,7 @@ func TestNoteDAOFindMentions(t *testing.T) {
RawContent: "# A nested note\nThis one is in a sub sub directory",
WordCount: 8,
Links: []core.Link{},
Tags: []string{"adventure", "history"},
Tags: []string{"adventure", "history", "science"},
Metadata: map[string]interface{}{},
Created: time.Date(2019, 11, 20, 20, 32, 56, 0, time.UTC),
Modified: time.Date(2019, 11, 20, 20, 34, 6, 0, time.UTC),
@ -715,9 +660,10 @@ func TestNoteDAOFindMentions(t *testing.T) {
func TestNoteDAOFindUnlinkedMentions(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
Mention: []string{"log/2021-01-03.md", "index.md"},
MatchStrategy: core.MatchStrategyFts,
Mention: []string{"log/2021-01-03.md", "index.md"},
LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-03.md", "index.md"},
Hrefs: []string{"log/2021-01-03.md", "index.md"},
Negate: true,
},
},
@ -725,9 +671,23 @@ func TestNoteDAOFindUnlinkedMentions(t *testing.T) {
)
}
func TestNoteDAOFindMentionUnknown(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
opts := core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
Mention: []string{"will-not-be-found"},
}
_, err := dao.Find(opts)
assert.Err(t, err, "could not find notes at: will-not-be-found")
})
}
func TestNoteDAOFindMentionedBy(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"}},
core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"},
},
[]core.ContextualNote{
{
Note: core.Note{
@ -779,9 +739,10 @@ func TestNoteDAOFindMentionedBy(t *testing.T) {
func TestNoteDAOFindUnlinkedMentionedBy(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"},
MatchStrategy: core.MatchStrategyFts,
MentionedBy: []string{"ref/test/b.md", "log/2021-01-04.md"},
LinkedBy: &core.LinkFilter{
Paths: []string{"ref/test/b.md", "log/2021-01-04.md"},
Hrefs: []string{"ref/test/b.md", "log/2021-01-04.md"},
Negate: true,
},
},
@ -789,11 +750,22 @@ func TestNoteDAOFindUnlinkedMentionedBy(t *testing.T) {
)
}
func TestNoteDAOFindMentionedByUnknown(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
opts := core.NoteFindOpts{
MatchStrategy: core.MatchStrategyFts,
MentionedBy: []string{"will-not-be-found"},
}
_, err := dao.Find(opts)
assert.Err(t, err, "could not find notes at: will-not-be-found")
})
}
func TestNoteDAOFindLinkedBy(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{
Paths: []string{"f39c8.md", "log/2021-01-03"},
Hrefs: []string{"f39c8.md", "log/2021-01-03"},
Negate: false,
Recursive: false,
},
@ -806,7 +778,7 @@ func TestNoteDAOFindLinkedByRecursive(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"},
Hrefs: []string{"log/2021-01-04.md"},
Negate: false,
Recursive: true,
},
@ -819,7 +791,7 @@ func TestNoteDAOFindLinkedByRecursiveWithMaxDistance(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"},
Hrefs: []string{"log/2021-01-04.md"},
Negate: false,
Recursive: true,
MaxDistance: 2,
@ -832,7 +804,7 @@ func TestNoteDAOFindLinkedByRecursiveWithMaxDistance(t *testing.T) {
func TestNoteDAOFindLinkedByWithSnippets(t *testing.T) {
testNoteDAOFind(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{Paths: []string{"f39c8.md"}},
LinkedBy: &core.LinkFilter{Hrefs: []string{"f39c8.md"}},
},
[]core.ContextualNote{
{
@ -884,16 +856,28 @@ func TestNoteDAOFindLinkedByWithSnippets(t *testing.T) {
)
}
func TestNoteDAOFindLinkedByUnknown(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
opts := core.NoteFindOpts{
LinkedBy: &core.LinkFilter{
Hrefs: []string{"will-not-be-found"},
},
}
_, err := dao.Find(opts)
assert.Err(t, err, "could not find notes at: will-not-be-found")
})
}
func TestNoteDAOFindNotLinkedBy(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkedBy: &core.LinkFilter{
Paths: []string{"f39c8.md", "log/2021-01-03"},
Hrefs: []string{"f39c8.md", "log/2021-01-03"},
Negate: true,
Recursive: false,
},
},
[]string{"ref/test/b.md", "f39c8.md", "log/2021-02-04.md", "index.md"},
[]string{"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "log/2021-02-04.md", "index.md"},
)
}
@ -901,7 +885,7 @@ func TestNoteDAOFindLinkTo(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-04", "ref/test/a.md"},
Hrefs: []string{"log/2021-01-04", "ref/test/a.md"},
Negate: false,
Recursive: false,
},
@ -914,7 +898,7 @@ func TestNoteDAOFindLinkToRecursive(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"},
Hrefs: []string{"log/2021-01-04.md"},
Negate: false,
Recursive: true,
},
@ -927,7 +911,7 @@ func TestNoteDAOFindLinkToRecursiveWithMaxDistance(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkTo: &core.LinkFilter{
Paths: []string{"log/2021-01-04.md"},
Hrefs: []string{"log/2021-01-04.md"},
Negate: false,
Recursive: true,
MaxDistance: 2,
@ -940,12 +924,24 @@ func TestNoteDAOFindLinkToRecursiveWithMaxDistance(t *testing.T) {
func TestNoteDAOFindNotLinkTo(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
LinkTo: &core.LinkFilter{Paths: []string{"log/2021-01-04", "ref/test/a.md"}, Negate: true},
LinkTo: &core.LinkFilter{Hrefs: []string{"log/2021-01-04", "ref/test/a.md"}, Negate: true},
},
[]string{"ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"},
[]string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md", "index.md", "log/2021-01-04.md"},
)
}
func TestNoteDAOFindLinkToUnknown(t *testing.T) {
testNoteDAO(t, func(tx Transaction, dao *NoteDAO) {
opts := core.NoteFindOpts{
LinkTo: &core.LinkFilter{
Hrefs: []string{"will-not-be-found"},
},
}
_, err := dao.Find(opts)
assert.Err(t, err, "could not find notes at: will-not-be-found")
})
}
func TestNoteDAOFindRelated(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{
@ -965,7 +961,7 @@ func TestNoteDAOFindRelated(t *testing.T) {
func TestNoteDAOFindOrphan(t *testing.T) {
testNoteDAOFindPaths(t,
core.NoteFindOpts{Orphan: true},
[]string{"ref/test/b.md", "log/2021-02-04.md"},
[]string{"ref/test/ref.md", "ref/test/b.md", "log/2021-02-04.md"},
)
}
@ -987,7 +983,7 @@ func TestNoteDAOFindCreatedBefore(t *testing.T) {
core.NoteFindOpts{
CreatedEnd: &end,
},
[]string{"ref/test/b.md", "ref/test/a.md"},
[]string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md"},
)
}
@ -1019,7 +1015,7 @@ func TestNoteDAOFindModifiedBefore(t *testing.T) {
core.NoteFindOpts{
ModifiedEnd: &end,
},
[]string{"ref/test/b.md", "ref/test/a.md", "index.md"},
[]string{"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md"},
)
}
@ -1035,55 +1031,55 @@ func TestNoteDAOFindModifiedAfter(t *testing.T) {
func TestNoteDAOFindSortCreated(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortCreated, true, []string{
"ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md",
"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md",
"log/2021-01-03.md", "log/2021-02-04.md", "log/2021-01-04.md",
})
testNoteDAOFindSort(t, core.NoteSortCreated, false, []string{
"log/2021-02-04.md", "log/2021-01-04.md", "log/2021-01-03.md",
"f39c8.md", "index.md", "ref/test/b.md", "ref/test/a.md",
"f39c8.md", "index.md", "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md",
})
}
func TestNoteDAOFindSortModified(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortModified, true, []string{
"ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md",
"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "index.md", "f39c8.md",
"log/2021-02-04.md", "log/2021-01-03.md", "log/2021-01-04.md",
})
testNoteDAOFindSort(t, core.NoteSortModified, false, []string{
"log/2021-01-04.md", "log/2021-01-03.md", "log/2021-02-04.md",
"f39c8.md", "index.md", "ref/test/b.md", "ref/test/a.md",
"f39c8.md", "index.md", "ref/test/ref.md", "ref/test/b.md", "ref/test/a.md",
})
}
func TestNoteDAOFindSortPath(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortPath, true, []string{
"f39c8.md", "index.md", "log/2021-01-03.md", "log/2021-01-04.md",
"log/2021-02-04.md", "ref/test/a.md", "ref/test/b.md",
"log/2021-02-04.md", "ref/test/a.md", "ref/test/b.md", "ref/test/ref.md",
})
testNoteDAOFindSort(t, core.NoteSortPath, false, []string{
"ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md",
"ref/test/ref.md", "ref/test/b.md", "ref/test/a.md", "log/2021-02-04.md",
"log/2021-01-04.md", "log/2021-01-03.md", "index.md", "f39c8.md",
})
}
func TestNoteDAOFindSortTitle(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortTitle, true, []string{
"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md",
"ref/test/ref.md", "ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-01-03.md",
"log/2021-02-04.md", "index.md", "log/2021-01-04.md",
})
testNoteDAOFindSort(t, core.NoteSortTitle, false, []string{
"log/2021-01-04.md", "index.md", "log/2021-02-04.md",
"log/2021-01-03.md", "ref/test/a.md", "f39c8.md", "ref/test/b.md",
"log/2021-01-03.md", "ref/test/a.md", "f39c8.md", "ref/test/b.md", "ref/test/ref.md",
})
}
func TestNoteDAOFindSortWordCount(t *testing.T) {
testNoteDAOFindSort(t, core.NoteSortWordCount, true, []string{
"log/2021-01-03.md", "log/2021-02-04.md", "index.md",
"log/2021-01-04.md", "f39c8.md", "ref/test/a.md", "ref/test/b.md",
"log/2021-01-04.md", "ref/test/ref.md", "f39c8.md", "ref/test/a.md", "ref/test/b.md",
})
testNoteDAOFindSort(t, core.NoteSortWordCount, false, []string{
"ref/test/b.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md",
"ref/test/b.md", "ref/test/ref.md", "f39c8.md", "ref/test/a.md", "log/2021-02-04.md",
"index.md", "log/2021-01-04.md", "log/2021-01-03.md",
})
}
@ -1146,43 +1142,6 @@ func queryNoteRow(tx Transaction, where string) (noteRow, error) {
return row, err
}
type linkRow struct {
SourceId core.NoteID
TargetId *core.NoteID
Href, Title, Rels, Snippet string
SnippetStart, SnippetEnd int
IsExternal bool
}
func queryLinkRows(t *testing.T, tx Transaction, where string) []linkRow {
links := make([]linkRow, 0)
rows, err := tx.Query(fmt.Sprintf(`
SELECT source_id, target_id, title, href, external, rels, snippet, snippet_start, snippet_end
FROM links
WHERE %v
ORDER BY id
`, where))
assert.Nil(t, err)
for rows.Next() {
var row linkRow
var sourceId int64
var targetId *int64
err = rows.Scan(&sourceId, &targetId, &row.Title, &row.Href, &row.IsExternal, &row.Rels, &row.Snippet, &row.SnippetStart, &row.SnippetEnd)
assert.Nil(t, err)
row.SourceId = core.NoteID(sourceId)
if targetId != nil {
row.TargetId = idPointer(*targetId)
}
links = append(links, row)
}
rows.Close()
assert.Nil(t, rows.Err())
return links
}
func idPointer(i int64) *core.NoteID {
id := core.NoteID(i)
return &id

@ -1,30 +1,38 @@
package sqlite
import (
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/paths"
"path/filepath"
"regexp"
"strings"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/paths"
strutil "github.com/zk-org/zk/internal/util/strings"
)
// NoteIndex persists note indexing results in the SQLite database.
// It implements the port core.NoteIndex and acts as a facade to the DAOs.
type NoteIndex struct {
db *DB
dao *dao
logger util.Logger
notebookPath string
db *DB
dao *dao
logger util.Logger
}
type dao struct {
notes *NoteDAO
links *LinkDAO
collections *CollectionDAO
metadata *MetadataDAO
}
func NewNoteIndex(db *DB, logger util.Logger) *NoteIndex {
func NewNoteIndex(notebookPath string, db *DB, logger util.Logger) *NoteIndex {
return &NoteIndex{
db: db,
logger: logger,
notebookPath: notebookPath,
db: db,
logger: logger,
}
}
@ -46,10 +54,50 @@ func (ni *NoteIndex) FindMinimal(opts core.NoteFindOpts) (notes []core.MinimalNo
return
}
// FindLinkMatch implements core.NoteIndex.
func (ni *NoteIndex) FindLinkMatch(baseDir string, href string, linkType core.LinkType) (id core.NoteID, err error) {
err = ni.commit(func(dao *dao) error {
id, err = ni.findLinkMatch(dao, baseDir, href, linkType)
return err
})
return
}
func (ni *NoteIndex) findLinkMatch(dao *dao, baseDir string, href string, linkType core.LinkType) (core.NoteID, error) {
if strutil.IsURL(href) {
return 0, nil
}
id, _ := ni.findPathMatch(dao, baseDir, href)
if id.IsValid() {
return id, nil
}
allowPartialMatch := (linkType == core.LinkTypeWikiLink)
return dao.notes.FindIdByHref(href, allowPartialMatch)
}
func (ni *NoteIndex) findPathMatch(dao *dao, baseDir string, href string) (core.NoteID, error) {
href, err := ni.relNotebookPath(baseDir, href)
if err != nil {
return 0, err
}
return dao.notes.FindIdByHref(href, false)
}
// FindLinksBetweenNotes implements core.NoteIndex.
func (ni *NoteIndex) FindLinksBetweenNotes(ids []core.NoteID) (links []core.ResolvedLink, err error) {
err = ni.commit(func(dao *dao) error {
links, err = dao.links.FindBetweenNotes(ids)
return err
})
return
}
// FindCollections implements core.NoteIndex.
func (ni *NoteIndex) FindCollections(kind core.CollectionKind) (collections []core.Collection, err error) {
func (ni *NoteIndex) FindCollections(kind core.CollectionKind, sorters []core.CollectionSorter) (collections []core.Collection, err error) {
err = ni.commit(func(dao *dao) error {
collections, err = dao.collections.FindAll(kind)
collections, err = dao.collections.FindAll(kind, sorters)
return err
})
return
@ -72,6 +120,17 @@ func (ni *NoteIndex) Add(note core.Note) (id core.NoteID, err error) {
if err != nil {
return err
}
note.ID = id
err = ni.addLinks(dao, id, note.Links)
if err != nil {
return err
}
err = ni.fixExistingLinks(dao, note.ID, note.Path)
if err != nil {
return err
}
return ni.associateTags(dao.collections, id, note.Tags)
})
@ -80,20 +139,108 @@ func (ni *NoteIndex) Add(note core.Note) (id core.NoteID, err error) {
return
}
// fixExistingLinks will go over all indexed links and update their target to
// the given id if they match the given path better than their current
// targetPath.
func (ni *NoteIndex) fixExistingLinks(dao *dao, id core.NoteID, path string) error {
links, err := dao.links.FindInternal()
if err != nil {
return err
}
for _, link := range links {
// To find the best match possible, shortest paths take precedence.
// See https://github.com/zk-org/zk/issues/23
if link.TargetPath != "" && len(link.TargetPath) < len(path) {
continue
}
if matches, err := ni.linkMatchesPath(link, path); matches && err == nil {
err = dao.links.SetTargetID(link.ID, id)
}
if err != nil {
return err
}
}
return nil
}
// linkMatchesPath returns whether the given link can be used to reach the
// given note path.
func (ni *NoteIndex) linkMatchesPath(link core.ResolvedLink, path string) (bool, error) {
// Remove any anchor at the end of the HREF, since it's most likely
// matching a sub-section in the note.
href := strings.SplitN(link.Href, "#", 2)[0]
matchString := func(pattern string, s string) bool {
reg := regexp.MustCompile(pattern)
return reg.MatchString(s)
}
matches := func(href string, allowPartialHref bool) bool {
if href == "" {
return false
}
href = regexp.QuoteMeta(href)
if allowPartialHref {
if matchString("^(.*/)?[^/]*"+href+"[^/]*$", path) {
return true
}
if matchString(".*"+href+".*", path) {
return true
}
}
return matchString("^(?:"+href+"[^/]*|"+href+"/.+)$", path)
}
baseDir := filepath.Join(ni.notebookPath, filepath.Dir(link.SourcePath))
if relHref, err := ni.relNotebookPath(baseDir, href); err != nil {
if matches(relHref, false) {
return true, nil
}
}
allowPartialMatch := (link.Type == core.LinkTypeWikiLink)
return matches(href, allowPartialMatch), nil
}
// relNotebookHref makes the given href (which is relative to baseDir) relative
// to the notebook root instead.
func (ni *NoteIndex) relNotebookPath(baseDir string, href string) (string, error) {
path := filepath.Clean(filepath.Join(baseDir, href))
path, err := filepath.Rel(ni.notebookPath, path)
return path,
errors.Wrapf(err, "failed to make href relative to the notebook: %s", href)
}
// Update implements core.NoteIndex.
func (ni *NoteIndex) Update(note core.Note) error {
err := ni.commit(func(dao *dao) error {
noteId, err := dao.notes.Update(note)
id, err := dao.notes.Update(note)
if err != nil {
return err
}
err = dao.collections.RemoveAssociations(noteId)
// Reset links
err = dao.links.RemoveAll(id)
if err != nil {
return err
}
err = ni.addLinks(dao, id, note.Links)
if err != nil {
return err
}
return ni.associateTags(dao.collections, noteId, note.Tags)
// Reset tags
err = dao.collections.RemoveAssociations(id)
if err != nil {
return err
}
return ni.associateTags(dao.collections, id, note.Tags)
})
return errors.Wrapf(err, "%v: failed to update note index", note.Path)
@ -114,6 +261,33 @@ func (ni *NoteIndex) associateTags(collections *CollectionDAO, noteId core.NoteI
return nil
}
func (ni *NoteIndex) addLinks(dao *dao, id core.NoteID, links []core.Link) error {
resolvedLinks, err := ni.resolveLinkNoteIDs(dao, id, links)
if err != nil {
return err
}
return dao.links.Add(resolvedLinks)
}
func (ni *NoteIndex) resolveLinkNoteIDs(dao *dao, sourceID core.NoteID, links []core.Link) ([]core.ResolvedLink, error) {
resolvedLinks := []core.ResolvedLink{}
for _, link := range links {
targetID, err := ni.findLinkMatch(dao, "" /* base dir */, link.Href, link.Type)
if err != nil {
return resolvedLinks, err
}
resolvedLinks = append(resolvedLinks, core.ResolvedLink{
Link: link,
SourceID: sourceID,
TargetID: targetID,
})
}
return resolvedLinks, nil
}
// Remove implements core.NoteIndex
func (ni *NoteIndex) Remove(path string) error {
err := ni.commit(func(dao *dao) error {
@ -162,6 +336,7 @@ func (ni *NoteIndex) commit(transaction func(dao *dao) error) error {
return ni.db.WithTransaction(func(tx Transaction) error {
dao := dao{
notes: NewNoteDAO(tx, ni.logger),
links: NewLinkDAO(tx, ni.logger),
collections: NewCollectionDAO(tx, ni.logger),
metadata: NewMetadataDAO(tx),
}

@ -1,15 +1,185 @@
package sqlite
import (
"fmt"
"testing"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util"
"github.com/mickael-menu/zk/internal/util/test/assert"
"github.com/zk-org/zk/internal/core"
"github.com/zk-org/zk/internal/util"
"github.com/zk-org/zk/internal/util/test/assert"
)
// FIXME: Missing tests
func TestNoteIndexAddWithLinks(t *testing.T) {
db, index := testNoteIndex(t)
id, err := index.Add(core.Note{
Path: "log/added.md",
Links: []core.Link{
{
Title: "Same dir",
Href: "log/2021-01-04",
Rels: core.LinkRels("rel-1", "rel-2"),
},
{
Title: "Relative",
Href: "f39c8",
Snippet: "[Relative](f39c8) link",
SnippetStart: 50,
SnippetEnd: 100,
},
{
Title: "Second is added",
Href: "f39c8#anchor",
Rels: core.LinkRels("second"),
},
{
Title: "Unknown",
Href: "unknown",
},
{
Title: "URL",
Href: "http://example.com",
IsExternal: true,
Snippet: "External [URL](http://example.com)",
},
},
})
assert.Nil(t, err)
rows := queryLinkRows(t, db.db, fmt.Sprintf("source_id = %d", id))
assert.Equal(t, rows, []linkRow{
{
SourceId: id,
TargetId: idPointer(2),
Title: "Same dir",
Href: "log/2021-01-04",
Rels: "\x01rel-1\x01rel-2\x01",
},
{
SourceId: id,
TargetId: idPointer(4),
Title: "Relative",
Href: "f39c8",
Rels: "",
Snippet: "[Relative](f39c8) link",
SnippetStart: 50,
SnippetEnd: 100,
},
{
SourceId: id,
TargetId: idPointer(4),
Title: "Second is added",
Href: "f39c8#anchor",
Rels: "\x01second\x01",
},
{
SourceId: id,
TargetId: nil,
Title: "Unknown",
Href: "unknown",
Rels: "",
},
{
SourceId: id,
TargetId: nil,
Title: "URL",
Href: "http://example.com",
IsExternal: true,
Rels: "",
Snippet: "External [URL](http://example.com)",
},
})
}
func TestNoteIndexAddFillsLinksMissingTargetId(t *testing.T) {
db, index := testNoteIndex(t)
id, err := index.Add(core.Note{
Path: "missing_target.md",
})
assert.Nil(t, err)
rows := queryLinkRows(t, db.db, fmt.Sprintf("target_id = %d", id))
assert.Equal(t, rows, []linkRow{
{
SourceId: 3,
TargetId: &id,
Title: "Missing target",
Href: "missing",
Snippet: "There's a Missing target",
},
})
}
func TestNoteIndexUpdateWithLinks(t *testing.T) {
db, index := testNoteIndex(t)
links := queryLinkRows(t, db.db, "source_id = 1")
assert.Equal(t, links, []linkRow{
{
SourceId: 1,
TargetId: idPointer(2),
Title: "An internal link",
Href: "log/2021-01-04.md",
Snippet: "[[An internal link]]",
},
{
SourceId: 1,
TargetId: nil,
Title: "An external link",
Href: "https://domain.com",
IsExternal: true,
Snippet: "[[An external link]]",
},
})
err := index.Update(core.Note{
Path: "log/2021-01-03.md",
Links: []core.Link{
{
Title: "A new link",
Href: "index",
Type: core.LinkTypeWikiLink,
IsExternal: false,
Rels: core.LinkRels("rel"),
Snippet: "[[A new link]]",
},
{
Title: "An external link",
Href: "https://domain.com",
Type: core.LinkTypeMarkdown,
IsExternal: true,
Snippet: "[[An external link]]",
},
},
})
assert.Nil(t, err)
links = queryLinkRows(t, db.db, "source_id = 1")
assert.Equal(t, links, []linkRow{
{
SourceId: 1,
TargetId: idPointer(3),
Title: "A new link",
Href: "index",
Type: "wiki-link",
Rels: "\x01rel\x01",
Snippet: "[[A new link]]",
},
{
SourceId: 1,
TargetId: nil,
Title: "An external link",
Href: "https://domain.com",
Type: "markdown",
IsExternal: true,
Snippet: "[[An external link]]",
},
})
}
func TestNoteIndexAddWithTags(t *testing.T) {
db, index := testNoteIndex(t)
@ -50,7 +220,7 @@ func TestNoteIndexUpdateWithTags(t *testing.T) {
func testNoteIndex(t *testing.T) (*DB, *NoteIndex) {
db := testDB(t)
return db, NewNoteIndex(db, &util.NullLogger)
return db, NewNoteIndex("", db, &util.NullLogger)
}
func assertTagExistsOrNot(t *testing.T, db *DB, shouldExist bool, tag string) {

@ -4,7 +4,7 @@ import (
"database/sql"
"sync"
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/zk-org/zk/internal/util/errors"
)
// LazyStmt is a wrapper around a sql.Stmt which will be evaluated on first use.

@ -16,3 +16,6 @@
- id: 6
kind: "tag"
name: "empty"
- id: 7
kind: "tag"
name: "science"

@ -88,3 +88,16 @@
created: "2020-11-29T08:20:18Z"
modified: "2020-11-10T08:20:18Z"
metadata: "{}"
- id: 8
path: "ref/test/ref.md"
sortable_path: "ref/ref.md"
title: ""
lead: ""
body: ""
raw_content: ""
word_count: 5
checksum: "ientrs"
created: "2019-11-20T20:32:56Z"
modified: "2019-11-20T20:34:06Z"
metadata: '{}'

@ -16,3 +16,12 @@
- id: 6
note_id: 5 # ref/test/b.md
collection_id: 5 # tag:adventure
- id: 7
note_id: 5 # ref/test/b.md
collection_id: 7 # tag:science
- id: 8
note_id: 4 # f39c8.md
collection_id: 7 # tag:science
- id: 9
note_id: 5 # ref/test/b.md
collection_id: 7 # tag:science

@ -1,4 +1,4 @@
# See https://github.com/mickael-menu/zk/issues/23
# See https://github.com/zk-org/zk/issues/23
- id: 1
path: "prefix-longest.md"
sortable_path: "prefix-longest.md"

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save