├── .github ├── FUNDING.yaml └── workflows │ └── publish.yml ├── .gitignore ├── .npmcheckrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── coffee.png ├── logseq-logo.png └── logseq.png ├── docs ├── .nojekyll ├── _coverpage.md ├── _sidebar.md ├── alt__macros.md ├── assets │ ├── github.svg │ └── icon.png ├── changelog.md ├── index.html ├── index.md ├── reference__args.md ├── reference__commands.md ├── reference__configuring.md ├── reference__context.md ├── reference__query_language.md ├── reference__query_language__blocks.md ├── reference__query_language__table.md ├── reference__syntax.md ├── reference__tags.md ├── reference__tags_advanced.md ├── reference__tags_dev.md ├── reference__tags_nesting.md └── tutorial.md ├── icon.png ├── index.html ├── jest.config.js ├── package.json ├── pnpm-lock.yaml ├── release.config.js ├── src ├── app.tsx ├── context.ts ├── entry.ts ├── errors.ts ├── extensions │ ├── customized_eta.ts │ ├── dayjs_logseq_plugin.ts │ ├── mldoc_ast.ts │ └── sherlockjs.d.ts ├── logic.ts ├── query.ts ├── tags.ts ├── template.ts ├── ui │ ├── insert.css │ ├── insert.tsx │ └── query-table.ts └── utils │ ├── actions.ts │ ├── index.ts │ ├── logseq.ts │ ├── other.ts │ └── parsing.ts ├── tests ├── __mocks__ │ └── styleMock.js ├── index.ts ├── logic │ └── command_template.test.ts ├── markdown-parsing │ ├── ast-complimented.json │ ├── ast-example.json │ ├── test-ast.json │ ├── test.md │ └── unsupported.md ├── tags │ ├── dates.test.ts │ └── plain.test.ts └── utils │ └── other.test.ts ├── tsconfig.json └── vite.config.ts /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: buymeacoffee.com/stdword 4 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Setup Node.js environment 17 | uses: actions/setup-node@v3.6.0 18 | with: 19 | node-version: "20" 20 | 21 | - name: Setup pnpm 22 | uses: pnpm/action-setup@v4 23 | with: 24 | version: latest 25 | 26 | - name: Install dependencies 27 | run: pnpm install 28 | 29 | - name: Build dist 30 | run: pnpm prod 31 | 32 | - name: Install zip 33 | uses: montudor/action-zip@v1 34 | 35 | - name: Release 36 | run: npx semantic-release 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | *.sublime-* 5 | -------------------------------------------------------------------------------- /.npmcheckrc: -------------------------------------------------------------------------------- 1 | {"depcheck": {"ignoreMatches": [ 2 | "@semantic-release/changelog", 3 | "@semantic-release/exec", 4 | "@semantic-release/git", 5 | "conventional-changelog-conventionalcommits", 6 | "semantic-release" 7 | ]}} 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sergey Kolesnik (@stdword) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/coffee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdword/logseq13-full-house-plugin/a14e4f2155137fff6211300bf111af5dfe1a9efa/assets/coffee.png -------------------------------------------------------------------------------- /assets/logseq-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdword/logseq13-full-house-plugin/a14e4f2155137fff6211300bf111af5dfe1a9efa/assets/logseq-logo.png -------------------------------------------------------------------------------- /assets/logseq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdword/logseq13-full-house-plugin/a14e4f2155137fff6211300bf111af5dfe1a9efa/assets/logseq.png -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdword/logseq13-full-house-plugin/a14e4f2155137fff6211300bf111af5dfe1a9efa/docs/.nojekyll -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | ![logo](assets/icon.png ":size=256x256") 2 | 3 | # 🏛 Full House Templates 4 | 5 | > Logseq Templates you will really love ❤️ 6 | 7 | - JavaScript-based 8 | - Super-configurable 9 | - Focused on UX and simplicity 10 | 11 | [Get Started](index.md) 12 | [GitHub](https://github.com/stdword/logseq13-full-house-plugin#readme) 13 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [About](index.md) 2 | 3 | - [Changelog](changelog.md) 4 | 5 | - [Tutorial](tutorial.md) 6 | 7 | - *Reference* 8 | - [Commands](reference__commands.md) 9 | - [Syntax](reference__syntax.md) 10 | - [Configuring](reference__configuring.md) 11 | - [Context variables](reference__context.md) 12 | - [Arguments](reference__args.md) 13 | - [Template tags](reference__tags.md) 14 | - [Nesting templates](reference__tags_nesting.md) 15 | - [Advanced](reference__tags_advanced.md) 16 | - [Dev](reference__tags_dev.md) 17 | - Query language 18 | - [Pages](reference__query_language.md) 19 | - [Blocks](reference__query_language__blocks.md) 20 | - [Table View](reference__query_language__table.md) 21 | 22 | - *Alternatives* 23 | - [Logseq `:macros`](alt__macros.md) 24 | 25 | - *Links* 26 | - [🏛  ***Showroom***](https://github.com/stdword/logseq13-full-house-plugin/discussions/categories/showroom?discussions_q=is%3Aopen+label%3Aoriginal+category%3AShowroom) 27 | - [🙏  Q&A](https://github.com/stdword/logseq13-full-house-plugin/discussions/categories/q-a?discussions_q=is%3Aopen+label%3Aoriginal+category%3AQ%26A) 28 | -------------------------------------------------------------------------------- /docs/alt__macros.md: -------------------------------------------------------------------------------- 1 | ## Why do you need to use `:macros`? 2 | ### 1) To get block rendered every time (not just once) 3 | `🏛 Full House` can do *exactly* the same with [`:template-view`](reference__commands.md#template-view-command) command. Just use [UI](reference__commands.md#view-insertion-ui) to insert any template as `🏛️view`. 4 | 5 | ### 2) To use arguments `$1`, `$2`, ... 6 | `🏛 Full House` supports [arguments](reference__args.md) too. And they can be _named_, optional (with [macro mode](reference__args.md#macro-mode)) and have _default values_! \ 7 | See [wiki template](https://github.com/stdword/logseq13-full-house-plugin/discussions/8) showcase as an example. 8 | 9 | ### 3) To use Logseq dynamic variables 10 | `🏛 Full House` can render [Logseq dynamic variables](https://docs.logseq.com/#/page/60311eda-b6f7-4779-8187-8830545b3a64) too. And in a more powerful way. See [navigation for Daily Journals](https://github.com/stdword/logseq13-full-house-plugin/discussions/6) example. 11 | 12 | ?> You can use both Logseq syntax and [🏛syntax](reference__syntax.md#standard-syntax) with the plugin: 13 | 14 | 15 | #### ***Logseq syntax*** 16 | `<% today %>` \ 17 | `<% yesterday %>` \ 18 | `<% tomorrow %>` 19 | 20 | `<% time %>` 21 | 22 | `<% current page %>` 23 | 24 | #### ***🏛syntax*** 25 | ` ``[today]`` ` \ 26 | ` ``[yesterday]`` ` \ 27 | ` ``[tomorrow]`` ` 28 | 29 | ` ``time`` ` 30 | 31 | ` ``[c.currentPage]`` ` 32 | 33 | #### ***Rendered result*** 34 | [[2023-03-27 Mon]] \ 35 | [[2023-03-26 Sun]] \ 36 | [[2023-03-28 Tue]] 37 | 38 | 22:44 39 | 40 | [[Full House Templates]] 41 | 42 | 43 | 44 | ?> A frequent problem with standard dynamic variables is an inability to separate `[[ ]]` from results. But with 🏛️syntax it is easy: just erase the `[ ]`: 45 | 46 | 47 | #### ***🏛syntax*** 48 | ` ``today`` ` \ 49 | ` ``yesterday`` ` \ 50 | ` ``tomorrow`` ` 51 | 52 | #### ***Rendered result without `[[ ]]`*** 53 | 2023-03-27 Mon \ 54 | 2023-03-26 Sun \ 55 | 2023-03-28 Tue 56 | 57 | -------------------------------------------------------------------------------- /docs/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdword/logseq13-full-house-plugin/a14e4f2155137fff6211300bf111af5dfe1a9efa/docs/assets/icon.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 25 | 26 | 27 | 67 | 68 | 69 |
70 | 71 | 152 | 153 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 209 | 219 | 220 | 221 | 222 | 223 | 229 | 230 | 231 | 402 | 403 | 404 | 405 | 406 | 407 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 17 | 18 |
5 |

6 | 7 |

8 |
10 |

Full House Templates

11 | 12 | 13 | 14 | 15 | 16 |
19 | 20 | ## If you ❤️ what I'm doing — consider to support my work 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /docs/reference__args.md: -------------------------------------------------------------------------------- 1 | Used to tune and parametrize templates rendering process. 2 | 3 | ## Specifying 4 | ### Positional arguments 5 | Specify positional arguments separated by comma. 6 | 7 | 8 | #### ***Command*** 9 | `{{renderer :template, "test", 13}}` \ 10 | `{{renderer :template, "test", 13, value}}` \ 11 | `{{renderer :template, "test", 13, value, "value,with,commas"}}` 12 | 13 | 14 | 15 | ### Named arguments 16 | Specify named arguments in the form «:name value» (colon is required). 17 | 18 | - **Note**: names arguments could be mixed with positional ones 19 | 20 | 21 | #### ***Command*** 22 | `{{renderer :template, "test", :arg 13}}` \ 23 | `{{renderer :template, "test", :arg1 13, :arg2 value}}` \ 24 | `{{renderer :template, "test", :firstArg "13", value, :last-arg "with,commas"}}` 25 | 26 | 27 | - Escape named arguments with «::» — it becomes positional 28 | 29 | 30 | #### ***Command*** 31 | `{{renderer :view, "c.args.$1", ::test 13}}` 32 | 33 | #### ***Rendered*** 34 | :test 13 35 | 36 | 37 | 38 | 39 | ### Macro mode :id=macro-mode 40 | This is the special case when argument value (positional or named) is in the form `$1`, `$2`, ... 41 | The macro mode makes these values **empty**. 42 | 43 | - It is required to pass arguments from Logseq `:macros` to [`:template-view`](reference__commands.md#template-view-command) command to distinguish between empty and non-empty arguments. Logseq `:macros` doesn't support optional arguments. The macro mode adds it. 44 | 45 | 46 | #### ***`config.edn`*** 47 | ```clojure 48 | :commands [ 49 | ["macro1" "arg $1"], 50 | ["macro2" "{{renderer :view, 'arg ' + c.args[1], $1}}"], 51 | ] 52 | ``` 53 | 54 | #### ***Usage*** 55 | - `{{macro1 13}}` 56 | - `{{macro2 13}}` 57 | 58 | + `{{macro1}}` 59 | + `{{macro2}}` 60 | 61 | #### ***Rendered*** 62 | - arg 13 63 | - arg 13 64 | 65 | + arg $1 66 | + arg 67 | 68 | 69 | 70 | - It is always turned on automatically. To disable use double `$$`. 71 | 72 | 73 | #### ***Command*** 74 | - `{{renderer :view, "c.args.$1", $1}}` No value — with macro mode 75 | - `{{renderer :view, "c.args.$1", $1 with text}}` — no macro mode (value form is different) 76 | - `{{renderer :view, "c.args.$1", $$1}}` — disabled macro mode 77 | - `{{renderer :view, "c.args.test", :test $$1}}` — disabled macro mode 78 | 79 | #### ***Rendered*** 80 | - No value — with macro mode 81 | - $1 with text — no macro mode (value form is different) 82 | - $1 — disabled macro mode 83 | - $1 — disabled macro mode 84 | 85 | 86 | 87 | 88 | 89 | ### `arg-`properties :id=arg-properties 90 | Arguments can have *default values* specified in template properties. 91 | - Property name should start with prefix `arg-`. 92 | 93 | ?> You can **inherit templates** with the help of *arg-properties* and [`layout`](reference__tags_nesting.md#nesting-layout) template tag 94 | 95 | 96 | 97 | Example: 98 | 99 | 100 | 101 | #### ***Template*** 102 | ``` 103 | - template:: test 104 | arg-icon:: 🏛 105 | arg-the-name:: Full House Templates 106 | - ``c.args.icon + c.args['the-name']`` 107 | ``` 108 | #### ***Rendered*** 109 | 🏛Full House Templates 110 | 111 | 112 | 113 | Arg-properties **with** the «?» at the end of the name will be coerced to a boolean value: 114 | 115 | 116 | 117 | #### ***Template*** 118 | ``` 119 | - template:: test 120 | arg-ok:: yes 121 | arg-ok?:: yes 122 | - ``c.args.ok`` 123 | - ``c.args['ok?']`` 124 | ``` 125 | #### ***Rendered*** 126 | - yes 127 | - true 128 | 129 | 130 | 131 | 132 | Also the will be copied to args **without** «?» at the end of the name (if there are no collisions): 133 | 134 | 135 | 136 | #### ***Template*** 137 | ``` 138 | - template:: test 139 | arg-ok?:: yes 140 | - ``c.args.ok`` 141 | - ``c.args['ok?']`` 142 | ``` 143 | #### ***Rendered*** 144 | - true 145 | - true 146 | 147 | 148 | 149 | 150 | 151 | 152 | ## Accessing :id=accessing 153 | 154 | ### By argument name 155 | ?> **Note**: argument without value is *boolean* 156 | 157 | 158 | #### ***Command*** 159 | Boolean: `{{renderer :view, "c.args.test", :test}}` \ 160 | Boolean: `{{renderer :view, "c.args.test", :test ""}}` 161 | 162 | String: `{{renderer :view, "c.args.test", :test 13}}` \ 163 | String: `{{renderer :view, "c.args.test", :test "13"}}` 164 | 165 | #### ***Rendered*** 166 | Boolean: true \ 167 | Boolean: false 168 | 169 | String: 13 \ 170 | String: 13 171 | 172 | 173 | 174 | 175 | ### By argument name with inappropriate characters 176 | 177 | *E.g.* «-», «+», «.», etc. 178 | 179 | 180 | #### ***Command*** 181 | `{{renderer :view, "c.args['test-arg']", :test-arg}}` 182 | 183 | #### ***Rendered*** 184 | true 185 | 186 | 187 | 188 | 189 | ### By argument position 190 | 191 | 192 | #### ***Command*** 193 | `{{renderer :view, "c.args[0]", 13}}`: template name \ 194 | `{{renderer :view, "c.args[1]", 13}}`: first \ 195 | `{{renderer :view, "c.args[2]", 13}}`: second (empty value) 196 | 197 | #### ***Rendered*** 198 | \_\_inline\_\_: template name \ 199 | 13: first \ 200 | : second (empty value) 201 | 202 | 203 | 204 | 205 | 206 | ### By argument position, excluding named ones 207 | 208 | ?> **Note**: position count starts from 1 209 | 210 | 211 | #### ***Command*** 212 | `{{renderer :view, "c.args.$1", :first 1}}`: no positional \ 213 | `{{renderer :view, "c.args.$1", :first 1, 2}}`: second 214 | 215 | #### ***Rendered*** 216 | : no positional \ 217 | 2: second 218 | 219 | 220 | 221 | 222 | ### Mixing named & positional access :id=args-accessing-mixed 223 | 224 | 225 | #### ***Command*** 226 | `{{renderer :view, "c.args.name$1", 13, :name 7}}` \ 227 | `{{renderer :view, "c.args.name$1", :name 7, 13}}` \ 228 | `{{renderer :view, "c.args.name$1", 13, :test 7}}` \ 229 | `{{renderer :view, "c.args.name$1", :test 7, 13}}` 230 | 231 | #### ***Rendered*** 232 | 7 \ 233 | 7 \ 234 | 13 \ 235 | 13 236 | 237 | 238 | 239 | #### ***Command*** 240 | `{{renderer :view, "c.args.$1name", 13, :name 7}}` \ 241 | `{{renderer :view, "c.args.$1name", :name 7, 13}}` \ 242 | `{{renderer :view, "c.args.$1name", :test 13, :name 7}}` \ 243 | `{{renderer :view, "c.args.$1name", :name 7, :test 13}}` 244 | 245 | #### ***Rendered*** 246 | 13 \ 247 | 13 \ 248 | 7 \ 249 | 7 250 | 251 | -------------------------------------------------------------------------------- /docs/reference__commands.md: -------------------------------------------------------------------------------- 1 | ## Render template :id=template-command 2 | Render existed template block, non-template block or page. 3 | 4 | #### Via insertion UI :id=insertion-ui 5 | 6 | 7 | 8 | 1. Use `⌘t` (or `ctrl+t`) shortcut 9 | 10 | 11 | 1a. Use `/`-command or *Command Palette* (`⌘⇧P` or `Ctrl+Shift+P`) 12 | 13 | 1b. Select the `«Insert 🏛template or 🏛️view»` command 14 | 15 | 16 | 2. Then select the appropriate template. 17 | 18 | ?> You cannot render non-template blocks or pages via UI. To do that use another approach: 19 | 20 | #### Via block / page context menu :id=indirect 21 | 1. Right-click on any block's bullet or page title to open *Context Menu* 22 | 2. Select the `«Copy as 🏛template»` item 23 | 3. Code to call the command will be copied to clipboard. Paste it to any block. 24 | 25 | 26 | ### Examples :id=template-command-examples 27 | 28 | #### Rendering template by name :id=template-command-rendering-by-name 29 | Standard way of rendering templates. 30 | Plugin will find the block with the property `template` and it's specified name. 31 | - The property `template-including-parent` with any bool value could be used to change the parent block inclusion. By default it is set to «*No*» (in opposite to standard Logseq templates). 32 | - This can be configured with [inclusion control](reference__configuring.md#parent-block-inclusion-control) to ignore `template-including-parent` property. 33 | 34 | 35 | #### ***Command*** 36 | `{{renderer :template, template name}}` \ 37 | `{{renderer :template, "template name, with comma"}}` 38 | 39 | 40 | 41 | #### Rendering any non-template block 42 | Use block reference to specify the block. Copy it from *Block Context Menu* directly or use [indirect way](#indirect). 43 | - By default the parent block will be included (in opposite to [rendering by name](#template-command-rendering-by-name)). 44 | - This still can be configured with [inclusion control](reference__configuring.md#parent-block-inclusion-control). 45 | 46 | 47 | #### ***Command*** 48 | `{{renderer :template, ((64e61063-1689-483f-903f-409766d81b2e)) }}` 49 | 50 | 51 | 52 | #### Rendering page as a template 53 | Use page reference to specify the page. Enter it manually or use [*Context Menu*](#indirect). Only **first page block** and all it's **children** will be used as the template. 54 | - By default the first block will **not** be included. 55 | - This still can be configured with [inclusion control](reference__configuring.md#parent-block-inclusion-control). 56 | 57 | 58 | #### ***Command*** 59 | `{{renderer :template, [[Template Page]] }}` \ 60 | `{{renderer :template, "[[Template Page, with comma]]" }}` 61 | 62 | 63 | ### Configuring arguments 64 | See documentation [here](reference__configuring.md#configure-arguments). 65 | 66 | 67 | 68 | ## Render current block :id=render-this-block-command 69 | Render current block (without it's children). «/»-command to call from editing mode. 70 | 71 | 1. Open slash commands menu by typing `/` 72 | 2. Select the `«Render this 🏛️block»` command 73 | 74 | Useful for accessing [date-syntax](reference__syntax.md#dates-nlp-syntax), [`query.*` namespace](reference__tags_advanced#query), [blocks creation](reference__tags_advanced.md#blocks-spawn), [`dev.uuid`](reference__tags_dev.md#dev-uuid) tag, etc. 75 | 76 | 77 | 78 | ## Render template button :id=template-button-command 79 | Create a clickable button that inserts a template near a specific block or inside the page. 80 | 81 | 1. Use `⌘t` (or `ctrl+t`) shortcut to open [Insertion UI](#insertion-ui). 82 | 2. Then select the appropriate template. 83 | 3. Press `⌘⇧↩︎` (or `ctrl+shift+enter` for Windows) to insert a button for the chosen template. 84 | 85 | ?> You cannot create buttons for non-template blocks or pages 86 | 87 | ?> You cannot create buttons to insert Views, but you can create buttons for a Template that inserts a View 88 | 89 | ### Configuring with arguments 90 | You can specify a `:title` for the button (default: same as the template name): 91 | 92 | 93 | #### ***Command*** 94 | `{{renderer :template-button, Name, :title "Click Me"}}` \ 95 | `{{renderer :template-button, Name}}` 96 | 97 | #### ***Rendered*** 98 | `[Click Me]` \ 99 | `[Name]` 100 | 101 | 102 | 103 | And you can specify an `:action` to insert the template in different ways: 104 | - `append`: insert the template as the *last child* of chosen block or page (default action) 105 | - `prepend`: insert the template as the *first child* of chosen block or page 106 | - `replace`: insert the template into the chosen block, replacing its content 107 | - `call`: don't insert the template anywhere, but render it (to execute code) 108 | 109 | ?> The short form is used in these examples for conciseness: \ 110 | `:action append` \ 111 | instead of: \ 112 | `{{renderer :template-button, Name, :action append}}` 113 | 114 | 115 | 116 | #### **`append`** 117 | `:action append` — to button's block \ 118 | `:action append, :block ((66dcb92b-a0...))` — to specified block \ 119 | `:action append, :page [[The Page]]` — to specified page as 1st-level block \ 120 | `:action append, :page` — to Today's journal page as 1st-level block 121 | 122 | #### **`prepend`** 123 | `:action prepend` — to button's block \ 124 | `:action prepend, :block ((66dcb92b-a0...))` — to specified block \ 125 | `:action prepend, :page [[The Page]]` — to specified page as 1st-level block \ 126 | `:action prepend, :page` — to Today's journal page as 1st-level block 127 | 128 | #### **`replace`** 129 | `:action replace, :block ((66dcb92b-a0...))` — to specified block 130 | 131 | #### **`call`** 132 | `:action call` 133 | 134 | 135 | 136 | You can find more details about the `:page` and `:block` arguments here: 137 | - [`:page`](reference__configuring.md#page-argument) argument 138 | - [`:block`](reference__configuring.md#block-argument) argument 139 | 140 | Also available for configuration: 141 | - [`:delay-until-rendered`](reference__configuring.md#delay-until-rendered) argument 142 | 143 | 144 | 145 | ## Render view :id=template-view-command 146 | Render existed template block, non-template block or page as 🏛view. For views rendering occurs every time the block becomes visible. 147 | 148 | !> Rendered content will not be persisted. If you need to keep it, use [render template command](#template-command) instead. 149 | 150 | !> Rendered page or block *references* will not be displayed in *Linked references* and *Block references* sections. If you need it to be displayed, use [render template command](#template-command) instead. 151 | 152 | Another reason to use 🏛view is availability of applying custom CSS. See example [here](https://github.com/stdword/logseq13-full-house-plugin/discussions/9). 153 | 154 | 155 | #### Via insertion UI :id=view-insertion-ui 156 | 157 | 158 | 159 | 1. Use `⌘t` (or `ctrl+t`) shortcut 160 | 161 | 162 | 1a. Use `/`-command or *Command Palette* (`⌘⇧P` or `Ctrl+Shift+P`) 163 | 164 | 1b. Select the `«Insert 🏛template or 🏛️view»` command 165 | 166 | 167 | 2. Then select the appropriate view. Or select the template and hold `⌘` or `Ctrl` to insert it as view. 168 | 169 | ?> You cannot render non-template blocks or pages via UI. To do that use another approach: 170 | 171 | #### Via block / page context menu :id=indirect 172 | 1. Right-click on any block's bullet or page title to open *Context Menu* 173 | 2. Select the `«Copy as 🏛view»` item 174 | 3. Code to call the command will be copied to clipboard. Paste it to any block. 175 | 176 | #### Examples 177 | ?> [Arguments](reference__configuring.md#configure-arguments), [examples and way of rendering](#template-command-examples) are very similar to [render template command](#template-command). Only differences are reflected below. 178 | 179 | 180 | 181 | 🏛View always displays in one block. 182 | 183 | 1. The parent block of template and it's first-level children will be displayed as wall of text. 184 | 185 | 186 | 187 | #### ***Template*** 188 | ``` 189 | - parent 190 | - child 1 191 | - child 2 192 | ``` 193 | 194 | #### ***Rendered w/o parent*** 195 | child 1 \ 196 | child 2 197 | 198 | #### ***Rendered w/ parent*** 199 | parent \ 200 | child 1 \ 201 | child 2 202 | 203 | 204 | 205 | 206 | 207 | 208 | 2. The every other level of children will be displayed as bullet list. 209 | 210 | 211 | 212 | #### ***Template*** 213 | ``` 214 | - parent 215 | - child 1 216 | - item 217 | - child 2 218 | - item 219 | - item 220 | ``` 221 | 222 | #### ***Rendered w/o parent*** 223 | child 1 224 | - item 225 | 226 | child 2 227 | - item 228 | - item 229 | 230 | #### ***Rendered w/ parent*** 231 | parent \ 232 | child 1 233 | - item 234 | 235 | child 2 236 | - item 237 | - item 238 | 239 | 240 | 241 | 242 | 243 | 244 | ## Render inline view :id=inline-view-command 245 | Rendering inline view: fast way to render any 🏛syntax without creating a whole template block. 246 | 247 | ?> The only syntax allowed here is [` `` `-syntax](reference__syntax.md#interpolation-syntax) and it will be **applied automatically**. 248 | 249 | #### Usage 250 | 251 | 252 | 1. Call via `/`-command or *Command Palette* (`⌘⇧P` or `Ctrl+Shift+P`) 253 | 2. Select the `«Insert inline 🏛view»` command 254 | 255 | 256 | 257 | There is no need to use back-tick «\`» quotes: 258 | 259 | 260 | 261 | #### ***Template*** 262 | `{{renderer :view, c.page.name}}` 263 | 264 | #### ***Rendered*** 265 | [[Test Page]] 266 | 267 | 268 | 269 | 270 | 271 | 272 | ## Convert template to new syntax style :id=convert-syntax-command 273 | This command converts old template syntax style (used before `v3.0.0`) to new one. See details [here](changelog.md#v3-new-syntax). 274 | 275 | #### Usage 276 | 277 | 278 | 1. Select blocks with *old* plugin syntax 279 | 2. Run the *Command Palette* (`⌘⇧P` or `Ctrl+Shift+P`) 280 | 3. Select the `«Convert to new 🏛syntax»` command 281 | -------------------------------------------------------------------------------- /docs/reference__configuring.md: -------------------------------------------------------------------------------- 1 | ## Insertion UI 2 | 3 | ### Hide template from list :id=hiding-from-list 4 | Prepend «.» before template name to automatically hide it from UI list. Use shortcut tips in the UI footer to show them back. 5 | 6 | 7 | 8 | This template will be hidden from the list: 9 | 10 | 11 | 12 | #### ***Template*** 13 | `template:: .test` 14 | 15 | 16 | 17 | 18 | ### Rendering as View / Template only :id=restricting-rendering 19 | Use optional `template-list-as` property to do that. 20 | 21 | 22 | 23 | Insert only as *View*: 24 | 25 | 26 | 27 | #### ***Template*** 28 | `template:: Name` \ 29 | `template-list-as:: view` 30 | 31 | 32 | 33 | 34 | 35 | Insert only as *Template*: 36 | 37 | 38 | 39 | #### ***Template*** 40 | `template:: Name` \ 41 | `template-list-as:: template` 42 | 43 | 44 | 45 | 46 | 47 | Or use any other text to label template in the *Insertion UI*: 48 | 49 | 50 | 51 | #### ***Template*** 52 | `template:: Name` \ 53 | `template-list-as:: related to work` 54 | 55 | 56 | 57 | 58 | ### Default arguments :id=default-usage 59 | Use optional `template-usage` property to specify default template arguments. 60 | 61 | 62 | 63 | It will be used automatically with [*UI*](reference__commands.md#insertion-ui) or [*Context Menu*](reference__commands.md#indirect) insertions. 64 | 65 | 66 | 67 | #### ***Template*** 68 | `template:: Name` \ 69 | `template-usage:: :age 21` 70 | 71 | #### ***Will be inserted as*** 72 | `{{renderer :template, Name, :age 21}}` 73 | 74 | 75 | 76 | 77 | 78 | Back-tick quotes can be used to prevent displaying property value as a page reference: 79 | 80 | 81 | 82 | #### ***Template*** 83 | `template:: Name` \ 84 | ``template-usage:: `:page [[Another]]` `` 85 | 86 | #### ***Will be inserted as*** 87 | `{{renderer :template, Name, :page [[Another]] }}` 88 | 89 | 90 | 91 | 92 | ### Position the cursor for arguments :id=control-cursor 93 | Use optional `template-usage` property to specify cursor position (or text selection) for template arguments. 94 | 95 | ?> **Note**: Using cursor positioning forces template insertion to be *non-instant*. 96 | This means that before the template is inserted, a `{{renderer...}}` macro will be used as an intermediate step. 97 | 98 | 99 | 100 | Use a `{|}` marker to indicate the cursor position: 101 | 102 | 103 | 104 | #### ***Template*** 105 | `template:: Name` \ 106 | `template-usage:: :age 21{|}` 107 | 108 | #### ***Will be inserted as*** 109 | `{{renderer :template, Name, :age 21`**|**`}}` 110 | The cursor will be after argument value, but before curly brackets, so you can easily type-in new arguments without navigating the cursor. 111 | 112 | 113 | 114 | 115 | 116 | Or to open **page search modal** window: 117 | 118 | 119 | 120 | #### ***Template*** 121 | `template:: Name` \ 122 | ``template-usage:: `:page [[{|}]]` `` 123 | 124 | #### ***Will be inserted as*** 125 | `{{renderer :template, Name, :page [[`**|**`]]}}` 126 | The page search modal will be opened, and it only requires you to type-in the page name without navigating the cursor. 127 | 128 | 129 | 130 | 131 | 132 | Use a double `{|}` marker to indicate the text selection: 133 | 134 | 135 | 136 | #### ***Template*** 137 | `template:: Name` \ 138 | `template-usage:: :title "{|}Meeting{|}"` 139 | 140 | #### ***Will be inserted as*** 141 | `{{renderer :template, Name, :title "`Meeting`" }}` 142 | Text "Meeting" will be selected, and it only requires you to type-in the value without navigating the cursor. 143 | 144 | 145 | 146 | 147 | ### Shortcut for insertion :id=insertion-shortcut 148 | Use optional `template-shortcut` property to specify keyboard shortcut for insertion. 149 | 150 | ?> The shortcut can be changed via standard **Logseq Keymap**. After changing the application need to be reloaded (or the plugin need to be reloaded separately). 151 | 152 | ?> **How to know which value to set for the property?** \ 153 | 1. Specify the property `template-shortcut::` for the template, but leave the value empty \ 154 | 2. Reload the application \ 155 | 3. Open the *Logseq Keymap* \ 156 | 4. Find the line with the label **"Insert 🏛️template: *TEMPLATE NAME*"** \ 157 | 5. Change shortcut \ 158 | 6. Reload the application again \ 159 | 7. Done! 160 | 161 | ?> **Command Templates** \ 162 | With shortcuts, you can render templates outside of edit mode and selection mode. 163 | Additionally, with the help of [`blocks.actions.*`](reference__tags_advanced.md#blocks-actions) template tags, templates become much like Logseq Commands, which you can call with shortcuts. \ 164 | \ 165 | Prepare Logseq just the way you like it. 166 | 167 | 168 | 169 | #### ***Template*** 170 | `template:: Name` \ 171 | `template-shortcut:: shift+meta+d` 172 | 173 | 174 | 175 | 176 | ## Rendering arguments :id=configure-arguments 177 | 178 | ### Controlling parent block inclusion :id=parent-block-inclusion-control 179 | Use the «+» or «-» sign as the first letter of the template reference to control the inclusion of the parent block. 180 | - Use «++» or «--» to *escape* this behaviour and use «+» or «-» as part of the template reference. 181 | 182 | 183 | #### ***Command*** 184 | `{{renderer :template, +[[Template Page]] }}` \ 185 | `{{renderer :template, "+[[Template Page, with comma]]" }}` 186 | 187 | `{{renderer :template, -Template Name }}` \ 188 | `{{renderer :template, --Template name with single minuses around- }}` 189 | 190 | `{{renderer :template, -((64e61063-1689-483f-903f-409766d81b2e)) }}` 191 | 192 | 193 | 194 | ### `:page` argument :id=page-argument 195 | Set page for `c.page` [context variable](reference__context.md#page-context). By default it is the current page opened in main view. \ 196 | See arguments' [*Reference*](reference__args.md) for syntax details. 197 | 198 | 199 | 200 | Example: 201 | 202 | 203 | 204 | #### ***Template «test»*** 205 | ` ``c.page.name`` ` 206 | 207 | #### ***Command*** 208 | `{{renderer :template, test}}` 209 | `{{renderer :template, test, :page [[Another Page]]}}` 210 | 211 | #### ***Rendered*** 212 | Test Page \ 213 | Another Page 214 | 215 | 216 | 217 | 218 | Specifying page with **comma** «,» in its name: 219 | 220 | 221 | 222 | #### ***Template «test»*** 223 | ` ``c.page.name`` ` 224 | 225 | #### ***Command*** 226 | `{{renderer :template, test, :page [[One, Two]]}}` 227 | `{{renderer :template, test, :page "[[One, Two]]"}}` 228 | `{{renderer :template, test, ":page [[One, Two]]"}}` 229 | 230 | #### ***Rendered*** 231 | ERROR: No such page **[[One** \ 232 | ERROR: No such page **"[[One** \ 233 | One, Two 234 | 235 | 236 | 237 | 238 | 239 | ### `:block` argument :id=block-argument 240 | Set block for `c.block` [context variable](reference__context.md#block-context). By default it is the block rendering occurs in. \ 241 | See arguments' [*Reference*](reference__args.md) for syntax details. 242 | 243 | 244 | 245 | Example: 246 | 247 | 248 | 249 | #### ***Template «test»*** 250 | ` ``c.block.content`` ` 251 | 252 | #### ***Command*** 253 | `{{renderer :template, test}}` 254 | `{{renderer :template, test, :block ((64e61063-1689-483f-903f-409766d81b2e))}}` 255 | 256 | #### ***Rendered*** 257 | {{renderer :template, test}} \ 258 | Another's block content 259 | 260 | 261 | 262 | 263 | 264 | ### `:delay-until-rendered` argument :id=delay-until-rendered 265 | 266 | 267 | 268 | **Delay** the rendering process until the external rendering occurs. \ 269 | Use it when you need to **nest** a command inside another template and prevent it from rendering just in time. 270 | 271 | 272 | 273 | #### ***Template*** 274 | Delayed rendering: \ 275 | *{{renderer :template, nested, :delay-until-rendered}}* 276 | 277 | #### ***Template «nested»*** 278 | ` ``c.page.name`` ` 279 | 280 | #### ***Rendered*** 281 | Delayed rendering: \ 282 | Test Page 283 | 284 | 285 | -------------------------------------------------------------------------------- /docs/reference__context.md: -------------------------------------------------------------------------------- 1 | ## `c`: whole context variable :id=context 2 | The main variable to keep all context information. 3 | 4 | ?> You can always see whole context with `{{renderer :view, "c"}}`. \ 5 | Or individual one (e.g. `c.tags`) with `{{renderer :view, "c.tags"}}`. 6 | 7 | 8 | ### `c.mode` 9 | Current rendering mode. Can be only `template` or `view`. 10 | 11 | 12 | ### `c.identity` 13 | CSS context. Accessing CSS class names for currently rendering view. 14 | 15 | Used for complex 🏛view development. See example [here](https://github.com/stdword/logseq13-full-house-plugin/discussions/9). 16 | 17 | #### Schema 18 | 19 | ```javascript 20 | { 21 | slot: (string) 'slot__34emiluj' 22 | key: (string) '34emiluj' 23 | } 24 | ``` 25 | 26 | 27 | ### `c.args` 28 | Arguments context. Accessing arguments by its names. 29 | 30 | See detailed [Reference for arguments](reference__args.md). 31 | 32 | #### Schema 33 | 34 | ```javascript 35 | { 36 | (string) arg name: (string or boolean) arg value 37 | } 38 | ``` 39 | 40 | 41 | ### `c.page` & `c.currentPage` :id=page-context 42 | Page contexts: 43 | - `c.page`: current page or page provided with `:page` [argument](reference__configuring.md#page-argument) 44 | - `c.currentPage` is always the current page 45 | 46 | #### Schema 47 | 48 | ```javascript 49 | { 50 | id: (number) 11320, 51 | uuid: (string) '64d7d3a2-b635-487b-8aa9-0a44ad21e142', 52 | name: (string) 'logseq/plugins/Full House Templates', 53 | name_: (string) 'logseq/plugins/full house templates', 54 | namespace: { 55 | parts: [ 'logseq', 'plugins', 'Full House Templates' ], 56 | prefix: 'logseq/plugins', 57 | suffix: 'Full House Templates', 58 | pages: [ 'logseq', 'logseq/plugins' ] 59 | }, 60 | file: (string) 'pages/logseq___plugins___Full House Templates.md', 61 | isJournal: (boolean) false, 62 | props: { 63 | icon: (string) 🏛, 64 | related: (string) '[[logseq]], [[logseq/plugins]]' 65 | }, 66 | propsRefs: { 67 | icon: (array of string) [], 68 | related: : (array of string) ['logseq', 'logseq/plugins'] 69 | } 70 | } 71 | ``` 72 | 73 | 74 | ### `c.block` & `c.currentBlock` :id=block-context 75 | Block contexts: 76 | - `c.block`: current block or block provided with `:block` [argument](reference__configuring.md#block-argument) 77 | - `c.currentBlock` is always block rendering occurs in 78 | 79 | #### Schema 80 | 81 | ```javascript 82 | { 83 | id: (number) 25686, 84 | uuid: (string) '64d8c048-37dd-4666-a653-15fb14eda201', 85 | content: (string) '{{renderer :view, "c.block"}}\nproperty:: already existed', 86 | props: { 87 | property: (string) 'already existed' 88 | }, 89 | propsRefs: { 90 | property: (array of string) [] 91 | }, 92 | 93 | page: (page) ... 94 | 95 | // NOTE: always starts from zero 96 | level: (number) 0, 97 | 98 | children: (array of block) [...] 99 | parentBlock: { id: (number) 25640 }, 100 | prevBlock: { id: (number) 25678 }, 101 | refs: (array of page) [ { id: (number) 25679 } ] 102 | } 103 | ``` 104 | 105 | 106 | ### `c.template` 107 | Template block context. 108 | 109 | #### Schema 110 | 111 | ```javascript 112 | { 113 | name: (string) template name, page name or block UUID 114 | includingParent: (boolean) true 115 | block: (block) template block 116 | props: same as `block.props` 117 | propsRefs: same as `block.propsRefs` 118 | } 119 | ``` 120 | 121 | 122 | ### `c.self` 123 | Context for currently rendering block of template. This is dynamic variable: it changes during rendering. 124 | 125 | It is handy if you need to access properties of child template block. 126 | 127 | !> Doesn't available for [*Inline Views*](reference__commands.md#inline-view-command) 128 | 129 | ?> Schema is the same as `c.block` 130 | 131 | #### Structure 132 | 133 | ``` 134 | [[Test Page]] ← c.page 135 | 136 | - template:: test ← c.template.block 137 | - child ← c.self 138 | - sub child ← c.self 139 | 140 | - {{renderer :template, test}} ← c.block 141 | ``` 142 | 143 | 144 | ### `c.config` :id=context-config 145 | Configuration context. 146 | 147 | #### Schema 148 | 149 | ```javascript 150 | { 151 | appVersion: (string) '0.9.13', 152 | pluginVersion: (string) '3.0.0', 153 | 154 | preferredWorkflow: (string) 'now', 155 | preferredThemeMode: (string) 'light', 156 | preferredFormat: (string) 'markdown', 157 | preferredLanguage: (string) 'en-GB', 158 | preferredDateFormat: (string) 'yyyy-MM-dd EEE', 159 | preferredStartOfWeek: (number) 0, 160 | 161 | enabledTooltip: (boolean) false, 162 | enabledTimetracking: (boolean) false, 163 | enabledFlashcards: (boolean) true, 164 | enabledJournals: (boolean) true, 165 | enabledWhiteboards: (boolean) true, 166 | enabledPropertyPages: (boolean) true, 167 | 168 | showBrackets: (boolean) true, 169 | 170 | graph: { 171 | name: (string) 'My Notes', 172 | path: (string) '/Users/User/Documents/My Notes', 173 | data: { 174 | favorites: (array of strings) [ 175 | 'logseq/plugins/Full House Templates', 176 | ... 177 | ], 178 | defaultHome: { 179 | 'page': (string) 'Journals', 180 | }, 181 | } 182 | } 183 | } 184 | ``` 185 | 186 | 187 | ### `c.tags` :id=tags-context 188 | Template tags context. Helpful utils for creating templates. 189 | Every item available in two ways: `c.tags.` and ``. 190 | 191 | See detailed [Reference for tags](reference__tags.md). 192 | 193 | #### Schema 194 | 195 | ```javascript 196 | { 197 | (string) tag name: (function) signature 198 | } 199 | ``` 200 | 201 | -------------------------------------------------------------------------------- /docs/reference__query_language__blocks.md: -------------------------------------------------------------------------------- 1 | ## `Query for blocks` 2 | See feature request [here](https://github.com/stdword/logseq13-full-house-plugin/issues/54). 3 | -------------------------------------------------------------------------------- /docs/reference__query_language__table.md: -------------------------------------------------------------------------------- 1 | Use this template tag to represent query results in a nice-looking table — just like the one in Logseq itself. 2 | 3 | `query.table(rows, fields?, {orderBy?, orderDesc?})` 4 | 5 | - `rows`: an array of objects or an array of arrays with values 6 | - `fields`: (optional) field names to represent (default: auto-generated) 7 | - just the names of the columns if `rows` is an array of arrays with values 8 | - columns will get auto names if the parameter is absent 9 | - names to retrieve values from an object if `rows` is an array of objects 10 | - could be names of object properties if `rows` is an array of **page** objects 11 | - could be paths (see [`dev.get`](reference__tags_dev.md#dev-get) documentation) 12 | - `orderBy`: (optional) field name to order by (default: no ordering will be performed) 13 | - needs to be one of the provided `fields` if `rows` is an array of arrays with values 14 | - could be any object attribute (or path to attribute) if `rows` is an array of objects 15 | - `orderDesc`: (optional, boolean) set order direction to descending (default: *false* — ascending) 16 | 17 |
18 |
19 | 20 | - Let's take this query as an example of page objects data: 21 | ```javascript 22 | ``{ 23 | var books = query.pages() 24 | .tags('book') 25 | .property('year') 26 | .integerValue('=', 1975) 27 | .get() 28 | }`` 29 | ``` 30 | 31 | 32 | #### ***Template*** 33 | - ``query.table(books)`` 34 | - ``query.table(books, ['page', 'year'])`` 35 | 36 | #### ***Rendered*** 37 | - |page|alias|author|category|year|tags| 38 | |:-- |:-- |:-- |:-- |:--:|:-- | 39 | |Патриция Баумгартен — Подарок с озера Ковичан|«Подарок с озера Ковичан», «Gifts from Lake Cowichan»|Патриция Баумгартен|📖/psy/types/gestalt|1975|book| 40 | |Даниэл Розенблат — Открывая двери. Вступление в гештальт-терапию|«Открывая двери. Вступление в гештальт-терапию», «Opening Doors. What Happens in Gestalt Therapy»|Даниэл Розенблат|📖/psy/types/gestalt|1975|book| 41 | - |page|year| 42 | |:-- |:--:| 43 | |Патриция Баумгартен — Подарок с озера Ковичан|1975| 44 | |Даниэл Розенблат — Открывая двери. Вступление в гештальт-терапию|1975| 45 | 46 | 47 | 48 | 49 | 50 | #### ***Template*** 51 | - ``query.table(books, ['author', '@alias.-1', 'year'])`` 52 | - ``query.table(books, ['author', '@alias.-1', 'year'], {orderBy: 'author'})`` 53 | 54 | #### ***Rendered*** 55 | - |author|@alias.-1|year| 56 | |:-- |:-- |:--:| 57 | |Патриция Баумгартен|«Gifts from Lake Cowichan»|1975| 58 | |Даниэл Розенблат|«Opening Doors. What Happens in Gestalt Therapy»|1975| 59 | - |author|@alias.-1|year| 60 | |:-- |:-- |:--:| 61 | |Даниэл Розенблат|«Opening Doors. What Happens in Gestalt Therapy»|1975| 62 | |Патриция Баумгартен|«Gifts from Lake Cowichan»|1975| 63 | 64 | 65 | 66 | 67 | 68 | #### ***Template*** 69 | - ```javascript 70 | ``{ 71 | var journals = query.pages() 72 | .day('in', date.today, 'month') 73 | .get(false) 74 | }`` 75 | ``query.table(journals, ['page'])`` 76 | ``query.table(journals, ['page'], {orderBy: 'journal-day'})`` 77 | ``` 78 | 79 | #### ***Rendered*** 80 | - |page| 81 | |:-- | 82 | |2024-07-01 Mon| 83 | |2024-07-16 Tue| 84 | |2024-07-07 Sun| 85 | - |page| 86 | |:-- | 87 | |2024-07-01 Mon| 88 | |2024-07-07 Sun| 89 | |2024-07-16 Tue| 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/reference__syntax.md: -------------------------------------------------------------------------------- 1 | Inside template you can write any text you want. But there is special syntax to access JavaScript environment. 2 | 3 | ## JavaScript environment :id=js-env 4 | Has some particular qualities: 5 | - Logseq blocks can share variables created in JS environment. The ones created in the first block are available in any child block and any sibling block. 6 | - Only `var` variables can be shared, not `let` & `const`. 7 | - To share a function or a class assign it to `var` variable. 8 | - JS code execution runs in the order that blocks are visible in Logseq. And it is synchronous-like: async/await syntax is available, but every next block waits for the previous one to finish. 9 | - Global functions like `fetch` need to be prefixed with `window`: `window.fetch`, `window.alert`, etc. 10 | 11 | 12 | ## \`\`...\`\` :id=interpolation-syntax 13 | Use double back-tick «` `` `» to *start* and *finish* JavaScript expression. 14 | 15 | 16 | 17 | Simple JavaScript expressions: 18 | 19 | 20 | 21 | #### ***Template*** 22 | Calculate: ` ``1 + 2`` ` 23 | 24 | #### ***Rendered*** 25 | Calculate: 3 26 | 27 | 28 | 29 | Accessing [context](reference__context.md) data: 30 | 31 | 32 | 33 | #### ***Template*** 34 | Current page name: ` ``c.page.name.italics()`` ` 35 | 36 | #### ***Rendered*** 37 | Current page name: *Test Page* 38 | 39 | 40 | 41 | 42 | [Template tags](reference__tags.md) usage: 43 | 44 | 45 | 46 | #### ***Template*** 47 | Argument value: ` ``empty(c.args.some_arg, 'Unspecified')`` ` 48 | 49 | #### ***Rendered*** 50 | Argument value: Unspecified 51 | 52 | 53 | 54 | 55 | 56 | 57 | ## \`\`[...]\`\` :id=reference-interpolation-syntax 58 | Use «` ``[ `» and «` ]`` `» to call [`ref`](reference__tags.md#ref) template tag in a short form. 59 | 60 | 61 | 62 | These lines are completely the same: 63 | 64 | 65 | 66 | #### ***Template*** 67 | Reference to current page: ` ``[c.page]`` ` \ 68 | Reference to current page: ` ``ref(c.page)`` ` 69 | 70 | #### ***Rendered*** 71 | Reference to current page: [[Test Page]] \ 72 | Reference to current page: [[Test Page]] 73 | 74 | 75 | 76 | 77 | 78 | Use an alias for reference: 79 | 80 | 81 | 82 | #### ***Template*** 83 | Reference to current page: ` ``[c.page, "Current"]`` ` 84 | 85 | #### ***Rendered*** 86 | Reference to current page: [Current]([[Test Page]]) 87 | 88 | 89 | 90 | 91 | 92 | ## \`\`@...\`\` :id=dates-nlp-syntax 93 | Use «` ``@ `» and «` `` `» to call [`date.nlp`](reference__tags.md#date-nlp) template tag in a short form. 94 | 95 | 96 | 97 | These lines are completely the same: 98 | 99 | 100 | 101 | #### ***Template*** 102 | - ` ``@in two days`` ` 103 | - ` ``ref(date.nlp('in two days'))`` ` 104 | 105 | #### ***Rendered*** 106 | - [[2023-08-14 Mon]] 107 | - [[2023-08-14 Mon]] 108 | 109 | 110 | 111 | 112 | 113 | 114 | Start from current journal page or any date: 115 | 116 | 117 | 118 | #### ***Template*** 119 | - Current journal: ` ``c.page.name.italics()`` ` 120 | - ` ``@in two days, page`` ` 121 | - ` ``@in two days, 2020-01-02`` ` 122 | 123 | #### ***Rendered*** 124 | - Current journal: *2020-01-01 Wed* 125 | - [[2020-01-03 Fri]] 126 | - [[2020-01-04 Sat]] 127 | 128 | 129 | 130 | 131 | 132 | 133 | ## \`\`{...}\`\` :id=statement-syntax 134 | Use «` ``{ `» and «` }`` `» to execute custom JavaScript code. 135 | 136 | 137 | 138 | JavaScript statements: 139 | 140 | 141 | 142 | #### ***Template*** 143 | ` ``{ var x = 13 }`` ` \ 144 | Value of variable: ` ``x`` ` 145 | 146 | #### ***Rendered*** 147 | Value of variable: 13 148 | 149 | 150 | 151 | 152 | **Note**. There is no interpolation to value: 153 | 154 | 155 | 156 | #### ***Template*** 157 | No result: ` ``{ c.page.name }`` ` 158 | 159 | #### ***Rendered*** 160 | No result: 161 | 162 | 163 | 164 | But there are the special `out` & `outn` functions to output info within ` ``{...}`` `: 165 | 166 | 167 | #### ***Template*** 168 | - ```javascript 169 | ``{ for (const i of [1, 2, 3]) out(i) }`` 170 | ``` 171 | - ```javascript 172 | ``{ for (const i of [1, 2, 3]) outn(i) }`` 173 | ``` 174 | 175 | #### ***Rendered*** 176 | - 123 177 | - 1 \ 178 | 2 \ 179 | 3 180 | 181 | 182 | 183 | 184 | 185 | ### Cursor positioning with ` ``{|}`` ` :id=cursor-positioning 186 | This special syntax positions the cursor after template insertion. 187 | 188 | - Use ` ``{|}`` ` once to position cursor at the specified place. 189 | - Use ` ``{|}`` ` twice to make a selection between two cursor positions. 190 | 191 | ?> This will activate edit mode. However, due to Logseq restrictions, only one block can be edited at a time. Therefore, you can only use this syntax once in the entire template." 192 | 193 | 194 | #### ***Template*** 195 | - The cursor is before this ``{|}``word 196 | - And this ``{|}``word``{|}`` will be selected 197 | 198 | #### ***Rendered*** 199 | - The cursor is before this   **|** word 200 | - And this word will be selected 201 | 202 | 203 | 204 | To position the cursor from code inside curly brackets, use `cursor()` template tag: 205 | 206 | 207 | #### ***Template*** 208 | ```javascript 209 | ``{ 210 | var sentence = `Hello, ${cursor()}user${cursor()}!` 211 | }`` 212 | 213 | ``sentence`` 214 | ``` 215 | 216 | #### ***Rendered*** 217 | Hello, user! 218 | 219 | 220 | 221 | To position cursor inside [spawned blocks](reference__tags_advanced.md#blocks-spawn), use *cursorPosition* parameter: 222 | 223 | 224 | #### ***Template*** 225 | - ```javascript 226 | ``{ 227 | blocks.spawn('Hello, {|}Logseq{|}!', null, {cursorPosition: true}) 228 | }`` 229 | ``` 230 | - ```javascript 231 | ``{ 232 | blocks.spawn.tree({ 233 | content: 'Hello, {|}Full House Templates{|}!', 234 | data: {cursorPosition: true}, 235 | }) 236 | }`` 237 | ``` 238 | 239 | #### ***Rendered*** 240 | - Hello, Logseq! 241 | - Hello, Full House Templates! 242 | 243 | 244 | 245 | 246 | 247 | ### Extended `Array` type :id=extended-array-type 248 | The `Array` type is extended with helpful functions: 249 | - `Array.zip(...arrays)` 250 | - `.zipWith(...arrays)` 251 | - `.unique()` 252 | - `.sorted(keyfunc)` 253 | - `.groupby(keyfunc, wrapToObject? = true)` 254 | - `.countby(keyfunc, wrapToObject? = true)` 255 | 256 | 257 | #### ***Template*** 258 | - ```javascript 259 | Array.zip([1, 2, 3], ['a', 'b']) 260 | [1, 2, 3].zipWith(['a', 'b']) 261 | ``` 262 | - ```javascript 263 | [1, 2, 2, 3, 2, 1, 1].unique() 264 | ``` 265 | 266 | #### ***Rendered*** 267 | - [[1, "a"], [2, "b"]] 268 | - [1, 2, 3] 269 | 270 | 271 | 272 | 273 | 274 | #### ***Template*** 275 | - ```javascript 276 | var items = [ 277 | { type: "vegetables", quantity: 5 }, 278 | { type: "fruit", quantity: 1 }, 279 | { type: "meat", quantity: 23 }, 280 | { type: "fruit", quantity: 5 }, 281 | { type: "meat", quantity: 22 }, 282 | ] 283 | ``` 284 | - ```javascript 285 | items.sorted((x) => [x.type, x.quantity]).join('\n') 286 | ``` 287 | - ```javascript 288 | items.groupby((x) => x.type) 289 | ``` 290 | - ```javascript 291 | items.countby((x) => x.type) 292 | items.countby((x) => x.type, false) 293 | ``` 294 | 295 | #### ***Rendered*** 296 | - ```javascript 297 | // .sorted 298 | {"type": "fruit", "quantity": 1 } 299 | {"type": "fruit", "quantity": 5 } 300 | {"type": "meat", "quantity": 22 } 301 | {"type": "meat", "quantity": 23 } 302 | {"type": "vegetables", "quantity": 5 } 303 | ``` 304 | - ```javascript 305 | // .groupby 306 | { 307 | "vegetables": [ 308 | {"type": "vegetables", "quantity": 5 } 309 | ], 310 | "fruit": [ 311 | { "type": "fruit", "quantity": 1 }, 312 | { "type": "fruit", "quantity": 5 } 313 | ], 314 | "meat": [ 315 | { "type": "meat", "quantity": 23 }, 316 | { "type": "meat", "quantity": 22 } 317 | ] 318 | } 319 | ``` 320 | - ```javascript 321 | // .countby wrapped to object 322 | { 323 | "vegetables": 1, 324 | "fruit": 2, 325 | "meat": 2 326 | } 327 | 328 | // .countby non-wrapped 329 | [ ["vegetables", 1], ["fruit", 2], ["meat", 2] ] 330 | ``` 331 | 332 | 333 | 334 | 335 | 336 | ## <%...%> :id=standard-syntax 337 | Use «`<%`» and «`%>`» to render standard Logseq Templates 338 | 339 | 340 | 341 | Accessing pages & current time: 342 | 343 | 344 | 345 | #### ***Template*** 346 | Time: `<% time %>` 347 | 348 | Reference to current page: `<% current page %>` 349 | 350 | Today's journal page: `<% today %>` \ 351 | Yesterday's journal page: `<% yesterday %>` \ 352 | Tomorrow's journal page: `<% tomorrow %>` 353 | 354 | #### ***Rendered*** 355 | Time: 20:32 356 | 357 | Reference to current page: [[Test Page]] 358 | 359 | Today's journal page: [[2023-08-12]] \ 360 | Yesterday's journal page: [[2023-08-11]] \ 361 | Tomorrow's journal page: [[2023-08-13]] 362 | 363 | 364 | 365 | 366 | Journal pages via natural language dates: 367 | 368 | 369 | 370 | #### ***Template*** 371 | Relative: \ 372 | `<% last friday %>` \ 373 | `<% 5 days ago %> %>` \ 374 | `<% 2 weeks from now %>` 375 | 376 | Specified: \ 377 | `<% 17 August 2013 %>` \ 378 | `<% Sat Aug 17 2013 %>` \ 379 | `<% 2013-08-17 %>` 380 | 381 | #### ***Rendered*** 382 | Relative: \ 383 | [[2023-08-11]] \ 384 | [[2023-08-07]] \ 385 | [[2023-08-26]] 386 | 387 | Specified: \ 388 | [[2013-08-17]] \ 389 | [[2013-08-17]] \ 390 | [[2013-08-17]] 391 | 392 | 393 | 394 | -------------------------------------------------------------------------------- /docs/reference__tags.md: -------------------------------------------------------------------------------- 1 | ## `Reference operations` :id=section-refs 2 | 3 | ### `ref` :id=ref 4 | Make a reference to page (`[[name]]`) or block (`((uuid))`). 5 | 6 | `ref(obj, label?)` 7 | - `obj` is one of the following items: 8 | - page name 9 | - page context 10 | - block uuid 11 | - block context 12 | - dayjs object 13 | - date ISO string (YYYY-MM-DD) 14 | - `label`: (optional) custom label for the reference 15 | 16 | ?> Could be used implicitly with [` ``[...]`` `](reference__syntax.md#reference-interpolation-syntax) syntax 17 | 18 | 19 | Example: 20 | 21 | 22 | #### ***Template*** 23 | `ref('page name')` \ 24 | `ref(c.currentPage)` \ 25 | `ref('64e61063-1689-483f-903f-409766d81b2e')` \ 26 | `ref(c.block)` \ 27 | `ref(date.tomorrow)` \ 28 | `ref('2020-01-01')` 29 | 30 | #### ***Rendered*** 31 | [[page name]] \ 32 | [[page name]] \ 33 | ((64e61063-1689-483f-903f-409766d81b2e)) \ 34 | ((64e61063-1689-483f-903f-409766d81b2e)) \ 35 | [[2023-08-12 Sat]] \ 36 | [[2020-01-01 Wed]] 37 | 38 | 39 | Additional usage: 40 | 41 | 42 | #### ***Template*** 43 | - ` ``['page name']`` ` 44 | - `ref('the long page name', 'page')` 45 | 46 | #### ***Rendered*** 47 | - [[page name]] 48 | - [[page]] 49 | 50 | 51 | 52 | 53 | 54 | ### `tag` :id=tag 55 | Make a tag reference to page (`#name`). 56 | 57 | `tag(obj)` 58 | - `obj` is one of the following items: 59 | - page name 60 | - page context 61 | - dayjs object 62 | - date ISO string (YYYY-MM-DD) 63 | 64 | 65 | #### ***Template*** 66 | `tag('name')` \ 67 | `tag(c.currentPage)` \ 68 | `tag(date.tomorrow)` \ 69 | `tag('2020-01-01')` 70 | 71 | #### ***Rendered*** 72 | #name \ 73 | #[[page name]] \ 74 | #[[2023-08-12 Sat]] \ 75 | #[[2020-01-01 Wed]] 76 | 77 | 78 | 79 | 80 | ### `embed` :id=embed 81 | Call an `embed` macro for pages `{{embed [[page]] }}` or blocks `{{embed ((uuid...)) }}`. 82 | 83 | `embed(obj)` 84 | - `obj`: one of the allowed objects for [`ref`](#ref) 85 | 86 | 87 | #### ***Template*** 88 | `embed('page name')` \ 89 | `embed(c.currentPage)` \ 90 | `embed('64e61063-1689-483f-903f-409766d81b2e')` \ 91 | `embed(c.block)` \ 92 | `embed(date.tomorrow)` \ 93 | `embed('2020-01-01')` 94 | 95 | #### ***Rendered*** 96 | {{embed [[page name]] }} \ 97 | {{embed [[page name]] }} \ 98 | {{embed ((64e61063-1689-483f-903f-409766d81b2e)) }} \ 99 | {{embed ((64e61063-1689-483f-903f-409766d81b2e)) }} \ 100 | {{embed [[2023-08-12 Sat]] }} \ 101 | {{embed [[2020-01-01 Wed]] }} 102 | 103 | 104 | 105 | 106 | ## `String operations` :id=section-string 107 | 108 | ### `empty` :id=empty 109 | Checks whether the object is empty or not. If it is empty it will be replaced with fallback object. 110 | 111 | `empty(obj, fallback = '') → obj, fallback` 112 | - `obj`: value to check for emptyness 113 | - `fallback`: replacement object (default: `''`) 114 | 115 | !> It is very different than JavaScript «empty» values. \ 116 | \ 117 | *Empty* values example: \ 118 | `null`, `undefined`, `[]`, `{}`, `' '`, `''`, `""`, ` `` `, `«»`, `-`, `—`, etc. \ 119 | \ 120 | *Non-empty* values example: \ 121 | `0`, `false`, etc. 122 | 123 | 124 | #### ***Template*** 125 | ` ``empty([1, 2, 3])`` ` \ 126 | ` ``empty([])`` ` \ 127 | ` ``empty([], 'empty array')`` ` 128 | 129 | #### ***Rendered*** 130 | [1, 2, 3] \ 131 | \ 132 | empty array 133 | 134 | 135 | 136 | 137 | ### `bool` :id=bool 138 | Checks whether the string is `true`, `false` or non-boolean. 139 | 140 | `bool(obj, fallback = '') → true, false, fallback` 141 | - `obj`: value to check to be boolean 142 | - `fallback`: replacement object (default: `''`) 143 | 144 | !> It is very different than JavaScript «boolean» values. \ 145 | \ 146 | *true* values example: \ 147 | `✅`, `+`, `1`, `v`, `yes`, `ok`, `on`, etc. \ 148 | \ 149 | *false* values example: \ 150 | `❌`, `-`, `0`, `x`, `no`, `none`, `OFF`, etc. 151 | 152 | 153 | #### ***Template*** 154 | ` ``bool('true')`` ` \ 155 | ` ``bool('f')`` ` \ 156 | ` ``bool([])`` ` 157 | 158 | #### ***Rendered*** 159 | true \ 160 | false \ 161 | null 162 | 163 | 164 | 165 | 166 | ### `when` :id=when 167 | If the object is empty (in JavaScript way) return `fallback`, otherwise return `result` (which can be based on object value). 168 | 169 | `when(obj, result, fallback = '')` 170 | - `obj`: value to check the emptyness 171 | - `result`: value for non-empty case 172 | - Can contain `$1`, `${}`, `${_}`: these values will be replaces with object itself 173 | - `fallback`: value for empty case (default: `''`) 174 | 175 | 176 | #### ***Template*** 177 | This is ` ``when(c.page.day, 'journal page for $1 day', 'page')`` ` \ 178 | Root namespace: ` ``when(c.page.name.split('/').at(0), '$1')`` ` 179 | 180 | #### ***Rendered*** 181 | *In journal page:* 182 | This is journal page for 2023-08-12 Sat 183 | Root namespace: 2023-08-12 Sat 184 | 185 | *In logseq/plugins page:* 186 | This is journal page 187 | Root namespace: logseq 188 | 189 | 190 | 191 | 192 | 193 | ### `fill` :id=fill 194 | Complements the input string `value` with `char` characters to reach the `width` width. 195 | 196 | `fill(value, char, width, align = 'right')` 197 | - `value`: value to complement 198 | - `char`: character to use as filler 199 | - `width`: full width of result string 200 | - `align`: align initial value to the *left*, *right* or *center* (default: `'right'`) 201 | 202 | 203 | #### ***Template*** 204 | «` ``fill(1, '+', 2)`` `» \ 205 | «` ``fill(13, ' ', 4, 'center')`` `» \ 206 | «` ``fill('x', 'y', 3, 'left')`` `» 207 | 208 | #### ***Rendered*** 209 | «+1» \ 210 | « 13 » \ 211 | «xyy» 212 | 213 | 214 | 215 | 216 | ### `zeros` :id=zeros 217 | Shortcut for [`fill`](#fill) with zeros and right alignment. 218 | 219 | `zeros(value, width)` 220 | - Same as `fill(value, '0', width)` 221 | - `value`: value to complement 222 | - `width`: full width of result string 223 | 224 | 225 | #### ***Template*** 226 | «` ``zeros(1, 2)`` `» \ 227 | «` ``zeros(13, 4)`` `» 228 | 229 | #### ***Rendered*** 230 | «01» \ 231 | «0013» 232 | 233 | 234 | 235 | 236 | ### `spaces` :id=spaces 237 | Shortcut for [`fill`](#fill) with spaces. 238 | 239 | `spaces(value, width, align)` 240 | - Same as `fill(value, ' ', width, align)` 241 | - `value`: value to complement 242 | - `width`: full width of result string 243 | - `align`: align initial value to the *left*, *right* or *center* (default: `'right'`) 244 | 245 | 246 | #### ***Template*** 247 | «` ``spaces(1, 2)`` `» \ 248 | «` ``spaces('hey', 5, 'center')`` `» 249 | 250 | #### ***Rendered*** 251 | « 1» \ 252 | « hey » 253 | 254 | 255 | 256 | 257 | ## `Date constants` :id=section-date-constants 258 | 259 | ### `time` :id=time 260 | String time in format: `'HH:mm'`. In local timezone. 261 | 262 | 263 | #### ***Template*** 264 | ` ``time`` ` 265 | 266 | #### ***Rendered*** 267 | 23:32 268 | 269 | 270 | 271 | 272 | ### `yesterday` :id=yesterday 273 | String yesterday date in ISO format: `'YYYY-MM-DD'`. In local timezone. 274 | 275 | 276 | #### ***Template*** 277 | ` ``yesterday`` ` 278 | 279 | #### ***Rendered*** 280 | 2023-08-11 281 | 282 | 283 | 284 | 285 | ### `today` :id=today 286 | String today date in ISO format: `'YYYY-MM-DD'`. In local timezone. 287 | 288 | 289 | #### ***Template*** 290 | ` ``today`` ` 291 | 292 | #### ***Rendered*** 293 | 2023-08-12 294 | 295 | 296 | 297 | 298 | ### `tomorrow` :id=tomorrow 299 | String tomorrow date in ISO format: `'YYYY-MM-DD'`. In local timezone. 300 | 301 | 302 | #### ***Template*** 303 | ` ``tomorrow`` ` 304 | 305 | #### ***Rendered*** 306 | 2023-08-13 307 | 308 | 309 | 310 | 311 | ## `date` 312 | 313 | ### `.now` :id=date-now 314 | [Day.js](https://day.js.org) object with now date and time value. 315 | 316 | 317 | #### ***Template*** 318 | Local timezone: ` ``date.now`` (``time``) ` \ 319 | UTC: ` ``date.now.toString()`` ` \ 320 | UTC ISO: ` ``date.now.toISOString()`` ` 321 | 322 | #### ***Rendered*** 323 | Local timezone: 2023-08-12 Wed (23:23) \ 324 | UTC: Wed, 12 Aug 2023 20:23:00 GMT \ 325 | UTC ISO: 2023-08-12T20:23:00.000Z 326 | 327 | 328 | ### `.today` :id=date-today 329 | Same as [`date.now`](#date-now), but points to the starts of the day (00:00 in local timezone). 330 | 331 | ### `.yesterday` :id=date-yesterday 332 | Same as [`date.today`](#date-today), but for yesterday date. 333 | 334 | ### `.tomorrow` :id=date-tomorrow 335 | Same as [`date.today`](#date-today), but for tomorrow date. 336 | 337 | 338 | 339 | ### `.from` :id=date-from 340 | [Day.js](https://day.js.org) object builder. See whole documentation section [here](https://day.js.org/docs/en/parse/parse) for details. 341 | 342 | 343 | #### ***Template*** 344 | Timezone: ` ``date.from.tz.guess()``, ``date.from().offsetName()`` ` \ 345 | ` ``date.from('2024').toISOString()`` ` \ 346 | ` ``date.from('2024-08').toISOString()`` ` \ 347 | ` ``date.from('2024-08-12').toISOString()`` ` \ 348 | ` ``date.from('12|08|2024 (23h)', 'DD|MM|YYYY (HH[h])').toISOString()`` ` — escaping with `[]` for `h` 349 | 350 | #### ***Rendered*** 351 | Europe/Minsk, GMT+3 \ 352 | 2023-12-31T21:00:00.000Z \ 353 | 2024-07-31T21:00:00.000Z \ 354 | 2024-08-11T21:00:00.000Z \ 355 | 2024-08-12T20:00:00.000Z — escaping with `[]` for `h` 356 | 357 | 358 | 359 | 360 | ### `.fromJournal` :id=date-from-journal 361 | Conversion from Logseq internal date format (e.g. `20240820`) to [Day.js](https://day.js.org) object. 362 | 363 | 364 | #### ***Template*** 365 | ` ``date.fromJournal(20240820).toISOString()`` ` 366 | 367 | #### ***Rendered*** 368 | 2024-08-20T21:00:00.000Z 369 | 370 | 371 | 372 | ### `.nlp` :id=date-nlp 373 | Getting dates via natural language processing. 374 | 375 | `date.nlp(query, moment = 'now')` 376 | - `query`: string representation of NLP date 377 | - `moment`: zero point to base relative dates on (default: `'now'`) 378 | - Use `'page'` to set relative moment to current journal's day 379 | - Acts like `'now'` if page is not a journal page 380 | - To set any custom moment use: 381 | - String in ISO format (e.g `'2020-01-01'`) 382 | - Dayjs object (e.g. `date.tomorrow`). See [*date.from*](#date-from) for details 383 | 384 | ?> Could be used implicitly with [` ``@...`` `](reference__syntax.md#dates-nlp-syntax) syntax 385 | 386 | 387 | 388 | Equivalent of `<% in two days %>`: 389 | 390 | 391 | 392 | #### ***Template*** 393 | - Standard Logseq syntax: `<% in two days %>` 394 | - Plugin syntax: ` ``[ date.nlp('in two days') ]`` ` 395 | - Shorthand syntax: ` ``@in two days`` ` 396 | 397 | #### ***Rendered*** 398 | - Standard Logseq syntax: [[2023-08-12 Sat]] 399 | - Plugin syntax: [[2023-08-12 Sat]] 400 | - Shorthand syntax: [[2023-08-12 Sat]] 401 | 402 | 403 | 404 | 405 | Changing the relative moment: 406 | 407 | 408 | 409 | #### ***Template*** 410 | Tomorrow: ` ``[ date.nlp('in two days', date.yesterday) ]`` ` \ 411 | The next day after date: ` ``[ date.nlp('tomorrow', '2020-01-01') ]`` ` 412 | 413 | #### ***Rendered*** 414 | Tomorrow: [[2023-08-13 Sun]] \ 415 | The next day after date: [[2020-01-02 Thu]] 416 | 417 | 418 | 419 | 420 | In journal page `[[2020-01-01]]`: 421 | 422 | 423 | 424 | #### ***Template*** 425 | Next journal page: ` ``[ date.nlp('tomorrow', 'page') ]`` ` 426 | 427 | #### ***Rendered*** 428 | Next journal page: [[2020-01-02 Thu]] 429 | 430 | 431 | 432 | -------------------------------------------------------------------------------- /docs/reference__tags_nesting.md: -------------------------------------------------------------------------------- 1 | 2 | ## `include` :id=nesting-include 3 | Include another template by it's name. 4 | 5 | There are different ways and different situations around inclusion: 6 | - **Runtime** inclusion renders an included template or view at the moment of current template rendering. 7 | - **Lazy** inclusion relies on the renderer macro (`{{renderer ...}}`) to render an included template or view at a later time, after the current template rendering is finished. 8 | 9 | Use the table to select the appropriate template tag: 10 | 11 | | intention ↓       rendering as → | [`template`](reference__commands.md#template-command) | [`view`](reference__commands.md#template-view-command) | 12 | | :-- | :-- | :--: | 13 | | Include (*runtime*) | `include` | `include` | 14 | | Include (*lazy*) as template | `include.template` | — | 15 | | Include (*lazy*) as view | `include.view`
`include.inlineView` | — | 16 | 17 | --- 18 | 19 | 20 | - `async include(name, args?)` → string 21 | - `name`: template name. Only templates with `template::` property can be included. 22 | - `args`: (optional) arguments for included template. Can be a string or an array of strings. 23 | - If not specified `template-usage::` property will be used to get default arguments' values. 24 | - If you need to ignore `template-usage::` and include template with no arguments: use explicit empty value `[]` or `''`. 25 | 26 | !> **Returns only template head as a string!** Head children nodes and tail nodes will be spawned automatically. This template tag is not intended to access whole included template blocks tree! 27 | 28 | 29 | 30 | #### ***Template*** 31 | This is ` ``await include('nested')`` `! 32 | 33 | #### ***Template «nested»*** 34 | ` ``c.template.name`` ` 35 | 36 | #### ***Rendered*** 37 | This is nested! 38 | 39 | 40 | 41 | 42 | #### ***Template*** 43 | Args from usage string: ` ``await include('nested')`` ` \ 44 | No args: ` ``await include('nested', [])`` ` \ 45 | No args: ` ``await include('nested', '')`` ` \ 46 | Explicit args: ` ``await include('nested', ':value ARG')`` ` \ 47 | Explicit args: ` ``await include('nested', [':value ARG', ':another TOO'])`` ` 48 | 49 | #### ***Template «nested»*** 50 | ``` 51 | - template:: nested 52 | template-usage:: :value USAGE 53 | - ``c.args.value`` 54 | ``` 55 | 56 | #### ***Rendered*** 57 | Args from usage string: USAGE \ 58 | No args: \ 59 | No args: \ 60 | Explicit args: ARG \ 61 | Explicit args: ARG 62 | 63 | 64 | 65 | 66 | #### ***Template*** 67 | Buy list: \ 68 | ` ``{ for (const item of ['apple', 'orange', 'lemon']) { }`` ` \ 69 |     → ` ``await include('nested', item)`` ` \ 70 | ` ``{ } }`` ` 71 | 72 | #### ***Template «nested»*** 73 | ` ``c.args.$1.bold()`` ` 74 | 75 | #### ***Rendered*** 76 | Buy list: \ 77 |     → **apple** \ 78 |     → **orange** \ 79 |     → **lemon** 80 | 81 | 82 | ?> Note: if you need to place buy list items in child blocks, use [blocks spawning](reference__tags_advanced.md#blocks-spawn) 83 | 84 | 85 | ### `.template` :id=include-template 86 | Lazy inclusion as template. 87 | 88 | - `async include.template(name, args?)` 89 | 90 | 91 | #### ***Template*** 92 | ` ``await include.template('test', ':arg 13')`` ` 93 | 94 | #### ***Rendered*** 95 | `{{renderer :template, test, :arg 13}}` 96 | 97 | 98 | 99 | 100 | ### `.view` :id=include-view 101 | Lazy inclusion as a view 102 | 103 | - `async include.view(name, args?)` 104 | 105 | 106 | #### ***Template*** 107 | ` ``await include.view('test', ':arg 13')`` ` 108 | 109 | #### ***Rendered*** 110 | `{{renderer :template-view, test, :arg 13}}` 111 | 112 | 113 | 114 | 115 | ### `.inlineView` :id=include-inline-view 116 | Lazy inclusion as an inline view 117 | 118 | - `async include.inlineView(body, args?)` 119 | - `body`: A string with JavaScript code for inline view. See details [here](reference__commands.md#inline-view-command). 120 | 121 | 122 | #### ***Template*** 123 | ` ``await include.inlineView('c.args.arg', ':arg 13')`` ` 124 | 125 | #### ***Rendered*** 126 | `{{renderer :view, "c.args.arg", :arg 13}}` 127 | 128 | 129 | 130 | 131 | ## `layout` :id=nesting-layout 132 | Include another template by it's name. Acts like [`include`](#nesting-include) with the only difference: it preserves outer-template [arg-properties](reference__args.md#arg-properties). Use it to **inherit templates**. 133 | 134 | Read the difference between lazy & runtime inclusion in [`include`](#nesting-include) section. 135 | 136 | Use the table to select the appropriate template tag: 137 | 138 | | intention ↓       rendering as → | [`template`](reference__commands.md#template-command) | [`view`](reference__commands.md#template-view-command) | 139 | | :-- | :-- | :--: | 140 | | Layout (*runtime*) | `layout` | `layout` | 141 | | Layout (*lazy*) as template | `layout.template` | — | 142 | | Layout (*lazy*) as view | not supported | — | 143 | 144 | --- 145 | 146 | - `async layout(name, args?)` 147 | - See parameters description in [`include`](#nesting-include) section. 148 | 149 | 150 | #### ***Template «included»*** 151 | ``` 152 | - template:: included 153 | arg-test:: ORIGINAL 154 | - ``c.args.test`` 155 | ``` 156 | 157 | #### ***Template «main»*** 158 | ``` 159 | - template:: main 160 | arg-test:: OVERRIDED 161 | - ``await include('included')`` 162 | - ``await layout('included')`` 163 | - ``await layout('included', layout.args('test'))`` 164 | - ``await layout('included', layout.args(['test', c.args.test]))`` 165 | - ``await layout('included', layout.args({test: 'COMPUTED'}))`` 166 | ``` 167 | 168 | #### ***Rendered*** 169 | - ORIGINAL 170 | - OVERRIDED 171 | - USER 172 | - USER 173 | - COMPUTED 174 | 175 | 176 | 177 | ?> Real life example is [here](https://github.com/stdword/logseq13-full-house-plugin/discussions/9#view-for-blocks), in the section «🏛view for blocks» 178 | 179 | 180 | ### `.template` :id=layout-template 181 | Lazy layout as template. 182 | 183 | - `async layout.template(name, args?)` 184 | 185 | 186 | #### ***Template*** 187 | ` ``await layout.template('test', ':arg 13')`` ` 188 | 189 | #### ***Rendered*** 190 | `{{renderer :template, test, :arg 13}}` 191 | 192 | 193 | 194 | 195 | ### `.args` :id=layout-args 196 | Used to pass-through current arguments to layout template. 197 | 198 | - `layout.args(...args?)` 199 | - `args`: (optional) an array or string 200 | - if unspecified: all arguments will be passed through automatically 201 | - if specified, every item could be: 202 | - the name of an argument 203 | - positional link to an argument: `$1`, `$2`, etc. 204 | - the pair of argument name and it's value: `[name, value]` 205 | - object with arguments' names as keys and values as values: `{name1: v1, name2: v2, ...}` 206 | 207 | > See example of usage in [`layout`](#nesting-layout) section 208 | -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | > The goal of this document is to be the step by step guide to learn the basic parts of the plugin 2 | 3 | 4 | ## Creation 5 | 6 | ### How to create a template? 7 | - Just add `template` property with the name of the template as it's value (like Logseq standard templates) 8 | 9 | ``` 10 | - template:: simple 11 | - Hello, Logseq! 12 | ``` 13 | 14 | 15 | 16 | ### How to render the template? 17 | - Via [Insertion UI](reference__commands.md#insertion-ui): 18 | - Open it with the `⌘t` (or `ctrl+t`) default shortcut 19 | - Select the appropriate template to render it 20 | 21 | ?> Template's root block will be *skipped* by default. But you can [change](reference__configuring.md#parent-block-inclusion-control) this behavior 22 | 23 | 24 | 25 | ### How to render the non-template block? 26 | - Via block's [Context Menu](reference__commands.md#indirect): 27 | - Right-click on the block's bullet to open it 28 | - Select the «Copy as 🏛template» item 29 | - The command will be copied to clipboard. Paste it to any block. 30 | 31 | ?> Template's root block will be *included* by default. But you can [change](#reference__configuring.md#parent-block-inclusion-control) this behavior 32 | 33 | ?> Also the current block can be [rendered in a fast way](reference__commands.md#render-this-block-command) 34 | 35 | 36 | 37 | ### What is a "view" and how to use it? 38 | The template that renders it's content every time it becomes visible called `view`. See details [here](reference__commands.md#template-view-command). 39 | 40 | The view can be inserted in Insertion UI with `⌘↩︎` shortcut. 41 | 42 | 43 | 44 | ### What is an "inline view" and how to use it? 45 | Inline view is a fast way to create the single-line `view`. See details [here](reference__commands.md#inline-view-command). 46 | 47 | The inline view can be inserted with «/»-command `Insert inline 🏛️view`. 48 | 49 | 50 | 51 | ### What is a "template button" and how to use it? 52 | Template button is a clickable button for inserting particular templates. See details [here](reference__commands.md#template-button-command). 53 | 54 | The template button can be inserted in Insertion UI with `⌘⇧↩︎` shortcut. 55 | 56 | 57 | 58 | ### How to create the keyboard shortcut for the template? 59 | Use [this](reference__configuring.md#insertion-shortcut) instruction. 60 | 61 | 62 | 63 | ### What other rendering configuration are there? 64 | See [detailed description](reference__configuring.md). 65 | 66 | 67 | 68 | 69 | 70 | ## Template syntax 71 | 72 | ### Accessing meta information 73 | - Use special *context variables* with the two **back-ticks** brackets inside the template: 74 | 75 | ``` 76 | - template:: vars 77 | - Current page: ``c.page.name`` 78 | - This text was rendered from template «``c.template.name``» 79 | ``` 80 | 81 | Another variables: 82 | - `c.block`: block you are rendering template in: you can access its uuid, properties, related blocks 83 | - `c.page`: page of the block you are rendering template in: to get title, properties and journal day 84 | - `c.template`: name, properties & access to template block 85 | - `c.self`: template block from which current block is rendering: to get corresponding meta information 86 | 87 | 88 | See the full list and description [here](reference__context.md). 89 | 90 | 91 | 92 | ### Accessing properties values 93 | - Via `props` and `propsRefs` attributes of page and block [context objects](reference__context.md) 94 | 95 | ``` 96 | - template:: properties 97 | description:: An example of how to retrieve properties values 98 | - from template itself: ``c.template.props.description`` 99 | - from current block: ``c.self.props.message`` 100 | message:: hello! 101 | - from the page, the second tag: ``c.page.propsRefs.tags[1]`` 102 | - and from the destination block: ``c.block.props.info`` 103 | ``` 104 | 105 | ``` 106 | - {{renderer :template, properties}} 107 | info:: This is so powerful! 108 | ``` 109 | 110 | 111 | 112 | ### References to pages and blocks 113 | - Use [`ref`](reference__tags.md#ref) template tag or [special syntax](reference__syntax.md#reference-interpolation-syntax): 114 | 115 | ``` 116 | - template:: refs 117 | - Links to the page: 118 | - ``ref(c.page)`` 119 | - ``[c.page]`` 120 | - And links to blocks: 121 | - ``ref(c.block)`` 122 | - ``[c.block]`` 123 | - Embedding the current block: 124 | - ``embed(c.block)`` 125 | ``` 126 | 127 | 128 | 129 | ### References to journals 130 | - With [`ref`](reference__tags.md#ref) template tag or [special syntax](reference__syntax.md#reference-interpolation-syntax) 131 | - There is a way to get `today`, `yesterday` and `tomorrow` dates; and `now` — the current time 132 | - **Note**: these values based on today's date, not on journal's date. To calculate date based on journal's date see [NLP](#nlp-references-to-journals) or [date calculation](#refs-journals) 133 | 134 | ``` 135 | - template:: refs to journals 136 | template-including-parent:: yes 137 | rendered-at:: ``today``, ``time`` 138 | - There is no difference between ``ref(today)`` and ``[today]`` 139 | - References to yesterday's and tomorrow's journals: 140 | - ``[yesterday]``, ``[tomorrow]`` 141 | - Reference to current journal page: ``[c.page.day]`` 142 | - And to specific journal (date in ISO format): ``['2023-03-01']`` 143 | ``` 144 | 145 | 146 | 147 | #### NLP 148 | - With [`date.npl`](reference__tags.md#date-nlp) template tag or [special syntax](reference__syntax.md#dates-nlp-syntax): 149 | 150 | ``` 151 | - template:: nlp refs 152 | - There is no difference between ``date.nlp('next friday')`` and ``@next friday`` 153 | - Use any simple date description in english: ``@in two days`` 154 | - Set the starting point date to calculate from: ``@in two days, 2023-03-01`` 155 | - References to yesterday's and tomorrow's journals: 156 | - ``@yesterday``, ``@tomorrow`` 157 | - ``@yesterday, page``, ``@tomorrow, page`` — with respect to current journal 158 | ``` 159 | 160 | 161 | 162 | #### Based on journal's date :id=refs-journals 163 | - Every journal page's `day` field and [template tags](reference__tags.md#date) `date.now`, `date.yesterday` and `date.tomorrow` is a special date objects 164 | - You can access them and use full power of [Day.js](https://day.js.org) API 165 | 166 | 167 | ``` 168 | - template:: dynamic refs to journals 169 | - There is a difference between ``today`` and ``date.today`` 170 | - But reference to a journal can be made with both of them: 171 | - ``[today]`` 172 | - ``[date.today]`` 173 | - References to yesterday's and tomorrow's journals (with respect to current journal): 174 | - ``@yesterday, page``, ``@tomorrow, page`` — special mode of @-syntax 175 | - Calculation from current journal page: ``[c.page.day.add(1, 'week')]`` 176 | ``` 177 | 178 | 179 | 180 | #### Formatting and constructing 181 | - You can access different parts of date object and format it as string 182 | - Documentation for getting date parts [→](https://day.js.org/docs/en/get-set/get-set) 183 | - Documentation for `.format` date pattern: [→](https://day.js.org/docs/en/display/format#list-of-all-available-formats) and [→](https://day.js.org/docs/en/plugin/advanced-format) 184 | 185 | ``` 186 | - template:: formatting 187 | - Accessing parts of date object: 188 | - Time: ``zeros(date.now.hour(), 2)``:``zeros(date.now.minute(), 2)`` 189 | - Week: ``date.now.year()``-W``date.now.week()`` 190 | - Format to any custom string: 191 | - Time: ``date.now.format('HH:mm')`` 192 | - Week: ``date.now.format('YYYY-[W]w')`` 193 | - Special formatting form to easily repeat journals name format: 194 | - ``date.now.format('page')`` 195 | - ``date.now.toPage()`` 196 | ``` 197 | 198 | 199 | - To construct references to specific journal pages use [`date.from`](reference__tags.md#date-from) template tag 200 | - Documentation for date pattern [→](https://day.js.org/docs/en/parse/string-format#list-of-all-available-parsing-tokens) 201 | - Documentation of available units [→](https://day.js.org/docs/en/manipulate/add#list-of-all-available-units) 202 | 203 | ``` 204 | - template:: new dates 205 | - Constructing from any string with `date.from` template tag 206 | - ISO string: ``date.from('2023-03-01')`` 207 | - Custom format should be specified explicitly: ``date.from('2210', 'YYMM')`` 208 | - Or even several formats at a time: ``date.from('2210', ['YYMM', 'YYYYMM'])`` 209 | - Constructing by shifting date object 210 | - Last week: ``date.now.subtract(1, 'week').startOf('week')`` 211 | - This week: ``date.now.startOf('week')`` 212 | - Next week: ``date.now.add(1, 'w').startOf('w')`` 213 | - Last month: ``date.now.startOf('month').subtract(1, 'M')`` 214 | - Next year: ``date.now.endOf('y').add(1, 'ms')`` 215 | - This week's friday: ``date.now.startOf('day').weekday(5)`` 216 | - Next quarter: ``date.now.startOf('quarter').add(1, 'Q')`` 217 | ``` 218 | 219 | 220 | ### Cursor positioning 221 | - You can position cursor after template insertion with [`cursor`](reference__syntax.md#cursor-positioning) template tag 222 | 223 | 224 | 225 | 226 | ## JavaScript environment 227 | - All that templates magic is possible with JavaScript language 228 | - Template parts is literally JavaScript code, so if you familiar with JavaScript — you can use it extensively 229 | 230 | ``` 231 | - template:: js env 232 | - Full page name: «``c.page.name``» 233 | - The last word in page name: «``c.page.name.split('/').slice(-1)``» 234 | - PLugin's name: ``c.page.name.match(/plugins\/(?[^\/]+)/).groups.name`` 235 | ``` 236 | 237 | ``` 238 | - Full page name: «logseq/plugins/full-house/tutorial» 239 | - The last word in page name: «tutorial» 240 | - Plugin's name: «full-house» 241 | ``` 242 | 243 | 244 | 245 | ### Accessing another page (or block) from template 246 | - There is a quick way to replace `page` (`block`) context variable: use `:page` (`:block`) named argument to specify another page. See [details](reference__args.md) about different rendering arguments. 247 | 248 | 249 | ``` 250 | - `template:: page overriding` 251 | - authors:: ``c.page.name.split(' — ', 1)[0].split(', ').map(a => `[[${a}]]`).join(', ')`` 252 | title:: ``c.page.name.split(' — ', 2).slice(-1)`` 253 | rating:: ``'⭐️'.repeat(1 + Math.floor(Math.random() * 5))`` 254 | ``` 255 | 256 | ``` 257 | {{renderer :template, page overriding, :page "Author1, Author2 - Some book name"}} 258 | 259 | will be rendered to ↓ 260 | 261 | - authors:: [[Author1]], [[Author2]] 262 | title:: Some book name 263 | rating:: ⭐️⭐️ 264 | ``` 265 | 266 | 267 | 268 | ### Conditional contexts 269 | - As a part of [JavaScript environment](#javascript-environment) — you can write a fully supported JavaSscript code just inside template. Use the [syntax](reference__syntax.md#statement-syntax) to do it. 270 | 271 | ``` 272 | - template:: if logic 273 | - ``{ 274 | if ((c.page.propsRefs.tags || []).includes('book')) { 275 | let [ authors, title ] = c.page.name.split(' — ', 2) 276 | authors = authors.split(', ').map(ref) 277 | }`` 278 | name:: ``{ title }`` 279 | authors:: ``{ authors.join('; ') }`` 280 | ``{ 281 | } else logseq.App.showMsg('The page is not a book', 'info', {timeout: 3000}) 282 | }`` 283 | ``` 284 | 285 | 286 | 287 | ## See also 288 | 289 | > This tutorial is cover only a basic parts of the plugin. See the whole reference for the rest ones: 290 | > - List of [simple](reference__tags.md) and [advanced](reference__tags_advanced.md) template tags 291 | > - [Reusing templates](reference__tags_nesting.md) 292 | > - Lot's of [tools](reference__tags_dev.md) for complex tasks 293 | > - Special [query language for pages](reference__query_language.md) with it's own [table view](reference__query_language__table.md) 294 | > - It is more easy to use, than standard Logseq's queries! 295 | 296 | > Also see the [Showroom](https://github.com/stdword/logseq13-full-house-plugin/discussions/categories/showroom?discussions_q=is%3Aopen+label%3Aoriginal+category%3AShowroom) for advanced examples. The most exciting ones is: 297 | > - [Glass Card view](https://github.com/stdword/logseq13-full-house-plugin/discussions/9) 298 | > - [Live Namespace View](https://github.com/stdword/logseq13-full-house-plugin/discussions/55) 299 | > - [Switching headers colors with a shortcut](https://github.com/stdword/logseq13-full-house-plugin/discussions/49) 300 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdword/logseq13-full-house-plugin/a14e4f2155137fff6211300bf111af5dfe1a9efa/icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | logseq13-full-house 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "preset": "ts-jest", 3 | "testEnvironment": "node", 4 | "globals": { 5 | "process.env.NODE_ENV": "test", 6 | }, 7 | "moduleNameMapper": { 8 | '^@src/(.*)$': "/src/$1", 9 | '^@tests/(.*)$': "/tests/$1", 10 | 11 | "\\.(css)$": "/tests/__mocks__/styleMock.js" 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logseq13-full-house-plugin", 3 | "version": "4.3.6", 4 | "description": "Templates you will really love ❤️", 5 | "keywords": [ 6 | "templates", 7 | "schemas", 8 | "forms", 9 | "fill-data", 10 | "views", 11 | "javascript", 12 | "meta-data", 13 | "meta-info", 14 | "logic" 15 | ], 16 | "author": "stdword", 17 | "repository": "https://github.com/stdword/logseq13-full-house-plugin.git", 18 | "license": "MIT", 19 | "logseq": { 20 | "id": "logseq13-full-house", 21 | "title": "Full House Templates", 22 | "icon": "./icon.png", 23 | "main": "./dist/index.html" 24 | }, 25 | "scripts": { 26 | "preinstall": "npx only-allow pnpm", 27 | "clean": "rm -r ./dist/* || true", 28 | "docs": "docsify serve ./docs", 29 | "dev": "vite", 30 | "test": "jest", 31 | "build": "tsc && vite build --mode=dev", 32 | "prod": "tsc && pnpm run clean && vite build" 33 | }, 34 | "dependencies": { 35 | "@logseq/libs": "^0.0.17", 36 | "dayjs": "^1.11.11", 37 | "eta": "^3.4.0", 38 | "fuzzysort": "^3.0.2", 39 | "mldoc": "^1.5.8", 40 | "neatjson": "^0.10.6", 41 | "preact": "^10.22.1", 42 | "sherlockjs": "^1.4.2" 43 | }, 44 | "devDependencies": { 45 | "@semantic-release/changelog": "^6.0.3", 46 | "@semantic-release/exec": "^6.0.3", 47 | "@semantic-release/git": "^10.0.1", 48 | "@types/jest": "^29.5.12", 49 | "@types/node": "^20.14.9", 50 | "@types/uuid": "^10.0.0", 51 | "conventional-changelog-conventionalcommits": "^8.0.0", 52 | "cz-conventional-changelog": "^3.3.0", 53 | "global-jsdom": "^24.0.0", 54 | "jest": "^29.7.0", 55 | "jsdom": "^24.1.0", 56 | "semantic-release": "^24.0.0", 57 | "ts-jest": "^29.1.5", 58 | "typescript": "^5.5.3", 59 | "uuid": "^10.0.0", 60 | "vite": "^5.3.3", 61 | "vite-plugin-logseq": "^1.1.2" 62 | }, 63 | "config": { 64 | "commitizen": { 65 | "path": "cz-conventional-changelog" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | var PLUGIN_NAME = 'logseq13-full-house-plugin' 2 | 3 | module.exports = { 4 | branches: ['main'], 5 | plugins: [ 6 | ['@semantic-release/commit-analyzer', { 7 | preset: 'conventionalcommits', 8 | }], 9 | '@semantic-release/release-notes-generator', 10 | '@semantic-release/changelog', 11 | ['@semantic-release/npm', { 12 | verifyConditions: false, 13 | npmPublish: false, 14 | }], 15 | '@semantic-release/git', 16 | ['@semantic-release/exec', { 17 | prepareCmd: 18 | `zip -qq -r ${PLUGIN_NAME}-` + "${nextRelease.version}.zip dist icon.png package.json README.md LICENSE", 19 | }], 20 | ['@semantic-release/github', { 21 | assets: `${PLUGIN_NAME}-*.zip`, 22 | fail: false, 23 | failComment: false, 24 | failTitle: false, 25 | }], 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /src/entry.ts: -------------------------------------------------------------------------------- 1 | import '@logseq/libs' 2 | 3 | import { App } from './app' 4 | 5 | 6 | App(logseq) 7 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export class BaseError extends Error { 2 | args: object 3 | 4 | constructor(msg: string, args: object = {}) { 5 | super(msg) 6 | this.args = args 7 | 8 | Object.setPrototypeOf(this, BaseError.prototype); 9 | } 10 | } 11 | 12 | export class StateError extends BaseError { 13 | constructor(msg: string, args: object = {}) { 14 | super(msg, args) 15 | Object.setPrototypeOf(this, StateError.prototype); 16 | } 17 | } 18 | 19 | export class StateMessage extends BaseError { 20 | constructor(msg: string, args: object = {}) { 21 | super(msg, args) 22 | Object.setPrototypeOf(this, StateMessage.prototype); 23 | } 24 | } 25 | 26 | export class RenderError extends BaseError { 27 | constructor(msg: string, args: object = {}) { 28 | super(msg, args) 29 | Object.setPrototypeOf(this, RenderError.prototype); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/extensions/dayjs_logseq_plugin.ts: -------------------------------------------------------------------------------- 1 | export class LogseqDayjsState { 2 | static #format = '' 3 | 4 | /** 5 | * Remap date format pattern 6 | * Ex: yyyy-MM-dd EEE → YYYY-MM-DD ddd 7 | * 8 | * from: https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/text/SimpleDateFormat.html 9 | * to: https://day.js.org/docs/en/parse/string-format 10 | * https://day.js.org/docs/en/display/format#list-of-localized-formats 11 | */ 12 | static set format(f) { 13 | // NOTE: mapping is not full — it supports only formats listed in logseq 14 | // https://github.com/logseq/logseq/blob/master/src/main/frontend/date.cljs#L22 15 | // 16 | // Not supported: 17 | // 'W': Week in month 18 | // 'D': Day in year 19 | 20 | // order is important to prevent intersections 21 | const remapRules = { 22 | // examples for date 2023-02-08 23 | 24 | 'yyyy': 'YYYY', // 2023 25 | 'yy': 'YY', // 23 26 | 27 | 'MMMM': 'MMMM', // February 28 | 'MMM': 'MMM', // Feb 29 | 'MM': 'MM', // 02 30 | 'M': 'M', // 2 31 | 32 | 'dd': 'DD', // 08 33 | 'do': 'Do', // 8th 34 | 'd': 'D', // 8 35 | 36 | 'EEEE': 'dddd', // Wednesday 37 | 'EEE': 'ddd', // Wed 38 | 'EE': 'dd', // We 39 | 'u': 'd', // 3 (Sunday is 0) 40 | 41 | 'ww': 'ww', // 06 42 | 'w': 'w', // 6 43 | } 44 | 45 | for (const [ from, to ] of Object.entries(remapRules)) 46 | f = f.replaceAll(from, to) 47 | 48 | LogseqDayjsState.#format = f 49 | } 50 | static get format() { 51 | return LogseqDayjsState.#format 52 | } 53 | } 54 | 55 | function formatLikeJournalPage(dayjsObj): string { 56 | if (!LogseqDayjsState.format) 57 | return '' 58 | 59 | return dayjsObj.format(LogseqDayjsState.format) 60 | } 61 | 62 | export default (option, dayjsClass, dayjsFactory) => { 63 | // extend dayjs() 64 | dayjsClass.prototype.toPage = function(args) { 65 | return formatLikeJournalPage(this) 66 | } 67 | dayjsClass.prototype.toLogseqInternalFormat = function(args) { 68 | return Number(this.format('YYYYMMDD')) 69 | } 70 | 71 | // extend dayjs 72 | Object.defineProperty( 73 | dayjsFactory, 74 | 'logseqJournalPageFormat', 75 | { get: () => LogseqDayjsState.format }, 76 | ) 77 | 78 | // decorate dayjs().format(...) 79 | const oldFormat = dayjsClass.prototype.format 80 | dayjsClass.prototype.format = function(args) { 81 | if (args === 'page') 82 | return formatLikeJournalPage(this) 83 | 84 | return oldFormat.bind(this)(args) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/extensions/sherlockjs.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'sherlockjs' 2 | -------------------------------------------------------------------------------- /src/ui/insert.css: -------------------------------------------------------------------------------- 1 | /* Colors table for simple Black and White default logseq themees 2 | 3 | --ls-primary-background-color: var(--rx-mauve-01); 4 | --ls-secondary-background-color: var(--rx-mauve-02); 5 | --ls-tertiary-background-color: var(--rx-mauve-03); 6 | --ls-quaternary-background-color: var(--rx-mauve-04); 7 | 8 | --ls-primary-text-color: var(--rx-mauve-12); 9 | --ls-secondary-text-color: var(--rx-mauve-11); 10 | 11 | --ls-link-text-color: var(--rx-red-11); 12 | --ls-link-text-hover-color: var(--rx-red-12); 13 | --ls-block-ref-link-text-color: var(--rx-red-09); 14 | 15 | --ls-border-color: var(--rx-mauve-05); 16 | --ls-secondary-border-color: var(--rx-red-05); 17 | 18 | --ls-page-checkbox-color: var(--rx-mauve-07); 19 | --ls-selection-background-color: var(--rx-mauve-04-alpha); 20 | --ls-block-highlight-color: var(--rx-mauve-04); 21 | --ls-focus-ring-color: var(--rx-red-09); 22 | --ls-table-tr-even-background-color: var(--rx-mauve-04); 23 | --ls-page-properties-background-color: var(--rx-mauve-04); 24 | --ls-block-properties-background-color: var(--rx-mauve-03); 25 | --ls-page-inline-code-bg-color: var(--rx-mauve-03); 26 | --ls-cloze-text-color: var(--rx-red-08); 27 | 28 | --ls-wb-stroke-color-default: var(--rx-red-07); 29 | --ls-wb-background-color-default: var(--rx-red-04); 30 | --ls-wb-text-color-default: var(--rx-mauve-12); 31 | 32 | --ls-a-chosen-bg: var(--rx-mauve-01) 33 | */ 34 | 35 | 36 | * { 37 | box-sizing: border-box; 38 | } 39 | 40 | html ::-webkit-scrollbar-thumb { 41 | /* background-color: ;*/ 42 | background-color: var(--fht-scrollbar-thumb, var(--lx-accent-05, color-mix(in srgb, var(--ls-scrollbar-thumb-hover-color, var(--rx-gray-07)) 50%, transparent))); 43 | } 44 | 45 | html ::-webkit-scrollbar { 46 | background-color: transparent; 47 | height: 8px; 48 | width: 6px; 49 | } 50 | 51 | html ::-webkit-scrollbar-thumb:active { 52 | background-color: var(--fht-scrollbar-thumb-hover, var(--lx-accent-09, var(--ls-scrollbar-thumb-hover-color, var(--rx-gray-07)))); 53 | } 54 | 55 | html ::-webkit-scrollbar-corner { 56 | background: 0 0; 57 | } 58 | 59 | div { 60 | color: var(--ls-primary-text-color, var(--rx-gray-12)); 61 | font-size: var(--ls-page-text-size); 62 | font-family: var(--ls-font-family),sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; 63 | 64 | display: block; 65 | } 66 | 67 | #overlay { 68 | z-index: -1; 69 | 70 | position: fixed; 71 | bottom: 0; 72 | left: 0; 73 | right: 0; 74 | top: 0; 75 | 76 | background-color: var(--ls-quaternary-background-color, var(--rx-gray-04)); 77 | opacity: 0; /* will be set at runtime */ 78 | 79 | transition-duration: .15s; 80 | transition-property: opacity; 81 | transition-timing-function: ease-in; 82 | } 83 | 84 | #modal { 85 | z-index: 999; 86 | 87 | display: flex; 88 | align-items: baseline; 89 | justify-content: center; 90 | 91 | padding-bottom: 1rem; 92 | padding-left: 1rem; 93 | padding-right: 1rem; 94 | 95 | position: fixed; 96 | left: 0; 97 | right: 0; 98 | bottom: 0; 99 | top: 15vh; 100 | } 101 | 102 | #panel { 103 | background: var(--ls-secondary-background-color, var(--rx-gray-02)); 104 | 105 | box-shadow: 0 0 #0000, 0 0 #0000, 0 20px 25px -5px rgba(0,0,0,.1), 0 8px 10px -6px rgba(0,0,0,.1); 106 | border: 1px solid var(--ls-border-color, var(--rx-gray-05)); 107 | border-radius: 8px; 108 | 109 | overflow: hidden; 110 | position: relative; 111 | 112 | transition-timing-function: ease-out; 113 | transition-duration: .15s; 114 | transition-property: all; 115 | 116 | opacity: 0; /* will be set at runtime */ 117 | } 118 | 119 | #panel-content { 120 | min-width: 35em; 121 | overflow-x: hidden; 122 | overflow-y: overlay; 123 | padding: 2rem; 124 | width: auto; 125 | } 126 | 127 | #content { 128 | display: flex; 129 | flex-direction: column; 130 | margin: -2rem; 131 | max-height: 72vh; 132 | overflow: hidden; 133 | } 134 | 135 | #input-wrap { 136 | height: 64px; 137 | } 138 | 139 | #search-query-input { 140 | font-size: var(--ls-page-text-size); 141 | font-family: var(--ls-font-family); 142 | 143 | border: none; 144 | border-radius: 0; 145 | 146 | color: var(--ls-secondary-text-color, var(--rx-gray-11)); 147 | 148 | background: 0 0; 149 | appearance: none; 150 | 151 | line-height: 1.5rem; 152 | 153 | outline: 0; 154 | padding: 16px; 155 | margin: 0; 156 | 157 | width: 100%; 158 | height: 100%; 159 | } 160 | 161 | #search-query-input:focus { 162 | box-shadow: none; 163 | } 164 | 165 | 166 | #results-wrap { 167 | max-height: calc(75vh - 64px); 168 | overflow-x: hidden; 169 | overflow-y: auto; 170 | } 171 | 172 | #items { 173 | -webkit-overflow-scrolling: touch; 174 | overflow-x: hidden; 175 | overflow-y: auto; 176 | position: relative; 177 | } 178 | 179 | .nothing { 180 | padding-bottom: 1rem; 181 | padding-left: 1rem; 182 | padding-right: 1rem; 183 | } 184 | 185 | .item { 186 | word-break: break-all; 187 | 188 | background: 0 0; 189 | border: none; 190 | border-radius: unset!important; 191 | transition: none; 192 | 193 | color: var(--ls-primary-text-color, var(--rx-gray-12)); 194 | /* color: var(--ls-link-text-color);*/ 195 | 196 | -webkit-user-select: none; 197 | -moz-user-select: none; 198 | user-select: none; 199 | 200 | cursor: pointer; 201 | text-decoration: none; 202 | 203 | font-size: .875rem; 204 | line-height: 1.25rem; 205 | 206 | padding-bottom: 0.2rem; 207 | padding-top: 0.3rem; 208 | padding-left: 1rem; 209 | padding-right: 1rem; 210 | 211 | display: flex; 212 | justify-content: space-between; 213 | } 214 | 215 | .item > span { 216 | flex: 1 1 0%; 217 | } 218 | 219 | .selected { 220 | word-break: break-all; 221 | background-color: var(--fht-active, var(--lx-accent-05, var(--ls-quaternary-background-color, var(--rx-gray-04)))); 222 | } 223 | 224 | .selected span { 225 | color: var(--fht-active-text, var(--lx-accent-11, var(--ls-secondary-text-color, var(--rx-gray-11)))); 226 | } 227 | 228 | .item.selected.will-open:hover { 229 | cursor: e-resize; 230 | } 231 | 232 | .cell { 233 | align-items: center; 234 | width: 100%; 235 | display: inline-grid; 236 | grid-template-columns: repeat(4, minmax(0, 1fr)); 237 | } 238 | 239 | .cell-left { 240 | display: flex; 241 | grid-row: 1; 242 | grid-column: 1/span 3; 243 | } 244 | 245 | .cell-under { 246 | display: flex; 247 | grid-row: 2; 248 | grid-column: 1/span 3; 249 | 250 | opacity: .5; 251 | font-size: 12px; 252 | } 253 | 254 | .cell-right { 255 | justify-content: flex-end; 256 | grid-row: 1/span 2; 257 | grid-column: span 1/span 1; 258 | 259 | display: inline-grid; 260 | grid-template-rows: repeat(2, minmax(0, 1fr)); 261 | } 262 | 263 | .label { 264 | grid-row: 1; 265 | grid-column: 1/span 1; 266 | 267 | display: flex; 268 | justify-content: flex-end; 269 | 270 | white-space: nowrap; 271 | opacity: .4; 272 | 273 | border-radius: var(--ls-border-radius-low); 274 | -webkit-border-radius: var(--ls-border-radius-low); 275 | line-height: 1.45; 276 | 277 | color: var(--fht-label-text, var(--lx-accent-11, var(--ls-page-inline-code-color, var(--rx-gray-11)))); 278 | font-family: MonoLisa,Fira Code,Monaco,Menlo,Consolas,COURIER NEW,monospace; 279 | font-size: .9em; 280 | font-style: normal; 281 | letter-spacing: 0; 282 | text-rendering: optimizeSpeed; 283 | } 284 | 285 | .shortcut { 286 | grid-row: 2; 287 | grid-column: 1/span 1; 288 | 289 | display: inline-flex; 290 | margin-left: auto; 291 | 292 | gap: 0.1rem; 293 | opacity: 0.8; 294 | } 295 | 296 | .shortcut.mac > .tile { 297 | letter-spacing: 2px; 298 | } 299 | 300 | .shortcut > .tile { 301 | display: inline-flex; 302 | height: 1.25rem; 303 | 304 | font-family: system-ui; 305 | line-height: 1rem; 306 | font-weight: 400; 307 | font-size: .75rem; 308 | 309 | padding: 0 0.25rem 0 0.375rem; 310 | border-radius: 0.25rem; 311 | 312 | white-space: nowrap; 313 | justify-content: center; 314 | align-items: center; 315 | 316 | color: var(--ls-primary-text-color, var(--rx-gray-12)); 317 | background-color: var(--ls-quaternary-background-color, var(--rx-gray-04)); 318 | box-shadow: inset 0 1px 0 0 hsla(0,0%,100%,.15), inset 0 -1px 0 0 rgba(0,0,0,.15); 319 | } 320 | .item.selected .shortcut { 321 | opacity: 1; 322 | } 323 | .item.selected .shortcut > .tile { 324 | background-color: var(--ls-tertiary-background-color, var(--rx-gray-03)); 325 | box-shadow: inset 0 1px 0 0 hsla(0,0%,100%,.20), inset 0 -1px 0 0 rgba(0,0,0,.25); 326 | } 327 | 328 | mark { 329 | background-color: var(--fht-hightlight, var(--lx-accent-09-alpha, var(--ls-page-mark-bg-color, var(--rx-gray-09)))); 330 | color: var(--ls-page-mark-color, var(--rx-gray-12)); 331 | } 332 | 333 | footer { 334 | display: flex; 335 | align-items: center; 336 | 337 | min-height: 28px; 338 | 339 | margin-left: 12px; 340 | margin-right: 12px; 341 | } 342 | 343 | footer > ul { 344 | padding: 0px; 345 | margin: 0px; 346 | 347 | font-size: 12px; 348 | opacity: .7; 349 | } 350 | 351 | footer > ul > li { 352 | display: inline; 353 | margin-right: 16px; 354 | color: var(--fht-footer-text, var(--lx-accent-09, var(--ls-page-inline-code-color, var(--rx-gray-12)))); 355 | } 356 | 357 | footer > ul > li > svg { 358 | width: 12px; 359 | height: 12px; 360 | 361 | display: inline; 362 | margin-right: 4px; 363 | margin-bottom: -2px; 364 | 365 | fill: var(--fht-footer-text, var(--lx-accent-09, var(--ls-page-inline-code-color, var(--rx-gray-12)))); 366 | } 367 | 368 | footer > ul > li > span { 369 | font-size: 11px; 370 | padding-right: 1px; 371 | } 372 | -------------------------------------------------------------------------------- /src/ui/query-table.ts: -------------------------------------------------------------------------------- 1 | import { ILogseqContext as C, PageContext } from '../context' 2 | import { array_sorted, dev_get, dev_toHTML, ref } from "../tags" 3 | import { escapeForHTML, html } from "../utils" 4 | 5 | 6 | export const iconSortAsc = '' 7 | export const iconSortDesc = '' 8 | 9 | 10 | export function query_table_clickHeader(slot, index, order, saveState) { 11 | const block = top!.document.querySelector(`#${slot}`)!.closest('[blockid]') 12 | if (!block) 13 | return 14 | 15 | // @ts-expect-error 16 | const uuid = block.attributes.blockid.value! 17 | const container = top!.document.querySelector( 18 | `#${slot} .custom-query-results`) as HTMLElement 19 | 20 | const fields = JSON.parse(container.dataset.fields!) 21 | 22 | if (!order) 23 | order = 'asc' 24 | else if (order === 'asc') 25 | order = 'desc' 26 | else if (order === 'desc') 27 | order = '' 28 | 29 | if (saveState) { 30 | if (order) { 31 | // @ts-expect-error 32 | top!.logseq.api.upsert_block_property(uuid, 'query-sort-by', fields[index]) 33 | // @ts-expect-error 34 | top!.logseq.api.upsert_block_property(uuid, 'query-sort-desc', order === 'desc') 35 | } else { 36 | // @ts-expect-error 37 | top!.logseq.api.remove_block_property(uuid, 'query-sort-by') 38 | // @ts-expect-error 39 | top!.logseq.api.remove_block_property(uuid, 'query-sort-desc') 40 | } 41 | 42 | return 43 | } 44 | 45 | 46 | const headerClass = '.fht-qt-header' 47 | const iconClass = '.fht-qt-icon' 48 | 49 | 50 | // remove sort marker from every header 51 | for (const header of top!.document.querySelectorAll(`#${slot} a${headerClass}`)) { 52 | (header as HTMLElement).dataset.order = '' 53 | 54 | const icon = header.querySelector(iconClass)! 55 | icon.innerHTML = '' 56 | } 57 | 58 | 59 | // update sort marker for current header 60 | const currentHeader = top!.document.querySelector(`#${slot} a${headerClass}[data-index="${index}"]`) as HTMLElement 61 | currentHeader.dataset.order = order 62 | 63 | const currentIcon = currentHeader.querySelector(iconClass)! 64 | if (order) 65 | currentIcon.innerHTML = order === 'asc' ? iconSortDesc : iconSortAsc 66 | else 67 | currentIcon.innerHTML = '' 68 | 69 | 70 | // update new items order 71 | let items = JSON.parse(sessionStorage.getItem(`fht-query-items-${slot}`)!) 72 | items.sorted = array_sorted 73 | items = items.map((x, i) => [...x, i]) 74 | 75 | if (order === 'asc') 76 | items = items.sorted((x) => x[index]) 77 | else if (order === 'desc') 78 | items = items.sorted((x) => x[index]).reverse() 79 | 80 | const table = top!.document.querySelector(`#${slot} .custom-query-results tbody`) as HTMLElement 81 | const htmlItems: HTMLElement[] = Array(table.children.length).fill(undefined) 82 | for (const node of table.children) { 83 | const element = node as HTMLElement 84 | htmlItems[Number(element.dataset.index)] = element 85 | } 86 | 87 | const sortedHTMLItems: HTMLElement[] = [] 88 | for (const item of items) 89 | sortedHTMLItems.push(htmlItems[item.at(-1)]) 90 | 91 | table.replaceChildren(...sortedHTMLItems) 92 | } 93 | 94 | export function query_table_no_save_state(context: C, rows: any[], fields?: string[], 95 | opts?: { orderBy?: string, orderDesc?: boolean }, 96 | ) { 97 | const options: any = {} 98 | Object.assign(options, opts) 99 | options.fields = fields 100 | 101 | return query_table(context, rows, false, options) 102 | } 103 | export function query_table_save_state(context: C, rows: any[], fields?: string[]) { 104 | return query_table(context, rows, true, {fields}) 105 | } 106 | function query_table( 107 | context: C, 108 | rows: any[], 109 | saveState: boolean, 110 | opts?: { 111 | fields?: string[], 112 | orderBy?: string, 113 | orderDesc?: boolean, 114 | }, 115 | ) { 116 | let fields = opts?.fields 117 | if (fields && fields.length === 0) 118 | fields = undefined 119 | 120 | if (!rows || rows.length === 0) 121 | return html` 122 |
123 |
No matched result
124 |
125 | ` 126 | 127 | if (!Array.isArray(rows)) { 128 | if (typeof rows !== 'string' && rows[Symbol.iterator]) { 129 | const result: any[] = [] 130 | for (const row of rows as Iterable) 131 | result.push(row) 132 | rows = result 133 | } 134 | else 135 | rows = [rows] 136 | } 137 | 138 | const first = rows[0] 139 | 140 | // auto fill fields names 141 | if (!fields) { 142 | if (Array.isArray(first)) 143 | fields = Array(first.length).fill('column ').map((x, i) => x + (i + 1)) 144 | else if (first instanceof PageContext) { 145 | const propNames = Object.keys(first.props!) 146 | .filter(p => p !== 'title') 147 | fields = ['page', ...propNames] 148 | } 149 | else if (typeof first === 'object' && typeof first['original-name'] === 'string') { 150 | // assume this is PageEntity 151 | const propNames = Object.keys(first['properties-text-values']!) 152 | .filter(p => p !== 'title') 153 | fields = ['page', ...propNames] 154 | } 155 | else 156 | fields = ['column'] 157 | } 158 | fields = fields.filter(f => !!f) 159 | 160 | let orderBy: string | undefined 161 | if (saveState) { 162 | // @ts-expect-error 163 | orderBy = top!.logseq.api.get_block_property(context.currentBlock.uuid, 'query-sort-by') 164 | } 165 | if (!orderBy) 166 | orderBy = opts?.orderBy 167 | 168 | let orderDesc: boolean | undefined 169 | if (saveState) { 170 | // @ts-expect-error 171 | orderDesc = top!.logseq.api.get_block_property(context.currentBlock.uuid, 'query-sort-desc') 172 | } 173 | if (!orderDesc) 174 | orderDesc = opts?.orderDesc 175 | 176 | const orderIndex = orderBy ? fields!.indexOf(orderBy) : -1 177 | const meta = { 178 | fields: fields!, 179 | order: { 180 | by: orderBy ?? null, 181 | index: orderIndex !== -1 ? orderIndex : null, 182 | desc: orderDesc ?? null, 183 | } 184 | } 185 | 186 | const orderByNonField = meta.order.by && meta.order.index === null 187 | 188 | // auto transform page objects 189 | if (!Array.isArray(first)) { 190 | let extendedFields = meta.fields 191 | if (orderByNonField) 192 | extendedFields = extendedFields.concat(meta.order.by!) 193 | 194 | // @ts-expect-error 195 | const listProps = context.config._settings['property/separated-by-commas'] ?? [] 196 | 197 | if (typeof first !== 'object') 198 | rows = rows.map(o => [o]) 199 | else { 200 | if (first instanceof PageContext) { 201 | const propNames = Object.keys(first.props!) 202 | rows = rows.map( 203 | p => extendedFields.map( 204 | f => { 205 | if (f === 'page') 206 | return ref(p.name) 207 | if (propNames.includes(f)) { 208 | if (listProps.includes(f)) { 209 | const value = p.propsRefs[f] 210 | return value 211 | ? value.map(r => ref(r)).join(', ') 212 | : value 213 | } 214 | return p.props[f] 215 | } 216 | return dev_get(context, f, p) 217 | } 218 | ) 219 | ) 220 | } 221 | else if (typeof first['original-name'] === 'string') { 222 | // assume this is PageEntity 223 | const propNames = Object.keys(first['properties-text-values'] ?? {}) 224 | rows = rows.map( 225 | p => extendedFields.map( 226 | f => { 227 | if (f === 'page') 228 | return ref(p['original-name']) 229 | if (propNames.includes(f)) { 230 | if (listProps.includes(f)) { 231 | const value = p['properties'][f] 232 | return value 233 | ? value.map(r => ref(r)).join(', ') 234 | : value 235 | } 236 | return p['properties-text-values'][f] 237 | } 238 | return dev_get(context, f, p) 239 | } 240 | ) 241 | ) 242 | } 243 | else 244 | rows = rows.map( 245 | row => extendedFields.map( 246 | field => dev_get(context, field, row) 247 | ) 248 | ) 249 | 250 | if (orderByNonField) { 251 | // @ts-expect-error 252 | rows.sorted = array_sorted 253 | // @ts-expect-error 254 | rows = rows.sorted(row => row.at(-1)) 255 | rows = rows.map(row => row.slice(0, -1)) 256 | } 257 | } 258 | 259 | if (orderByNonField) 260 | meta.order.by = null 261 | } 262 | 263 | // @ts-expect-error 264 | const slot = context.identity.slot 265 | 266 | function getCSS() { 267 | return html` 268 | #${slot} { width: 100% } 269 | #${slot} > div:first-child { width: 100% } 270 | #${slot} > div:first-child > .fh_template-view { width: 100% } 271 | #${slot} .fht-qt-header { display: contents } 272 | ` 273 | } 274 | function getHeader() { 275 | const htmlRow = meta.fields.map((name, index) => { 276 | return html` 277 | 278 | 289 |
290 | ${name} 291 | ${ 292 | meta.order.by === name 293 | ? (meta.order.desc ? iconSortDesc : iconSortAsc) 294 | : '' 295 | } 296 |
297 |
298 | 299 | ` 300 | }).join('\n') 301 | 302 | return `${htmlRow}` 303 | } 304 | function wrapRow(row) { 305 | const index = row.at(-1) 306 | const htmlRow = row.slice(0, -1).map( 307 | d => `${dev_toHTML(context, (d ?? '').toString())}` 308 | ).join('\n') 309 | 310 | return `${htmlRow}` 311 | } 312 | 313 | const fields_ = escapeForHTML(JSON.stringify(meta.fields)) 314 | if (!saveState) 315 | sessionStorage.setItem(`fht-query-items-${slot}`, JSON.stringify(rows)) 316 | 317 | 318 | // sorting 319 | 320 | // @ts-expect-error 321 | rows.sorted = array_sorted 322 | 323 | let sortedRows = rows.map((x, i) => [...x, i]) 324 | if (meta.order.index !== -1) { 325 | // @ts-expect-error 326 | sortedRows = sortedRows.sorted((x) => x[meta.order.index!]) 327 | if (meta.order.desc) 328 | sortedRows = sortedRows.reverse() 329 | } 330 | 331 | return html` 332 | 333 |
334 |
335 |
336 | ${rows.length} result${rows.length === 1 ? '' : 's'} 337 | 338 |
339 |
340 |
341 |
342 |
343 | 344 | ${getHeader()} 345 | ${sortedRows.map(wrapRow).join('\n')} 346 |
347 |
348 |
349 |
350 |
351 | ` 352 | } 353 | -------------------------------------------------------------------------------- /src/utils/actions.ts: -------------------------------------------------------------------------------- 1 | import '@logseq/libs' 2 | import { BlockEntity, IBatchBlock, ILSPluginUser } from '@logseq/libs/dist/LSPlugin' 3 | 4 | import { filterOutChildBlocks, getBlocksWithReferences, getChosenBlocks, getEditingCursorSelection, IBlockNode, insertBatchBlockBefore, mapBlockTree, PropertiesUtils, setEditingCursorSelection, walkBlockTree, walkBlockTreeAsync } from './logseq' 5 | import { sleep, unique } from './other' 6 | import { objectEquals } from './parsing' 7 | import { blocks_skip } from '../tags' 8 | import { ILogseqContext } from '@src/context' 9 | 10 | 11 | function initAction(context: ILogseqContext, skipTree: boolean) { 12 | if (skipTree) { 13 | // every action means non-standard way of rendering (skipping rendered tree content) 14 | blocks_skip(context, {self: true, children: true}) 15 | } 16 | } 17 | 18 | export async function updateBlocksAction( 19 | context: ILogseqContext, 20 | callback: ( 21 | contentWithoutProperties: string, 22 | properties: Record, 23 | level: number, 24 | block: BlockEntity, 25 | data: Record 26 | ) => Promise<[string, Record] | string | void>, 27 | opts?: { 28 | skipTree?: boolean, 29 | blocks?: BlockEntity[] | string[], 30 | recursive?: boolean, 31 | useMinimalUndoActions?: boolean, 32 | } 33 | ) { 34 | initAction(context, opts?.skipTree ?? true) 35 | 36 | const recursive = opts?.recursive ?? false 37 | const useMinimalUndoActions = opts?.useMinimalUndoActions ?? true 38 | 39 | const blocks_ = opts?.blocks ?? (await getChosenBlocks())[0] 40 | if (blocks_ && blocks_.length === 0) 41 | return 42 | 43 | let blocks: BlockEntity[] 44 | if (typeof blocks_[0] === 'string') 45 | blocks = (await Promise.all( 46 | unique(blocks_ as string[]) 47 | .map(async (uuid) => { 48 | return await logseq.Editor.getBlock(uuid) 49 | }) 50 | )).filter((b) => b !== null) as BlockEntity[] 51 | else 52 | blocks = blocks_ as BlockEntity[] 53 | 54 | if (recursive) { 55 | blocks = await Promise.all( 56 | blocks.map(async (b) => { 57 | return ( 58 | await logseq.Editor.getBlock(b.uuid, {includeChildren: true}) 59 | )! 60 | }) 61 | ) 62 | blocks = filterOutChildBlocks(blocks) 63 | } 64 | 65 | // it is important to check if any block in the tree has references 66 | // (Logseq replaces references with it's text) 67 | if (recursive) 68 | for (const block of blocks) { 69 | const blocksWithReferences = await getBlocksWithReferences(block) 70 | block._treeHasReferences = blocksWithReferences.length !== 0 71 | } 72 | 73 | for (const block of blocks) { 74 | // ensure .children is always an array 75 | if (!block.children) 76 | block.children = [] 77 | 78 | // skip child nodes in non-recursive mode 79 | if (!recursive) 80 | block.children = [] 81 | 82 | const newTree = await mapBlockTree(block as IBlockNode, async (b, level, data) => { 83 | data.node = b as BlockEntity 84 | 85 | let properties = PropertiesUtils.fromCamelCaseAll(b.properties) 86 | const propertiesOrder = PropertiesUtils.getPropertyNames(b.content ?? '') 87 | 88 | let content = PropertiesUtils.deleteAllProperties(b.content ?? '') 89 | const newItem = await callback(content, properties, level, b as BlockEntity, data) 90 | if (newItem === undefined) 91 | return b.content 92 | 93 | let newContent: string 94 | let newProperties: typeof properties 95 | if (Array.isArray(newItem)) 96 | [ newContent, newProperties ] = newItem 97 | else 98 | [ newContent, newProperties ] = [ newItem, {} ] 99 | 100 | if (content === newContent && objectEquals(properties, newProperties)) 101 | data.leftIntact = true 102 | 103 | for (const property of propertiesOrder) 104 | newContent += `\n${property}:: ${newProperties[property]}` 105 | 106 | return newContent 107 | }) 108 | 109 | if (!useMinimalUndoActions || block._treeHasReferences || block.children.length === 0) { 110 | walkBlockTreeAsync(newTree, async (b, level, path) => { 111 | // @ts-expect-error 112 | const block = b.data!.node as BlockEntity 113 | 114 | // @ts-expect-error 115 | if (!b.data!.leftIntact) 116 | if (b.content !== undefined) 117 | await logseq.Editor.updateBlock(block.uuid, b.content) 118 | }) 119 | } else { 120 | await insertBatchBlockBefore(block, newTree as IBatchBlock) 121 | await logseq.Editor.removeBlock(block.uuid) 122 | } 123 | } 124 | } 125 | 126 | // export async function transformBlocksAction( 127 | // callback: (blocks: BlockEntity[]) => BlockEntity[], 128 | // blocks: BlockEntity[], 129 | // isSelectedState: boolean, 130 | // ) { 131 | // // if all blocks relates to one root block 132 | // if (blocks.length === 1) { 133 | // const tree = await ensureChildrenIncluded(blocks[0]) 134 | // if (!tree.children || tree.children.length === 0) 135 | // return // nothing to transform 136 | 137 | // const newRoot = await transformBlocksTreeByReplacing(tree, callback) 138 | // if (newRoot) { // successfully replaced 139 | // if (isSelectedState) 140 | // await logseq.Editor.selectBlock(newRoot.uuid) 141 | // else 142 | // await logseq.Editor.editBlock(newRoot.uuid) 143 | 144 | // return 145 | // } 146 | 147 | // // fallback to array of blocks 148 | // blocks = tree.children as BlockEntity[] 149 | // } 150 | 151 | 152 | // // if all blocks from different parents 153 | // transformSelectedBlocksWithMovements(blocks, callback) 154 | // } 155 | 156 | // async function transformBlocksTreeByReplacing( 157 | // root: BlockEntity, 158 | // transformChildrenCallback: (blocks: BlockEntity[]) => BlockEntity[], 159 | // ): Promise { 160 | // root = await ensureChildrenIncluded(root) 161 | // if (!root || !root.children || root.children.length === 0) 162 | // return null // nothing to replace 163 | 164 | // // METHOD: blocks removal to replace whole tree 165 | // // but it is important to check if any block in the tree has references 166 | // // (Logseq replaces references with it's text) 167 | // const blocksWithReferences = await getBlocksWithReferences(root) 168 | // if (blocksWithReferences.length !== 0) 169 | // return null // blocks removal cannot be used 170 | 171 | // const transformedBlocks = transformChildrenCallback(root.children as BlockEntity[]) 172 | // walkBlockTree({content: '', children: transformedBlocks as IBatchBlock[]}, (b, level) => { 173 | // b.properties = PropertiesUtils.fromCamelCaseAll(b.properties ?? {}) 174 | // }) 175 | 176 | // // root is the first block in page 177 | // if (root.left.id === root.page.id) { 178 | // const page = await logseq.Editor.getPage(root.page.id) 179 | // await logseq.Editor.removeBlock(root.uuid) 180 | 181 | // // logseq bug: cannot use sibling next to root to insert whole tree to a page 182 | // // so insert root of a tree separately from children 183 | // const properties = PropertiesUtils.fromCamelCaseAll(root.properties) 184 | // let prepended = await logseq.Editor.insertBlock( 185 | // page!.uuid, root.content, 186 | // {properties, before: true, customUUID: root.uuid}, 187 | // ) 188 | // if (!prepended) { 189 | // // logseq bug: for empty pages need to change `before: true → false` 190 | // prepended = (await logseq.Editor.insertBlock( 191 | // page!.uuid, root.content, 192 | // {properties, before: false, customUUID: root.uuid}, 193 | // ))! 194 | // } 195 | 196 | // await logseq.Editor.insertBatchBlock( 197 | // prepended.uuid, transformedBlocks as IBatchBlock[], 198 | // {before: false, sibling: false, keepUUID: true}, 199 | // ) 200 | // return prepended 201 | // } 202 | 203 | // // use root to insert whole tree at once 204 | // const oldChildren = root.children 205 | // root.children = transformedBlocks 206 | 207 | // // root is the first child for its parent 208 | // if (root.left.id === root.parent.id) { 209 | // let parentRoot = (await logseq.Editor.getBlock(root.parent.id))! 210 | // await logseq.Editor.removeBlock(root.uuid) 211 | // await logseq.Editor.insertBatchBlock( 212 | // parentRoot.uuid, root as IBatchBlock, 213 | // {before: true, sibling: false, keepUUID: true}, 214 | // ) 215 | 216 | // // restore original object 217 | // root.children = oldChildren 218 | 219 | // parentRoot = (await logseq.Editor.getBlock(parentRoot.uuid, {includeChildren: true}))! 220 | // return parentRoot.children![0] as BlockEntity 221 | // } 222 | 223 | // // root is not first child of parent and is not first block on page: it has sibling 224 | // const preRoot = (await logseq.Editor.getPreviousSiblingBlock(root.uuid))! 225 | // await logseq.Editor.removeBlock(root.uuid) 226 | // await logseq.Editor.insertBatchBlock( 227 | // preRoot.uuid, root as IBatchBlock, 228 | // {before: false, sibling: true, keepUUID: true}, 229 | // ) 230 | 231 | // // restore original object 232 | // root.children = oldChildren 233 | 234 | // return (await logseq.Editor.getNextSiblingBlock(preRoot.uuid))! 235 | // } 236 | 237 | // async function transformSelectedBlocksWithMovements( 238 | // blocks: BlockEntity[], 239 | // transformCallback: (blocks: BlockEntity[]) => BlockEntity[], 240 | // ) { 241 | // // METHOD: blocks movement 242 | 243 | // // Logseq sorts selected blocks, so the first is the most upper one 244 | // let insertionPoint = blocks[0] 245 | 246 | // // Logseq bug: selected blocks can be duplicated (but sorted!) 247 | // // just remove duplication 248 | // blocks = unique(blocks, (b) => b.uuid) 249 | 250 | // const transformed = transformCallback(blocks) 251 | // for (const block of transformed) { 252 | // // Logseq don't add movement to history if there was no movement at all 253 | // // so we don't have to save API calls: just call .moveBlock on EVERY block 254 | // await logseq.Editor.moveBlock(block.uuid, insertionPoint.uuid, {before: false}) 255 | // insertionPoint = block 256 | // } 257 | // } 258 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logseq' 2 | export * from './parsing' 3 | export * from './other' 4 | -------------------------------------------------------------------------------- /src/utils/other.ts: -------------------------------------------------------------------------------- 1 | import { logseq as packageInfo } from '../../package.json' 2 | import { splitMacroArgs } from './logseq' 3 | 4 | 5 | export let locks: LockManager | null = null 6 | try { 7 | locks = navigator.locks 8 | } 9 | catch (error) { 10 | if (process.env.NODE_ENV !== 'test') 11 | console.error('Cannot use Web Locks API') 12 | } 13 | 14 | 15 | /** 16 | * Tagged template printing function 17 | * @usage console.log(p`Hello, Logseq!`) 18 | * @usage console.debug(p``, {var}) 19 | **/ 20 | export function p(strings: any, ...values: any[]): string { 21 | const raw = String.raw({raw: strings}, ...values) 22 | const space = raw ? ' ' : '' 23 | return `#${packageInfo.id}:${space}${raw}` 24 | } 25 | 26 | /** 27 | * Format-string 28 | * @usage f`Hello, ${'name'}!`({name: 'Logseq'}) 29 | * @usage 30 | * const format = f`Hello, ${'name'}!` 31 | * format({name: 'Logseq'}) // => 'Hello, Logseq!' 32 | **/ 33 | export function f(strings: any, ...values: any[]): Function { 34 | return (format: {[i: string]: any}) => String.raw({raw: strings}, ...values.map(v => format[v])) 35 | } 36 | 37 | /** 38 | * Clear spaces from HTML-string 39 | * @usage 40 | * html` 41 | *
42 | *

Text

43 | *
44 | * ` // => '

Text

' 45 | **/ 46 | export function html(strings: any, ...values: any[]): string { 47 | const raw = String.raw({raw: strings}, ...values) 48 | return raw.trim().replaceAll(/^ +/gm, '').replaceAll(/>\n<') 49 | } 50 | 51 | /** 52 | * Clear spaces from multi-line string 53 | * @usage 54 | * unspace` 55 | * text 56 | * - line 1 57 | * - line 2 58 | * ` 59 | * => 60 | * `text 61 | * - line 1 62 | * - line 2` 63 | **/ 64 | export function unspace(strings: any, ...values: any[]): string { 65 | const raw = String.raw({raw: strings}, ...values) 66 | return raw.trim().replaceAll(/^\s+/gm, '') 67 | } 68 | 69 | /** 70 | * Remove duplicates 71 | */ 72 | export function unique(items: Array, keyFunction?: (item: X) => any) { 73 | if (!keyFunction) 74 | keyFunction = x => x 75 | 76 | return items.filter((b, i, r) => { 77 | if (i === 0) 78 | return true 79 | return keyFunction!(r[i - 1]) !== keyFunction!(b) 80 | }) 81 | } 82 | 83 | /** 84 | * Count substrings in string 85 | */ 86 | export function countOf(string: string, substring: string): number { 87 | if (substring.length === 0) 88 | return 0 89 | 90 | const matchedCount = string.length - string.replaceAll(substring, '').length 91 | return matchedCount / substring.length 92 | } 93 | 94 | /** 95 | * Find index of Nth substring in string 96 | */ 97 | export function indexOfNth(string: string, substring: string, count: number = 1): number | null { 98 | if (count <= 0) 99 | throw new Error('count param should be positive') 100 | 101 | const realCount = countOf(string, substring) 102 | if (count > realCount) 103 | return null 104 | 105 | return string.split(substring, count).join(substring).length 106 | } 107 | 108 | export function lockOn(idFunc: ((args: any) => string)) { 109 | return (func: Function) => { 110 | if (!locks) 111 | return func 112 | 113 | return async (...args: any) => { 114 | const key = idFunc(args) 115 | await locks!.request(key, {ifAvailable: true}, async (lock) => { 116 | if (!lock) { 117 | console.warn(p`Excess call of "${func.name}" with`, {func, args}) 118 | return 119 | } 120 | await func(...args) 121 | }) 122 | } 123 | } 124 | } 125 | 126 | export function sleep(ms: number) { 127 | return new Promise(resolve => setTimeout(resolve, ms)) 128 | } 129 | 130 | export function escape(str: string, specials: string[]) { 131 | const replacer = new RegExp('(\\' + specials.join('|\\') + ')', 'g') 132 | return str.replaceAll(replacer, '\\$1') 133 | } 134 | 135 | export function escapeForRegExp(str: string) { 136 | const specials = [ 137 | // TODO: «-» need to be escaped inside [ ] 138 | // '^', '$', 139 | '/', '.', '*', '+', '?', '|', 140 | '(', ')', '[', ']', '{', '}', '\\', 141 | ] 142 | 143 | return escape(str, specials) 144 | 145 | // alternative from MDN 146 | // return str.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&') 147 | // $& means the whole matched string 148 | } 149 | 150 | export function escapeForHTML(unsafe: string) { 151 | // source: https://stackoverflow.com/a/18750001 152 | // return unsafe.replace(/[\u00A0-\u9999<>\&]/g, i => '&#' + i.charCodeAt(0) + ';') 153 | 154 | return unsafe 155 | .replaceAll('&', '&') 156 | .replaceAll('<', '<') 157 | .replaceAll('>', '>') 158 | .replaceAll('"', """) 159 | .replaceAll("'", ''') 160 | } 161 | 162 | export function unescapeHTML(safe: string) { 163 | return safe 164 | .replaceAll('&', '&') 165 | .replaceAll('<', '<') 166 | .replaceAll('>', '>') 167 | .replaceAll(""", '"') 168 | .replaceAll(''', "'") 169 | } 170 | 171 | export function escapeForHiccup(unsafe: string) { 172 | return unsafe.replaceAll('"', "'") 173 | } 174 | 175 | export function toISODate(date: Date) { 176 | const m = `${date.getMonth() + 1}` 177 | const d = date.getDate().toString() 178 | return `${date.getFullYear()}-${m.padStart(2, '0')}-${d.padStart(2, '0')}` 179 | } 180 | 181 | export function getCSSVars(names) { 182 | const style = getComputedStyle(top!.document.body) 183 | return names.map((name) => style.getPropertyValue(name)) 184 | } 185 | 186 | export function loadThemeVars(vars) { 187 | const vals = getCSSVars(vars) 188 | if (!vals) 189 | return 190 | 191 | const style = document.body.style 192 | vars.forEach((k, i) => { 193 | style.setProperty(k, vals[i]) 194 | }) 195 | } 196 | 197 | export function rgbToHex(r, g, b) { 198 | return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('') 199 | } 200 | 201 | /** 202 | * source: underscore.random 203 | * url: https://github.com/jashkenas/underscore/blob/ffabcd443fd784e4bc743fff1d25456f7282d531/underscore.js#L1393 204 | */ 205 | export function randomRange(min: number, max?: number) { 206 | if (max === undefined) { 207 | max = min 208 | min = 0 209 | } 210 | return min + Math.floor(Math.random() * (max - min + 1)) 211 | } 212 | 213 | export function functionSignature(f: Function) { 214 | const doc = f.toString() 215 | const m = doc.match(/function\s*(\w*?)\((.*?)\)\s*\{?/) 216 | if (!m) 217 | return null 218 | 219 | const isAsync = doc.startsWith('async ') 220 | const name = m[1] 221 | const args = m[2] ? splitMacroArgs(m[2], {clean: false}) : [] 222 | 223 | return { 224 | isAsync, 225 | name, 226 | args, 227 | whole: `${isAsync ? 'async' : ''} function ${name}(${args.join(', ')})`.trimStart(), 228 | whole_: `${isAsync ? 'async' : ''} function(${args.join(', ')})`.trimStart(), 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/utils/parsing.ts: -------------------------------------------------------------------------------- 1 | import { p } from './other' 2 | 3 | 4 | // Subset of Unicode «Punctuation, Dash» category 5 | const dashesValues = [ 6 | '\u002D', // HYPHEN-MINUS 7 | '\u058A', // ARMENIAN HYPHEN 8 | '\u1806', // MONGOLIAN TODO SOFT HYPHEN 9 | '\u2010', // HYPHEN 10 | '\u2011', // NON-BREAKING HYPHEN 11 | '\u2012', // FIGURE DASH 12 | '\u2013', // EN DASH 13 | '\u2014', // EM DASH 14 | '\u2015', // HORIZONTAL BAR 15 | '\u2E3A', // TWO-EM DASH 16 | '\u2E3B', // THREE-EM DASH 17 | '\uFE58', // SMALL EM DASH 18 | '\uFE63', // SMALL HYPHEN-MINUS 19 | '\uFF0D', // FULLWIDTH HYPHEN-MINUS 20 | ] 21 | 22 | // Subset of Unicode «Punctuation» category 23 | const quotesValues = [ 24 | '""', 25 | "''", 26 | '``', 27 | '«»', 28 | 29 | '\u2018\u2019', // LEFT & RIGHT SINGLE QUOTATION MARK 30 | '\u201C\u201D', // LEFT & RIGHT DOUBLE QUOTATION MARK 31 | '\u276E\u276F', // HEAVY LEFT- & RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT 32 | 33 | '\uFF02'.repeat(2), // FULLWIDTH QUOTATION MARK 34 | '\uFF07'.repeat(2), // FULLWIDTH APOSTROPHE 35 | '\u201B'.repeat(2), // SINGLE HIGH-REVERSED-9 QUOTATION MARK 36 | '\u201F'.repeat(2), // DOUBLE HIGH-REVERSED-9 QUOTATION MARK 37 | '\u201A'.repeat(2), // SINGLE LOW-9 QUOTATION MARK 38 | '\u201E'.repeat(2), // DOUBLE LOW-9 QUOTATION MARK 39 | '\u2E42'.repeat(2), // DOUBLE LOW-REVERSED-9 QUOTATION MARK 40 | '\u301D'.repeat(2), // REVERSED DOUBLE PRIME QUOTATION MARK 41 | '\u301F'.repeat(2), // LOW DOUBLE PRIME QUOTATION MARK 42 | '\u301E'.repeat(2), // DOUBLE PRIME QUOTATION MARK 43 | ] 44 | 45 | export function isUUID(str: string) { 46 | const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ 47 | return !!str.match(regex) 48 | } 49 | 50 | export function objectEquals(x: any, y: any) { 51 | // source: https://stackoverflow.com/a/16788517 52 | 53 | if (x === null || x === undefined || y === null || y === undefined) 54 | return x === y 55 | 56 | // after this just checking type of one would be enough 57 | if (x.constructor !== y.constructor) 58 | return false 59 | 60 | // if they are functions, they should exactly refer to same one (because of closures) 61 | if (x instanceof Function) 62 | return x === y 63 | 64 | // if they are regexps, they should exactly refer to same one 65 | // (it is hard to better equality check on current ES) 66 | if (x instanceof RegExp) 67 | return x === y 68 | 69 | if (x === y || x.valueOf() === y.valueOf()) 70 | return true 71 | 72 | if (Array.isArray(x) && x.length !== y.length) 73 | return false 74 | 75 | // if they are dates, they must had equal valueOf 76 | if (x instanceof Date) 77 | return false 78 | 79 | // if they are strictly equal, they both need to be object at least 80 | if (!(x instanceof Object)) 81 | return false 82 | if (!(y instanceof Object)) 83 | return false 84 | 85 | // recursive object equality check 86 | var p = Object.keys(x) 87 | return Object.keys(y).every(k => p.indexOf(k) !== -1) 88 | && p.every(k => objectEquals(x[k], y[k])) 89 | } 90 | 91 | export function isObject(item: any): boolean { 92 | return (item && typeof item === 'object' && !Array.isArray(item)) 93 | } 94 | 95 | export function isBoolean(obj: any): boolean { 96 | // source: https://stackoverflow.com/a/28814865 97 | if (typeof obj === typeof true) 98 | return true 99 | 100 | if (typeof obj === 'object' && obj !== null && typeof obj.valueOf() === typeof true) 101 | return true 102 | 103 | return false 104 | } 105 | 106 | export function isInteger(str: string): boolean { 107 | const x = Number(str) 108 | if (Number.isNaN(x)) 109 | return false 110 | 111 | // detect float 112 | if (str.includes('.') || str.includes('e-')) 113 | return false 114 | 115 | return true 116 | } 117 | 118 | export function isEmpty(obj: any): boolean { 119 | if (!obj) { 120 | return true 121 | } 122 | 123 | if ( 124 | obj.constructor !== Object && 125 | obj.constructor !== Array && 126 | obj.constructor !== String 127 | ) { 128 | console.debug(p``, typeof obj, {obj}) 129 | throw new Error('Cannot check user-class object for emptyness') 130 | } 131 | 132 | return Object.keys(obj).length === 0 133 | } 134 | 135 | export function isEmptyString(obj: string): boolean { 136 | const emptyValues = [''].concat(quotesValues).concat(dashesValues) 137 | return emptyValues.includes(obj.trim()) 138 | } 139 | 140 | export function coerceStringToBool(str: string): boolean | null { 141 | const trueValues = [ 142 | '✅', '✔️', '☑️', '🔘', 143 | '+', '1', 144 | 'v', 'y', 't', 145 | 'on', 'yes', 'true', 146 | 'ok', 'yep', 'yeah', 147 | 'checked', 'enabled', 148 | 'turned on', 'turn on', 149 | ] 150 | const falseValues = [ 151 | '🚫', '❌', '✖️', '⛔️', 152 | '-', '0', 153 | 'x', 'n', 'f', 154 | 'off', 'no', 'false', 155 | 'non', 'nope', 156 | 'none', 'null', 'nil', 157 | 'unchecked', 'empty', 'disabled', 158 | 'turned off', 'turn off', 'dont', "don't", 159 | ] 160 | 161 | str = str.toString().toLowerCase().trim() 162 | if (trueValues.includes(str)) 163 | return true 164 | if (falseValues.includes(str)) 165 | return false 166 | 167 | return null 168 | } 169 | 170 | /* Coerce to bool: 171 | * - any bool object 172 | * - non-empty string with value coercible to bool 173 | * - empty value (including [] and {}) 174 | */ 175 | export function coerceToBool( 176 | obj: any, 177 | opts?: { 178 | defaultForUncoercible?: boolean, 179 | defaultForEmpty?: boolean, 180 | }): boolean | null { 181 | if (isBoolean(obj)) 182 | return !!obj 183 | 184 | if (isEmpty(obj)) 185 | return opts?.defaultForEmpty ?? null 186 | 187 | const bool = coerceStringToBool(obj) 188 | if (bool !== null) 189 | return bool 190 | 191 | return opts?.defaultForUncoercible ?? null 192 | } 193 | 194 | export function unquote( 195 | ref: string, 196 | qoutes: string | string[] = quotesValues, 197 | once = true, 198 | ): string { 199 | if (isEmptyString(ref)) 200 | return '' 201 | 202 | if (!Array.isArray(qoutes)) 203 | qoutes = [qoutes] 204 | 205 | for (const [open, close] of qoutes) 206 | if (ref.startsWith(open) && ref.endsWith(close)) { 207 | ref = ref.slice(1, -1) 208 | if (!once) 209 | ref = unquote(ref, qoutes) 210 | } 211 | 212 | return ref 213 | } 214 | -------------------------------------------------------------------------------- /tests/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | import 'global-jsdom/register' 2 | 3 | import { v4 as genUUID } from 'uuid' 4 | 5 | 6 | import { _private as app } from '@src/app' 7 | import { BlockEntity, IEntityID, PageEntity } from '@logseq/libs/dist/LSPlugin' 8 | import { IBlockNode } from '@src/utils' 9 | 10 | import { logseq as packageInfo, version as pluginVersion } from '@src/../package.json' 11 | 12 | 13 | export async function LogseqMock( 14 | settingsOverride: object | null = {}, 15 | configOverride: object = {}, 16 | ) { 17 | settingsOverride ??= {} 18 | configOverride ??= {} 19 | 20 | const logseq = { 21 | _pages: [] as PageEntity[], 22 | _blocks: [] as BlockEntity[], 23 | 24 | baseInfo: { 25 | id: packageInfo.id, 26 | version: pluginVersion, 27 | }, 28 | 29 | settings: {}, 30 | updateSettings: jest.fn(), 31 | on: jest.fn(), 32 | 33 | App: { 34 | getInfo: jest.fn(), 35 | async getCurrentGraphConfigs() { return {} }, 36 | async getCurrentGraph() { return {} }, 37 | async getUserConfigs() { 38 | const defaultConfig = { 39 | preferredDateFormat: 'dd-MM-yyyy', 40 | preferredStartOfWeek: 0, 41 | } 42 | return Object.assign(defaultConfig, configOverride) 43 | } 44 | }, 45 | DB: { 46 | datascriptQuery: jest.fn(), 47 | }, 48 | Editor: { 49 | async getPage(id: number | string, options?: object): Promise { 50 | if (typeof id === 'string') 51 | return logseq._pages.find(page => (page.name === id)) ?? null 52 | return logseq._pages.find(page => (page.id === id)) ?? null 53 | }, 54 | async getBlock(id: number | string, options?: object): Promise { 55 | return logseq._getBlock(id, options) 56 | }, 57 | async insertAtEditingCursor(content: string) { 58 | logseq._createBlock({content, children: []}) 59 | }, 60 | async insertBatchBlock( 61 | uuid: string, 62 | children: IBlockNode[], 63 | opts?: Partial<{ 64 | sibling: boolean 65 | }>) { 66 | opts ||= {} 67 | 68 | const block = await this.getBlock(uuid) 69 | if (!block) 70 | throw Error(`Block doesn't exist: ${uuid}`) 71 | 72 | if (opts.sibling) 73 | for (const child of children) 74 | logseq._createBlock(child, block.parent.id, block.page.id) 75 | else 76 | for (const child of children) 77 | logseq._createBlock(child, block, block.page.id) 78 | }, 79 | async updateBlock(uuid, newContent) { 80 | const block = await this.getBlock(uuid) 81 | if (block) 82 | block.content = newContent 83 | }, 84 | async checkEditing() { return false }, 85 | }, 86 | UI: { 87 | showMsg: jest.fn(), 88 | }, 89 | 90 | api: { 91 | get_block: jest.fn(), 92 | get_page: jest.fn(), 93 | get_app_info: () => { return {} }, 94 | datascript_query: jest.fn(), 95 | }, 96 | 97 | _createPage: function (name: string): PageEntity { 98 | const obj: PageEntity = { 99 | format: 'markdown', 100 | 101 | id: this._pages.length + 1, 102 | uuid: genUUID(), 103 | 104 | name: name, 105 | originalName: name, 106 | 107 | 'journal?': false, 108 | journalDay: undefined, 109 | } 110 | 111 | logseq._pages.push(obj) 112 | return obj 113 | }, 114 | _createJournalPage: function (isoDay: string): PageEntity { 115 | const obj: PageEntity = { 116 | format: 'markdown', 117 | 118 | id: this._pages.length + 1, 119 | uuid: genUUID(), 120 | 121 | name: isoDay, 122 | originalName: isoDay, 123 | 124 | 'journal?': true, 125 | journalDay: Number(isoDay.replaceAll('-', '')), 126 | } 127 | 128 | logseq._pages.push(obj) 129 | return obj 130 | }, 131 | _createBlock: function ( 132 | block: IBlockNode | string, 133 | parent: BlockEntity | number | null = null, 134 | page: PageEntity | number | null = null, 135 | props: object = {}, 136 | ): BlockEntity { 137 | const content = typeof block === 'string' ? block : block.content 138 | const children = typeof block === 'string' ? [] : block.children 139 | const pageID = (typeof page === 'number') ? page : (page ? page.id : logseq._pages[0].id) 140 | const parentID = (typeof parent === 'number') ? parent : (parent ? parent.id : null) 141 | 142 | const obj: BlockEntity = { 143 | format: 'markdown', 144 | 145 | id: this._blocks.length + 1, 146 | uuid: genUUID(), 147 | content: content ?? '', 148 | properties: props, 149 | propertiesTextValues: props, 150 | 151 | // @ts-expect-error 152 | left: {}, 153 | // @ts-expect-error 154 | parent: parentID ? {id: parentID} : {}, 155 | page: {id: pageID}, 156 | } 157 | logseq._blocks.push(obj) 158 | 159 | obj.children = children.map((b) => this._createBlock(b, obj)) 160 | 161 | if (parentID) { 162 | const parentBlock = this._getBlock(parentID)! 163 | if (!parentBlock.children) 164 | parentBlock.children = [] 165 | parentBlock.children.push(obj) 166 | } 167 | 168 | return obj 169 | }, 170 | _createTemplateBlock: function (name: string, content: string, props: object) { 171 | const propsContent = props 172 | ? '\n' + Object.entries(props).map(([p, v]) => `${p}:: ${v}`).join('\n') 173 | : '' 174 | const obj = logseq._createBlock({ 175 | content: `template:: ${name}` + propsContent, 176 | children: [{content: content, children: []}], 177 | }, null, null, props) 178 | obj.properties!.template = name 179 | return obj 180 | }, 181 | _getBlock: function (id: number | string, options?: object): BlockEntity | null { 182 | if (typeof id === 'string') 183 | return logseq._blocks.find(block => (block.uuid === id)) ?? null 184 | return logseq._blocks.find(block => (block.id === id)) ?? null 185 | }, 186 | } 187 | 188 | Object.assign(logseq.settings, settingsOverride ?? {}) 189 | 190 | // @ts-expect-error 191 | global.logseq = top.logseq = logseq 192 | await app.postInit() 193 | 194 | logseq._createPage('PAGE') 195 | return logseq 196 | } 197 | -------------------------------------------------------------------------------- /tests/logic/command_template.test.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid' 2 | 3 | import { LogseqMock } from '@tests/index' 4 | 5 | import * as dayjs from 'dayjs' 6 | 7 | import { _private as tags } from '@src/tags' 8 | import { _private as app } from '@src/app' 9 | import { IBlockNode, LogseqReferenceAccessType, RendererMacro, parseReference } from '@src/utils' 10 | import { compileTemplateView, renderTemplateInBlock } from '@src/logic' 11 | import { PageEntity } from '@logseq/libs/dist/LSPlugin' 12 | import { Template } from '@src/template' 13 | import { BlockContext, ILogseqCurrentContext, PageContext } from '@src/context' 14 | 15 | 16 | let logseq: any 17 | beforeEach(async () => { 18 | logseq = await LogseqMock(null, {preferredDateFormat: 'YYYY-MM-DD'}) 19 | }) 20 | 21 | async function testRender( 22 | syntax: string, 23 | expected?: string, 24 | page?: PageEntity, 25 | createTemplateFunc?: Function, 26 | executeRendering?: Function, 27 | ) { 28 | const name = 'test_name' 29 | const command = RendererMacro.command('template').arg(name) 30 | const block = logseq._createBlock(command.toString(), null, page) 31 | 32 | if (!createTemplateFunc) 33 | createTemplateFunc = logseq._createTemplateBlock 34 | const templateBlock = createTemplateFunc!(name, syntax) 35 | 36 | let mockedValues = templateBlock 37 | if (!Array.isArray(mockedValues)) 38 | mockedValues = [mockedValues] 39 | 40 | for (const mockedValue of mockedValues) 41 | logseq.DB.datascriptQuery.mockReturnValueOnce([mockedValue]) 42 | if (!executeRendering) 43 | executeRendering = async () => { 44 | await renderTemplateInBlock('slot__test', block.uuid, parseReference(name), command, []) 45 | } 46 | await executeRendering() 47 | 48 | expect(logseq.DB.datascriptQuery).toHaveBeenCalledTimes(mockedValues.length) 49 | if (expected !== undefined) 50 | expect(block.content).toBe(expected) 51 | 52 | return block 53 | } 54 | 55 | describe('template syntax', () => { 56 | test('no syntax', async () => { 57 | await testRender('text', 'text') }) 58 | test('js expression', async () => { 59 | await testRender('``1 + 2``', '3') }) 60 | test('js statement', async () => { 61 | await testRender('``{ console._checkMe = 13 }``', '') 62 | // @ts-expect-error 63 | expect(console._checkMe).toBe(13) 64 | }) 65 | 66 | test('back ticks inside expression', async () => { 67 | await testRender('`` `1` + `2` ``', '12') }) 68 | 69 | test('save spaces to the right', async () => { 70 | await testRender('``{ var x = 1 }`` text', ' text') }) 71 | test('save spaces to the left', async () => { 72 | await testRender('text ``{ var x = 1 }``', 'text ') }) 73 | test('strip one new line to the right', async () => { 74 | await testRender('``{ var x = 1 }``\n\ntext', '\ntext') }) 75 | test('strip no one new lines to the left', async () => { 76 | await testRender('text\n\n``{ var x = 1 }``', 'text\n\n') }) 77 | 78 | test('ref shortcut', async () => { 79 | await testRender('``["page"]``', '[[page]]') }) 80 | }) 81 | 82 | describe('backwards compatibility', () => { 83 | test('new syntax: no exclamation mark', async () => { 84 | await testRender('``{ var x = c.page.name }`` ``x``', ' PAGE') }) 85 | 86 | test('don\'t mix new & old syntax', async () => { 87 | await testRender('``{ var x = c.page.name }`` ``{ x }``', ' ') }) 88 | 89 | test('statement signs: var', async () => { 90 | await testRender('``{ var x = c.page.name }``', '') }) 91 | test('statement signs: =', async () => { 92 | await testRender('``{ c.page.name = "new" }``', '') }) 93 | test('statement signs: absent', async () => { 94 | await testRender('``{ c.page.name }``', '') }) 95 | }) 96 | 97 | describe('template context', () => { 98 | test('page name', async () => { 99 | await testRender('``c.page.name``', 'PAGE') }) 100 | test('template name', async () => { 101 | await testRender('``c.template.name``', 'test_name') }) 102 | test('identity', async () => { 103 | await testRender('``c.identity.slot``, ``c.identity.key``', 'slot__test, test') }) 104 | test('full context', async () => { 105 | const block = await testRender('``c``') 106 | expect(block.content.slice(0, 6)).toBe('
{')
107 |         expect(block.content.slice(-7)).toBe('}
') 108 | }) 109 | }) 110 | 111 | describe('standard template syntax', () => { 112 | test('unknown dynamic variable', async () => { 113 | await testRender('<% UNknown %>', 'UNknown') }) 114 | 115 | test('time', async () => { 116 | const time = dayjs().format('HH:mm') 117 | await testRender('<% time %>', time) }) 118 | 119 | test('today page', async () => { 120 | const today = dayjs().format('page') 121 | await testRender('<% today %>', '[[' + today + ']]') }) 122 | test('today page with different spaces and case', async () => { 123 | const today = dayjs().format('page') 124 | await testRender('<%toDAY %>', '[[' + today + ']]') }) 125 | 126 | test('yesterday page', async () => { 127 | const yesterday = dayjs().subtract(1, 'day').format('page') 128 | await testRender('<% yesterday %>', '[[' + yesterday + ']]') }) 129 | 130 | test('tomorrow page', async () => { 131 | const tomorrow = dayjs().add(1, 'day').format('page') 132 | await testRender('<% tomorrow %>', '[[' + tomorrow + ']]') }) 133 | 134 | test('current page', async () => { 135 | await testRender('<% current page %>', '[[PAGE]]') }) 136 | 137 | test('NLP relative week day', async () => { 138 | const today = dayjs() 139 | let day: dayjs.Dayjs | string = today.day(5) 140 | if (day >= today) 141 | day = day.subtract(1, 'week') 142 | day = day.format('page') 143 | await testRender('<% last friday %>', '[[' + day + ']]') }) 144 | test('NLP relative days count', async () => { 145 | const day = dayjs().subtract(5, 'day').format('page') 146 | await testRender('<% 5 days ago %>', '[[' + day + ']]') }) 147 | test('NLP relative weeks count', async () => { 148 | const day = dayjs().add(2, 'week').format('page') 149 | await testRender('<% 2 weeks from now %>', '[[' + day + ']]') }) 150 | 151 | test('NLP exact 1', async () => { 152 | await testRender('<% 17 August 2013 %>', '[[2013-08-17]]') }) 153 | test('NLP exact 2', async () => { 154 | await testRender('<% Sat Aug 17 2013 %>', '[[2013-08-17]]') }) 155 | test('NLP exact 3', async () => { 156 | await testRender('<% 2013-08-17 %>', '[[2013-08-17]]') }) 157 | }) 158 | 159 | describe('template tags', () => { 160 | test('date.nlp relative to now', async () => { 161 | const day = dayjs().add(2, 'day').format('page') 162 | await testRender('``date.nlp("in two days")``', day) }) 163 | 164 | test('date.nlp relative to tomorrow via string', async () => { 165 | await testRender('``date.nlp("in two days", "2020-10-01")``', '2020-10-03') }) 166 | test('date.nlp relative to tomorrow via obj', async () => { 167 | const day = dayjs().add(1, 'day').add(2, 'day').format('page') 168 | await testRender('``date.nlp("in two days", date.tomorrow)``', day) }) 169 | 170 | test('date.nlp relative to now in journal page', async () => { 171 | const page = logseq._createJournalPage('2020-10-01') 172 | await testRender('``date.nlp("in two days", "page")``', '2020-10-03', page) }) 173 | 174 | test('special syntax for date.nlp', async () => { 175 | const page = logseq._createJournalPage('2020-10-01') 176 | await testRender('``@in two days, page``', '[[2020-10-03]]', page) }) 177 | }) 178 | 179 | describe('template structure', () => { 180 | test('rendering children of first child block', async () => { 181 | const block = await testRender('parent', 'parent', undefined, (name, syntax) => { 182 | const block = logseq._createTemplateBlock(name, syntax) 183 | logseq._createBlock('child text 1', block.children[0]) 184 | logseq._createBlock('child text 2', block.children[0]) 185 | return block 186 | }) 187 | expect(block.children).toHaveLength(2) 188 | expect(block.children[0].content).toBe('child text 1') 189 | expect(block.children[1].content).toBe('child text 2') 190 | }) 191 | test('rendering children of second child block', async () => { 192 | const block = await testRender('parent 1', 'parent 1', undefined, (name, syntax) => { 193 | const block = logseq._createTemplateBlock(name, syntax) 194 | logseq._createBlock('parent 2', block) 195 | logseq._createBlock('child text 1', block.children[1]) 196 | logseq._createBlock('child text 2', block.children[1]) 197 | return block 198 | }) 199 | expect(block.children).toHaveLength(0) 200 | 201 | const secondBlock = logseq._blocks.find(block => (block.content === 'parent 2')) ?? null 202 | expect(secondBlock).not.toBeNull() 203 | 204 | expect(secondBlock.children[0].content).toBe('child text 1') 205 | expect(secondBlock.children[1].content).toBe('child text 2') 206 | }) 207 | test('template inclusion', async () => { 208 | await testRender('``await include("base")``', 'base:PAGE', undefined, (name, syntax) => { 209 | const blockBase = logseq._createTemplateBlock('base', 'base:``c.page.name``') 210 | const blockChild = logseq._createTemplateBlock(name, syntax) 211 | return [blockChild, blockBase] 212 | }) 213 | }) 214 | test('view inclusion', async () => { 215 | const name = 'test_view' 216 | const command = RendererMacro.command('template-view').arg(name) 217 | const block = logseq._createBlock(command.toString()) 218 | 219 | const templateBlock = logseq._createTemplateBlock( 220 | name, 'child:``await include("base")``', {argProp: 'OVERRIDED', 'arg-prop': 'OVERRIDED'} 221 | ) 222 | const templateBlockBase = logseq._createTemplateBlock( 223 | 'base', 'base:``c.args.prop``', {argProp: 'BASE VALUE', 'arg-prop': 'BASE VALUE'}) 224 | 225 | const [includingParent, accessedVia] = [false, 'name' as LogseqReferenceAccessType] 226 | const template = new Template(templateBlock, {name, includingParent, accessedVia}) 227 | await template.init() 228 | 229 | logseq.DB.datascriptQuery.mockReturnValueOnce([templateBlockBase]) 230 | 231 | const view = await compileTemplateView( 232 | 'slot__test', 233 | template, 234 | [], 235 | { 236 | mode: 'view', 237 | currentPage: PageContext.createFromEntity(await logseq.Editor.getPage(block.page.id)), 238 | currentBlock: BlockContext.createFromEntity(block), 239 | } as ILogseqCurrentContext, 240 | ) 241 | 242 | expect(logseq.DB.datascriptQuery).toHaveBeenCalledTimes(1) 243 | 244 | const expectedBase = 'base:BASE VALUE' 245 | const expectedChild = `child:${expectedBase}` 246 | expect(view).toBe(expectedChild) 247 | }) 248 | test('view inheritance', async () => { 249 | const name = 'test_view' 250 | const command = RendererMacro.command('template-view').arg(name) 251 | const block = logseq._createBlock(command.toString()) 252 | 253 | const templateBlock = logseq._createTemplateBlock( 254 | name, 'child:``await layout("base")``', {argProp: 'OVERRIDED', 'arg-prop': 'OVERRIDED'} 255 | ) 256 | const templateBlockBase = logseq._createTemplateBlock( 257 | 'base', 'base:``c.args.prop``', {argProp: 'BASE VALUE', 'arg-prop': 'BASE VALUE'}) 258 | 259 | const [includingParent, accessedVia] = [false, 'name' as LogseqReferenceAccessType] 260 | const template = new Template(templateBlock, {name, includingParent, accessedVia}) 261 | await template.init() 262 | 263 | logseq.DB.datascriptQuery.mockReturnValueOnce([templateBlockBase]) 264 | 265 | const view = await compileTemplateView( 266 | 'slot__test', 267 | template, 268 | [], 269 | { 270 | mode: 'view', 271 | currentPage: PageContext.createFromEntity(await logseq.Editor.getPage(block.page.id)), 272 | currentBlock: BlockContext.createFromEntity(block), 273 | } as ILogseqCurrentContext, 274 | ) 275 | 276 | expect(logseq.DB.datascriptQuery).toHaveBeenCalledTimes(1) 277 | 278 | const expectedBase = 'base:OVERRIDED' 279 | const expectedChild = `child:${expectedBase}` 280 | expect(view).toBe(expectedChild) 281 | }) 282 | }) 283 | -------------------------------------------------------------------------------- /tests/markdown-parsing/ast-complimented.json: -------------------------------------------------------------------------------- 1 | // recognize inline Code Block 2 | ```var foo = 123``` 3 | ["Code", "`var foo = 123"], 4 | ["Plain", "`"], 5 | 6 | ["Code", "var foo = 123"] 7 | 8 | 9 | // recognize full Code Block 10 | ```javascript: main.js 11 | var foo = 123; 12 | ``` 13 | ["Code", "`javascript: main.js\nvar foo = 123;\n"], 14 | ["Plain", "`"], 15 | 16 | ["Code_Block", { 17 | "schema": "javascript", 18 | "header": "main.js", 19 | "body": "var foo = 123;", 20 | }] 21 | 22 | 23 | // added interpolation to every Link: "" or "!" 24 | ["Link", { 25 | "full_text": "![label](https://site.com/path/image.png)", 26 | "..."}] 27 | ["Link", { 28 | "full_text": "![label](https://site.com/path/image.png)", 29 | "interpolation": "!", 30 | "..."}] 31 | 32 | 33 | // expanded assets:// protocol for Link → Complex 34 | ["Link", { 35 | "url": ["Complex", {"protocol": "assets", "link": "image.png"}], 36 | "..."}] 37 | ["Link", { 38 | "url": ["Complex", {"protocol": "file", "link": "/path/to/assets/image.png"}], 39 | "..."}] 40 | 41 | 42 | // transform Link → Search to Complex: handle ../assets/ 43 | ["Link", { 44 | "url": ["Search", "../assets/image.png"], 45 | "..."}] 46 | ["Link", { 47 | "url": ["Complex", {"protocol": "file", "link": "/path/to/assets/image.png"}], 48 | "..."}] 49 | 50 | 51 | // transform Link → Search Complex and file:// protocol 52 | ["Link", { 53 | "url": ["Search", "/path/to/assets/image.png"], 54 | "..."}] 55 | ["Link", { 56 | "url": ["Complex", {"protocol": "file", "link": "/path/to/assets/image.png"}], 57 | "..."}] 58 | -------------------------------------------------------------------------------- /tests/markdown-parsing/ast-example.json: -------------------------------------------------------------------------------- 1 | ["Plain", " - plain текст"], 2 | ["Break_Line"], 3 | 4 | ["Link", { 5 | "full_text": "[[page name]]", 6 | "url": ["Page_ref", "page name"], 7 | "label": [ ["Plain", ""] ], 8 | "metadata": ""}], 9 | ["Link", { 10 | "full_text": "[ref-алиас1]([[page name]])", 11 | "url": ["Page_ref", "page name"], 12 | "label": [ ["Plain", "ref-алиас1"] ], 13 | "metadata": ""}], 14 | 15 | ["Link", { 16 | "full_text": "((64220174-baae-46be-8a7f-452fe220b741))", 17 | "url": ["Block_ref", "64220174-baae-46be-8a7f-452fe220b741"], 18 | "label": [], 19 | "metadata": ""}], 20 | ["Link", { 21 | "full_text": "[ref-alias2](((64220174-baae-46be-8a7f-452fe220b741)))", 22 | "url": ["Block_ref", "64220174-baae-46be-8a7f-452fe220b741"], 23 | "label": [ ["Plain", "ref-alias2"] ], 24 | "metadata": ""}], 25 | 26 | ["Link", { 27 | "full_text": "[link text](http://dev.nodeca.com)", 28 | "url": ["Complex", {"protocol": "http", "link": "dev.nodeca.com"}], 29 | "label": [ ["Plain", "link text"] ], 30 | "metadata": ""}], 31 | ["Link", { 32 | "full_text": "[link with title](http://nodeca.github.io/pica/demo/ \"title text!\")", 33 | "url": ["Complex", {"protocol": "http", "link": "nodeca.github.io/pica/demo/"}], 34 | "label": [ ["Plain", "link with title"] ], 35 | "title": "title text!", 36 | "metadata": ""}], 37 | 38 | ["Link", { 39 | "full_text": "[*text*](identifier)", 40 | "url": ["Search", "identifier"], 41 | "label": [["Emphasis", [["Italic"], [["Plain", "text"]]]]], 42 | "metadata": ""}], 43 | 44 | ["Inline_Hiccup", "[:b \"LOGSEQ!\"]"] 45 | ["Inline_Html", "text"], 46 | 47 | // @@html:
@@ 48 | ["Export_Snippet", "html", "
"], 49 | 50 | // **This is bold text** 51 | ["Emphasis", [ ["Bold"], [["Plain", "This is bold text"]] ]] 52 | 53 | // __This is bold text__ 54 | // _This is italic text_ 55 | // *This is italic text* 56 | ["Emphasis", [ ["Italic"], [["Plain", "This is bold text"]] ]] 57 | 58 | // ~~Strike-through~~ 59 | ["Emphasis", [ ["Strike_through"], [["Plain", "Strike-through"]] ]] 60 | 61 | // ==Marked text== 62 | ["Emphasis", [ ["Highlight"], [["Plain", "Marked text"]] ]] 63 | 64 | // ~~***text***~~ 65 | ["Emphasis", [ ["Strike_through"], [ 66 | ["Emphasis", [ ["Italic"], [ 67 | ["Emphasis", [ ["Bold"], [ 68 | ["Plain", "text"]]]]]]]]]] 69 | 70 | // `var js = null` 71 | ["Code", "var js = null"] 72 | 73 | // Footnote 1 link[^first] 74 | // [^first]: description 75 | ["Plain", "Footnote 1 link"], 76 | ["Footnote_Reference", {"id": 1, "name": "first"}], 77 | ["Footnote_Reference", {"id": 2, "name": "first"}], 78 | 79 | // {{test macro, arg}} 80 | [ "Macro", { 81 | "name": "test", 82 | "arguments": [ "macro", "arg" ]}] 83 | -------------------------------------------------------------------------------- /tests/markdown-parsing/test.md: -------------------------------------------------------------------------------- 1 | # To view AST: 2 | $ npx mldoc convert -i tests/test.md -f ast -o tests/ast.json 3 | 4 | --- 5 | 6 | - plain текст 7 | - [[logseq/plugins/Full House Templates]] 8 | - [ref-алиас1]([[logseq/plugins/Full House Templates]]) 9 | - [ref-alias2](((64220174-baae-46be-8a7f-452fe220b741))) 10 | - ((64220174-baae-46be-8a7f-452fe220b741)) 11 | - [:b "LOGSEQ!"] 12 | 13 | --- 14 | 15 | # h1 Heading 16 | ## h2 Heading 17 | ### h3 Heading 18 | #### h4 Heading 19 | ##### h5 Heading 20 | ###### h6 Heading 21 | 22 | 23 | ## Horizontal Rules 24 | 25 | ___ 26 | 27 | --- 28 | 29 | ## Typographic replacements 30 | 31 | Enable typographer option to see result. 32 | 33 | (c) (C) (r) (R) (tm) (TM) (p) (P) +- 34 | 35 | test.. test... test..... test?..... test!.... 36 | 37 | !!!!!! ???? ,, -- --- 38 | 39 | "Smartypants, double quotes" and 'single quotes' 40 | 41 | 42 | ## Emphasis 43 | 44 | **This is bold text** 45 | 46 | __This is bold text__ 47 | 48 | *This is italic text* 49 | 50 | _This is italic text_ 51 | 52 | ~~Strikethrough~~ 53 | 54 | 55 | ## Blockquotes 56 | 57 | 58 | > Blockquotes can also be nested... 59 | >> ...by using additional greater-than signs right next to each other... 60 | > > > ...or with spaces between arrows. 61 | 62 | 63 | ## Lists 64 | 65 | Unordered 66 | 67 | + Create a list by starting a line with `+`, `-`, or `*` 68 | + Sub-lists are made by indenting 2 spaces: 69 | - Marker character change forces new list start: 70 | * Ac tristique libero volutpat at 71 | + Facilisis in pretium nisl aliquet 72 | - Nulla volutpat aliquam velit 73 | + Very easy! 74 | 75 | Ordered 76 | 77 | 1. Lorem ipsum dolor sit amet 78 | 2. Consectetur adipiscing elit 79 | 3. Integer molestie lorem at massa 80 | 81 | 82 | 1. You can use sequential numbers... 83 | 1. ...or keep all the numbers as `1.` 84 | 85 | Start numbering with offset: 86 | 87 | 57. foo 88 | 1. bar 89 | 90 | 91 | ## Code 92 | 93 | Inline `code` 94 | 95 | Indented code 96 | 97 | // Some comments 98 | line 1 of code 99 | line 2 of code 100 | line 3 of code 101 | 102 | 103 | Block code "fences" 104 | 105 | ``` 106 | Sample text here... 107 | ``` 108 | 109 | Syntax highlighting 110 | 111 | ``` js 112 | var foo = function (bar) { 113 | return bar++; 114 | }; 115 | 116 | console.log(foo(5)); 117 | ``` 118 | 119 | ## Tables 120 | 121 | | Option | Description | 122 | | ------ | ----------- | 123 | | data | path to data files to supply the data that will be passed into templates. | 124 | | engine | engine to be used for processing templates. Handlebars is the default. | 125 | | ext | extension to be used for dest files. | 126 | 127 | Right aligned columns 128 | 129 | | Option | Description | 130 | | ------:| -----------:| 131 | | data | path to data files to supply the data that will be passed into templates. | 132 | | engine | engine to be used for processing templates. Handlebars is the default. | 133 | | ext | extension to be used for dest files. | 134 | 135 | 136 | ## Links 137 | 138 | [link text](http://dev.nodeca.com) 139 | 140 | [link with title](http://nodeca.github.io/pica/demo/ "title text!") 141 | 142 | Autoconverted link https://github.com/nodeca/pica (enable linkify to see) 143 | 144 | 145 | ## Images 146 | 147 | ![Minion](https://octodex.github.com/images/minion.png) 148 | ![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat") 149 | 150 | Like links, Images also have a footnote style syntax 151 | 152 | ![Alt text][id] 153 | 154 | With a reference later in the document defining the URL location: 155 | 156 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" 157 | 158 | 159 | ## Plugins 160 | 161 | The killer feature of `markdown-it` is very effective support of 162 | [syntax plugins](https://www.npmjs.org/browse/keyword/markdown-it-plugin). 163 | 164 | 165 | ### [Emojies](https://github.com/markdown-it/markdown-it-emoji) 166 | 167 | > Classic markup: :wink: :crush: :cry: :tear: :laughing: :yum: 168 | > 169 | > Shortcuts (emoticons): :-) :-( 8-) ;) 170 | 171 | see [how to change output](https://github.com/markdown-it/markdown-it-emoji#change-output) with twemoji. 172 | 173 | 174 | ### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup) 175 | 176 | - 19^th^ 177 | - H~2~O 178 | 179 | 180 | ### [\](https://github.com/markdown-it/markdown-it-ins) 181 | 182 | ++Inserted text++ 183 | 184 | 185 | ### [\](https://github.com/markdown-it/markdown-it-mark) 186 | 187 | ==Marked text== 188 | 189 | 190 | ### [Footnotes](https://github.com/markdown-it/markdown-it-footnote) 191 | 192 | Footnote 1 link[^first]. 193 | 194 | Footnote 2 link[^second]. 195 | 196 | Inline footnote^[Text of inline footnote] definition. 197 | 198 | Duplicated footnote reference[^second]. 199 | 200 | [^first]: Footnote **can have markup** 201 | 202 | and multiple paragraphs. 203 | 204 | [^second]: Footnote text. 205 | 206 | 207 | ### [Definition lists](https://github.com/markdown-it/markdown-it-deflist) 208 | 209 | Term 1 210 | 211 | : Definition 1 212 | with lazy continuation. 213 | 214 | Term 2 with *inline markup* 215 | 216 | : Definition 2 217 | 218 | { some code, part of Definition 2 } 219 | 220 | Third paragraph of definition 2. 221 | 222 | _Compact style:_ 223 | 224 | Term 1 225 | ~ Definition 1 226 | 227 | Term 2 228 | ~ Definition 2a 229 | ~ Definition 2b 230 | 231 | 232 | ### [Abbreviations](https://github.com/markdown-it/markdown-it-abbr) 233 | 234 | This is HTML abbreviation example. 235 | 236 | It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on. 237 | 238 | *[HTML]: Hyper Text Markup Language 239 | 240 | ### [Custom containers](https://github.com/markdown-it/markdown-it-container) 241 | 242 | ::: warning 243 | *here be dragons* 244 | ::: 245 | -------------------------------------------------------------------------------- /tests/markdown-parsing/unsupported.md: -------------------------------------------------------------------------------- 1 | /** horizontal line 2 | * mldoc: x 3 | * fh: x 4 | */ 5 | ["Plain", "___"], 6 | ["Plain", "---"], 7 | ["Plain", "***"], 8 | 9 | 10 | /** simple cases 11 | * mldoc: x 12 | * fh: x 13 | */ 14 | ["Plain", "### h3 Heading"], 15 | ["Plain", "> Blockquotes can also be nested..."], 16 | ["Plain", "1. Lorem ipsum dolor sit amet"], 17 | ["Plain", "- Lorem ipsum dolor sit amet"], 18 | 19 | 20 | /** code block 21 | * mldoc: x 22 | * fh: v 23 | */ 24 | ```js 25 | var foo = bar() 26 | ``` 27 | ["Code", "` js\nvar foo = bar()\n"], 28 | ["Plain", "`"], 29 | 30 | 31 | /** tables 32 | * mldoc: x 33 | * fh: x 34 | */ 35 | | A | B | 36 | | 1 | 2 | 37 | 38 | 39 | /** bug: no meta info about "!" 40 | * mldoc: x 41 | * fh: v 42 | */ 43 | ["Link", { 44 | "full_text": "![Minion](https://octodex.github.com/images/minion.png)", 45 | "url": ["Complex",{"protocol": "https", "link": "octodex.github.com/images/minion.png"}], 46 | "label": [ ["Plain", "Minion"] ], 47 | "metadata": ""}], 48 | 49 | 50 | /** reference by id 51 | * mldoc: x 52 | * fh: x 53 | */ 54 | ![Alt text][id] 55 | With a reference later in the document defining the URL location: 56 | [id]: https://octodex.github.com/images/dojocat.jpg "The Dojocat" 57 | 58 | 59 | /** inline footnote 60 | * mldoc: x 61 | * fh: x 62 | */ 63 | ["Plain", "Inline footnote^[Text of inline footnote] definition."], 64 | 65 | 66 | /** footnotes 67 | * mldoc: x 68 | * fh: x 69 | */ 70 | Footnote link[^first] 71 | [^first]: Footnote 72 | 73 | ["Footnote_Reference",{"id": 1,"name": "first"}], 74 | ["Footnote_Reference",{"id": 2,"name": "first"}], 75 | -------------------------------------------------------------------------------- /tests/tags/dates.test.ts: -------------------------------------------------------------------------------- 1 | import { LogseqMock } from '@tests/index' 2 | 3 | import { getTemplateTagsContext } from '@src/tags' 4 | import { ILogseqContext, dayjs } from '@src/context' 5 | import { toISODate } from '@src/utils' 6 | 7 | 8 | describe('string dates values', () => { 9 | test('ok iso format', async () => { 10 | await LogseqMock(null, {preferredDateFormat: 'YYYY-MM-DD'}) 11 | const tags = getTemplateTagsContext({} as ILogseqContext) as any 12 | 13 | expect( tags.yesterday ).toBe(dayjs().subtract(1, 'day').format('page')) 14 | expect( tags.tomorrow ).toBe(dayjs().add(1, 'day').format('page')) 15 | expect( tags.today ).toBe(dayjs().format('page')) 16 | 17 | expect( tags.time ).toBe(dayjs().format('HH:mm')) 18 | }) 19 | test('ok date objects', async () => { 20 | await LogseqMock(null, {preferredDateFormat: 'YYYY-MM-DD'}) 21 | const tags = getTemplateTagsContext({} as ILogseqContext) as any 22 | 23 | const yesterday = new Date() 24 | yesterday.setDate(yesterday.getDate() - 1) 25 | yesterday.setHours(0) 26 | yesterday.setMinutes(0) 27 | yesterday.setSeconds(0) 28 | yesterday.setMilliseconds(0) 29 | 30 | const tomorrow = new Date() 31 | tomorrow.setDate(tomorrow.getDate() + 1) 32 | tomorrow.setHours(0) 33 | tomorrow.setMinutes(0) 34 | tomorrow.setSeconds(0) 35 | tomorrow.setMilliseconds(0) 36 | 37 | const today = new Date() 38 | today.setHours(0) 39 | today.setMinutes(0) 40 | today.setSeconds(0) 41 | today.setMilliseconds(0) 42 | 43 | const now = new Date() 44 | now.setMilliseconds(0) 45 | 46 | expect( tags.date.yesterday ).toEqual(dayjs(yesterday)) 47 | expect( tags.date.today ).toEqual(dayjs(today)) 48 | expect( tags.date.now ).toEqual(dayjs(now)) 49 | expect( tags.date.tomorrow ).toEqual(dayjs(tomorrow)) 50 | 51 | expect( tags.date.from ).toBe(dayjs) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/tags/plain.test.ts: -------------------------------------------------------------------------------- 1 | import { LogseqMock } from '@tests/index' 2 | 3 | import { v4 as uuid } from 'uuid' 4 | import * as dayjs from 'dayjs' 5 | 6 | import { _private as tags } from '@src/tags' 7 | import { _private as app } from '@src/app' 8 | import { BlockContext, PageContext } from '@src/context' 9 | import { toISODate } from '@src/utils' 10 | 11 | 12 | describe('ref template tag', () => { 13 | test('strings', () => { 14 | expect( tags.ref('page') ).toBe('[[page]]') 15 | expect( tags.ref('[page]') ).toBe('[[[page]]]') 16 | expect( tags.ref('[[page]]') ).toBe('[[page]]') 17 | expect( tags.ref('[[page') ).toBe('[[[[page]]') 18 | 19 | expect( tags.ref('page with spaces') ).toBe('[[page with spaces]]') 20 | expect( tags.ref(' page with spaces ') ).toBe('[[page with spaces]]') 21 | expect( tags.ref(' [[page with spaces ]] ') ).toBe('[[page with spaces ]]') 22 | }) 23 | test('block refs', () => { 24 | const blockID = uuid() 25 | expect( tags.ref(blockID) ).toBe(`((${blockID}))`) 26 | expect( tags.ref(`[[${blockID}]]`) ).toBe(`[[${blockID}]]`) 27 | expect( tags.ref(`((${blockID}))`) ).toBe(`((${blockID}))`) 28 | expect( tags.ref(`((block))`) ).toBe(`[[((block))]]`) 29 | }) 30 | test('date strings', async () => { 31 | await LogseqMock(null, {preferredDateFormat: 'yyyy-MM-dd EEE'}) 32 | expect( tags.ref('2023-01-01') ).toBe('[[2023-01-01 Sun]]') 33 | expect( tags.ref('[[2023-01-01]]') ).toBe('[[2023-01-01]]') 34 | }) 35 | test('dates', async () => { 36 | await LogseqMock(null, {preferredDateFormat: 'yyyy-MM-dd EEE'}) 37 | expect( tags.ref(dayjs('2023-01-01')) ).toBe('[[2023-01-01 Sun]]') 38 | 39 | const now = new Date() 40 | const day = now.toDateString().slice(0, 3) 41 | expect( tags.ref(dayjs()) ).toBe(`[[${toISODate(now)} ${day}]]`) 42 | }) 43 | test('block context without api call', async () => { 44 | const logseq = await LogseqMock() 45 | 46 | const id = uuid() 47 | const block = new BlockContext(0, id) 48 | 49 | const mock_get_block = jest.spyOn(logseq.api, 'get_block') 50 | mock_get_block.mockReturnValue({uuid: id} as unknown as void) 51 | 52 | const r = tags.ref(block) 53 | expect(mock_get_block).toHaveBeenCalledTimes(0) 54 | expect(r).toBe(`((${id}))`) 55 | }) 56 | test('block context with api call', async () => { 57 | const logseq = await LogseqMock() 58 | 59 | const id = uuid() 60 | const block = new BlockContext(1) 61 | 62 | const mock_get_block = jest.spyOn(logseq.api, 'get_block') 63 | mock_get_block.mockReturnValue({uuid: id} as unknown as void) 64 | 65 | const r = tags.ref(block) 66 | expect(mock_get_block).toHaveBeenCalled() 67 | expect(r).toBe(`((${id}))`) 68 | }) 69 | test('page context without api call', async () => { 70 | const logseq = await LogseqMock() 71 | 72 | const name = 'Test Page' 73 | const page = new PageContext(0, name) 74 | 75 | logseq.api.get_page.mockReturnValue({originalName: name} as unknown as void) 76 | 77 | const r = tags.ref(page) 78 | expect(logseq.api.get_page).toHaveBeenCalledTimes(0) 79 | expect(r).toBe(`[[${name}]]`) 80 | }) 81 | test('page context with api call', async () => { 82 | const logseq = await LogseqMock() 83 | 84 | const name = 'Test Page' 85 | const page = new PageContext(0) 86 | 87 | logseq.api.get_page.mockReturnValue({originalName: name} as unknown as void) 88 | 89 | const r = tags.ref(page) 90 | expect(logseq.api.get_page).toHaveBeenCalled() 91 | expect(r).toBe(`[[${name}]]`) 92 | }) 93 | }) 94 | 95 | describe('tag template tag', () => { 96 | test('strings', () => { 97 | expect( tags.tag('page') ).toBe('#page') 98 | expect( tags.tag('[page]') ).toBe('#[page]') 99 | expect( tags.tag('[[page]]') ).toBe('#page') 100 | expect( tags.tag('[[page') ).toBe('#[[page') 101 | 102 | expect( tags.tag('page with spaces') ).toBe('#[[page with spaces]]') 103 | expect( tags.tag(' page with spaces ') ).toBe('#[[page with spaces]]') 104 | expect( tags.tag(' [[page with spaces ]] ') ).toBe('#[[page with spaces ]]') 105 | }) 106 | test('date strings', async () => { 107 | await LogseqMock(null, {preferredDateFormat: 'yyyy-MM-dd EEE'}) 108 | expect( tags.tag('2023-01-01') ).toBe('#[[2023-01-01 Sun]]') 109 | expect( tags.tag('[[2023-01-01]]') ).toBe('#2023-01-01') 110 | }) 111 | test('dates', async () => { 112 | await LogseqMock(null, {preferredDateFormat: 'yyyy-MM-dd EEE'}) 113 | expect( tags.tag(dayjs('2023-01-01')) ).toBe('#[[2023-01-01 Sun]]') 114 | 115 | const now = new Date() 116 | const day = now.toDateString().slice(0, 3) 117 | expect( tags.tag(dayjs()) ).toBe(`#[[${toISODate(now)} ${day}]]`) 118 | }) 119 | test('page context without api call', async () => { 120 | const logseq = await LogseqMock() 121 | 122 | const name = 'Test Page' 123 | const page = new PageContext(0, name) 124 | 125 | logseq.api.get_page.mockReturnValue({originalName: name} as unknown as void) 126 | 127 | const r = tags.tag(page) 128 | expect(logseq.api.get_page).toHaveBeenCalledTimes(0) 129 | expect(r).toBe(`#[[${name}]]`) 130 | }) 131 | test('page context with api call', async () => { 132 | const logseq = await LogseqMock() 133 | 134 | const name = 'Test Page' 135 | const page = new PageContext(0) 136 | 137 | logseq.api.get_page.mockReturnValue({originalName: name} as unknown as void) 138 | 139 | const r = tags.tag(page) 140 | expect(logseq.api.get_page).toHaveBeenCalled() 141 | expect(r).toBe(`#[[${name}]]`) 142 | }) 143 | }) 144 | 145 | describe('embed template tag', () => { 146 | test('strings', async () => { 147 | expect( tags.embed('page') ).toBe('{{embed [[page]]}}') 148 | expect( tags.embed('[[page]]') ).toBe('{{embed [[page]]}}') 149 | 150 | const blockID = uuid() 151 | expect( tags.embed(blockID) ).toBe(`{{embed ((${blockID}))}}`) 152 | expect( tags.embed(`((${blockID}))`) ).toBe(`{{embed ((${blockID}))}}`) 153 | }) 154 | test('dates', async () => { 155 | await LogseqMock(null, {preferredDateFormat: 'dd-MM-yyyy'}) 156 | expect( tags.embed('2023-01-01') ).toBe('{{embed [[01-01-2023]]}}') 157 | expect( tags.embed('[[2023|01|01]]') ).toBe('{{embed [[2023|01|01]]}}') 158 | expect( tags.embed(dayjs('2023-12-12')) ).toBe('{{embed [[12-12-2023]]}}') 159 | }) 160 | test('block context', async () => { 161 | const logseq = await LogseqMock() 162 | 163 | const id = uuid() 164 | const block = new BlockContext(0, id) 165 | 166 | expect( tags.embed(block) ).toBe(`{{embed ((${id}))}}`) 167 | }) 168 | test('page context', async () => { 169 | const logseq = await LogseqMock() 170 | 171 | const name = 'Test Page' 172 | const page = new PageContext(0, name) 173 | 174 | expect( tags.embed(page) ).toBe(`{{embed [[${name}]]}}`) 175 | }) 176 | }) 177 | 178 | describe('empty template tag', () => { 179 | test('empty values', () => { 180 | expect( tags.empty('') ).toBe('') 181 | expect( tags.empty(' ') ).toBe('') 182 | 183 | expect( tags.empty('""') ).toBe('') 184 | expect( tags.empty("''") ).toBe('') 185 | expect( tags.empty("``") ).toBe('') 186 | expect( tags.empty('«»') ).toBe('') 187 | 188 | expect( tags.empty('-') ).toBe('') 189 | expect( tags.empty('—') ).toBe('') 190 | 191 | expect( tags.empty({}) ).toBe('') 192 | expect( tags.empty([]) ).toBe('') 193 | 194 | expect( tags.empty(null) ).toBe('') 195 | expect( tags.empty(undefined) ).toBe('') 196 | }) 197 | test('non-empty values', () => { 198 | expect( tags.empty('page') ).toBe('page') 199 | 200 | expect( tags.empty(false) ).toBe(false) 201 | expect( tags.empty(true) ).toBe(true) 202 | 203 | expect( tags.empty(0) ).toBe(0) 204 | expect( tags.empty(1) ).toBe(1) 205 | 206 | expect( tags.empty(0.1) ).toBe(0.1) 207 | expect( tags.empty(1.1) ).toBe(1.1) 208 | 209 | expect( tags.empty({'page': 1}) ).toEqual({'page': 1}) 210 | expect( tags.empty(['page']) ).toEqual(['page']) 211 | }) 212 | test('fallback', () => { 213 | expect( tags.empty('', 'default') ).toBe('default') 214 | expect( tags.empty('no', 'default') ).toBe('no') 215 | }) 216 | }) 217 | 218 | describe('bool template tag', () => { 219 | test('true values', () => { 220 | expect( tags.bool('✅') ).toBe(true) 221 | expect( tags.bool('+') ).toBe(true) 222 | expect( tags.bool('1') ).toBe(true) 223 | expect( tags.bool('v') ).toBe(true) 224 | expect( tags.bool('yes')).toBe(true) 225 | expect( tags.bool('ok') ).toBe(true) 226 | expect( tags.bool('ON') ).toBe(true) 227 | }) 228 | test('false values', () => { 229 | expect( tags.bool('❌') ).toBe(false) 230 | expect( tags.bool('-') ).toBe(false) 231 | expect( tags.bool('0') ).toBe(false) 232 | expect( tags.bool('x') ).toBe(false) 233 | expect( tags.bool('no') ).toBe(false) 234 | expect( tags.bool('none')).toBe(false) 235 | expect( tags.bool('OFF') ).toBe(false) 236 | }) 237 | test('non-bool & fallback', () => { 238 | const fallback = [] 239 | expect( tags.bool(' ', fallback)).toBe(fallback) 240 | expect( tags.bool('', 13) ).toBe(13) 241 | expect( tags.bool('""') ).toBe('') 242 | expect( tags.bool('—') ).toBe('') 243 | }) 244 | }) 245 | 246 | describe('when template tag', () => { 247 | test('false condition', async () => { 248 | expect( tags.when('', 'result') ).toBe('') 249 | expect( tags.when(false, 'result') ).toBe('') 250 | expect( tags.when(0, 'result') ).toBe('') 251 | expect( tags.when(null, 'result') ).toBe('') 252 | expect( tags.when(undefined, 'result') ).toBe('') 253 | 254 | expect( tags.when('page'.length == 0, 'result') ).toBe('') 255 | }) 256 | test('fallback', async () => { 257 | expect( tags.when('', 'result', 'empty') ).toBe('empty') 258 | expect( tags.when('ok', 123, 'empty') ).toBe(123) 259 | expect( tags.when('', 'result', 123) ).toBe(123) 260 | }) 261 | test('substitution', async () => { 262 | expect( tags.when(123, '{{embed [[$1]]}}') ).toBe('{{embed [[123]]}}') 263 | expect( tags.when(123, '{{embed [[${}]]}}') ).toBe('{{embed [[123]]}}') 264 | expect( tags.when(123, '{{embed [[${_}]]}}') ).toBe('{{embed [[123]]}}') 265 | }) 266 | }) 267 | 268 | describe('fill template tag', () => { 269 | test('ok', async () => { 270 | expect( tags.fill('1', '+', 2) ).toBe('+1') 271 | expect( tags.fill(1, '_', 3) ).toBe('__1') 272 | expect( tags.fill('x', ' ', 3) ).toBe(' x') 273 | }) 274 | test('negative', async () => { 275 | expect( tags.fill('val', '', 10) ).toBe('val') 276 | expect( tags.fill('ok', 'x', 2) ).toBe('ok') 277 | expect( tags.fill('okay', ' ', 2) ).toBe('okay') 278 | }) 279 | test('alignment', async () => { 280 | expect( tags.fill(1, '_', 2) ).toBe('_1') 281 | expect( tags.fill(1, '_', 2, 'right') ).toBe('_1') 282 | expect( tags.fill(1, '_', 2, 'left') ).toBe('1_') 283 | expect( tags.fill(1, '_', 3, 'center') ).toBe('_1_') 284 | expect( tags.fill(1, '_', 4, 'center') ).toBe('__1_') 285 | }) 286 | }) 287 | 288 | describe('zeros template tag', () => { 289 | test('ok', async () => { 290 | expect( tags.zeros('1', 2) ).toBe('01') 291 | expect( tags.zeros(1, 3) ).toBe('001') 292 | }) 293 | test('negative', async () => { 294 | expect( tags.zeros('xxx', 2) ).toBe('xxx') 295 | }) 296 | }) 297 | 298 | describe('spaces template tag', () => { 299 | test('ok', async () => { 300 | expect( tags.spaces('1', 2) ).toBe(' 1') 301 | expect( tags.spaces(1, 3) ).toBe(' 1') 302 | }) 303 | test('negative', async () => { 304 | expect( tags.spaces('xxx', 2) ).toBe('xxx') 305 | }) 306 | test('alignment', async () => { 307 | expect( tags.spaces(1, 2) ).toBe(' 1') 308 | expect( tags.spaces(1, 2, 'right') ).toBe(' 1') 309 | expect( tags.spaces(1, 2, 'left') ).toBe('1 ') 310 | expect( tags.spaces(1, 3, 'center') ).toBe(' 1 ') 311 | expect( tags.spaces(1, 4, 'center') ).toBe(' 1 ') 312 | }) 313 | }) 314 | -------------------------------------------------------------------------------- /tests/utils/other.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | p, f, html, 3 | countOf, indexOfNth, 4 | escapeForRegExp, escapeForHTML, escapeForHiccup, 5 | } from '@src/utils/other' 6 | 7 | 8 | test('formatting string with f-template literal', () => { 9 | const format = f`Hello, ${'name'}!` 10 | expect( 11 | format({name: 'Logseq'}) 12 | ).toBe('Hello, Logseq!') 13 | }) 14 | 15 | test('constructing strings prefixed with plugin-id with p-template literal', () => { 16 | expect( 17 | p`Hello, Logseq!` 18 | ).toBe('#logseq13-full-house: Hello, Logseq!') 19 | }) 20 | 21 | test('removing spaces from html code with html-template literal', () => { 22 | expect( 23 | html` 24 |
25 |

Text

26 |
27 | ` 28 | ).toBe('

Text

') 29 | 30 | expect( 31 | html` 32 | 33 |
34 | 35 |

Text

36 | 37 |
38 | 39 | ` 40 | ).toBe('
\n\n

Text

\n\n
') 41 | }) 42 | 43 | test('counting of substring in string', () => { 44 | expect( countOf('aaa, bbb, ccc', ',') ).toBe(2) 45 | expect( countOf('aaa, bbb, ccc', ', ') ).toBe(2) 46 | 47 | expect( countOf('aaa, bbb, ccc', 'aaa') ).toBe(1) 48 | expect( countOf('aaa, bbb, ccc', 'aaa, bbb, ccc') ).toBe(1) 49 | 50 | expect( countOf('aaa a', 'a') ).toBe(4) 51 | expect( countOf('aaa a', 'a ') ).toBe(1) 52 | expect( countOf('aaa a', ' a') ).toBe(1) 53 | expect( countOf('aaa a', 'aa') ).toBe(1) 54 | expect( countOf('aaa a', 'aaa') ).toBe(1) 55 | 56 | expect( countOf('aaa', '') ).toBe(0) 57 | }) 58 | 59 | test('finding index of Nth substring in string', () => { 60 | expect( indexOfNth('aaa, bbb, ccc', ',', 1) ).toBe(3) 61 | expect( indexOfNth('aaa, bbb, ccc', ',', 2) ).toBe(8) 62 | expect( indexOfNth('aaa, bbb, ccc', ', ', 1) ).toBe(3) 63 | expect( indexOfNth('aaa, bbb, ccc', ', ', 2) ).toBe(8) 64 | 65 | expect( indexOfNth('aaa, bbb, ccc', ', ', 3) ).toBe(null) 66 | 67 | expect( indexOfNth('aaa, bbb, ccc', 'aaa', 1) ).toBe(0) 68 | expect( indexOfNth('aaa, bbb, ccc', 'aaa, bbb, ccc', 1) ).toBe(0) 69 | 70 | expect( indexOfNth('aaa a', 'a', 1) ).toBe(0) 71 | expect( indexOfNth('aaa a', 'a', 4) ).toBe(4) 72 | expect( indexOfNth('aaa a', 'aa', 1) ).toBe(0) 73 | 74 | expect( indexOfNth('aaa', '') ).toBe(null) 75 | }) 76 | 77 | test('escaping string to use as is in regexp', () => { 78 | expect( escapeForRegExp('(hell[\\o])')).toBe('\\(hell\\[\\\\o\\]\\)') 79 | }) 80 | 81 | test('escaping string to use in html', () => { 82 | expect( escapeForHTML('text')).toBe('<tag attr="val">text</tag>') 83 | expect( escapeForHTML("'&'")).toBe("'&'") 84 | }) 85 | 86 | test('escaping string to use in hiccup', () => { 87 | expect( escapeForHiccup('text')).toBe("text") 88 | }) 89 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src", 4 | "./tests/index.ts", 5 | "./tests/**/*.test.ts" 6 | ], 7 | "exclude": [ 8 | "dist", 9 | "node_modules" 10 | ], 11 | "compilerOptions": { 12 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 13 | 14 | /* Projects */ 15 | // "incremental": true, /* Enable incremental compilation */ 16 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 17 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 18 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 19 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 20 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 21 | 22 | /* Language and Environment */ 23 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 24 | "lib": ["DOM", "DOM.Iterable", "ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 25 | "jsx": "react-jsx", /* Specify what JSX code is generated. */ 26 | "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 27 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 28 | // "jsxFactory": "h", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 29 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 30 | "jsxImportSource": "preact", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 31 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 32 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 33 | "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 34 | 35 | /* Modules */ 36 | "module": "ESNext", /* Specify what module code is generated. */ 37 | // "rootDir": "./", /* Specify the root folder within your source files. */ 38 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 39 | "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 40 | "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ 41 | "@src/*": ["src/*"], 42 | "@tests/*": ["tests/*"] 43 | }, 44 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 45 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 46 | "types": ["vite/client", "node", "jest"], /* Specify type package names to be included without being referenced in a source file. */ 47 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 48 | "resolveJsonModule": true, /* Enable importing .json files */ 49 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 50 | 51 | /* JavaScript Support */ 52 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 53 | "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 54 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 55 | 56 | /* Emit */ 57 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 58 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 59 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 60 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 61 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 62 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 63 | // "removeComments": true, /* Disable emitting comments. */ 64 | "noEmit": true, /* Disable emitting files from a compilation. */ 65 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 66 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 67 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 68 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 69 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 70 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 71 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 72 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 73 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 74 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 75 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 76 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 77 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 78 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 79 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 80 | 81 | /* Interop Constraints */ 82 | "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 83 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 84 | "esModuleInterop": false, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 85 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 86 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 87 | 88 | /* Type Checking */ 89 | "strict": true, /* Enable all strict type-checking options. */ 90 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 91 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 92 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 93 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 94 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 95 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 96 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 97 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 98 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 99 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 100 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 101 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 102 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 103 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 104 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 105 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 106 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 107 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 108 | 109 | /* Completeness */ 110 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 111 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import logseqDevPlugin from 'vite-plugin-logseq' 3 | 4 | 5 | export default defineConfig(({ command, mode, ssrBuild }) => { 6 | const forProd = mode === 'production' 7 | 8 | return { 9 | plugins: [ 10 | // Makes HMR available for development 11 | logseqDevPlugin() 12 | ], 13 | build: { 14 | sourcemap: !forProd, 15 | target: 'esnext', 16 | minify: forProd ? 'esbuild' : false, 17 | }, 18 | esbuild: { // docs: https://esbuild.github.io/api/#minify 19 | // pure: ['console.log'], // remove any console.log 20 | minifyIdentifiers: false, // keep variable names 21 | }, 22 | } 23 | }) 24 | --------------------------------------------------------------------------------