├── .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 | 
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 |
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 |
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 | *
44 | * ` // => ''
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": "",
26 | "..."}]
27 | ["Link", {
28 | "full_text": "",
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 | 
148 | 
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": "",
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 |
27 | `
28 | ).toBe('')
29 |
30 | expect(
31 | html`
32 |
33 |
38 |
39 | `
40 | ).toBe('')
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 |
--------------------------------------------------------------------------------