├── .github
├── FUNDING.yaml
└── workflows
│ └── publish.yml
├── .gitignore
├── .npmcheckrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── coffee.png
├── logseq-logo.png
└── logseq.png
├── icon.png
├── index.html
├── package.json
├── pnpm-lock.yaml
├── release.config.js
├── src
├── app.ts
├── commands
│ ├── index.ts
│ └── magic_markup.ts
├── css
│ ├── border_view.css
│ ├── columns_view.css
│ ├── gallery_view.css
│ ├── hide_dot_refs__hover.css
│ ├── hide_dot_refs__wrap.css
│ ├── spare_blocks.css
│ └── tabular_view.css
├── entry.ts
├── features.ts
├── utils
│ ├── index.ts
│ ├── logseq.ts
│ └── other.ts
└── views.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 | persist-credentials: false
16 |
17 | - name: Setup Node.js environment
18 | uses: actions/setup-node@v3.6.0
19 | with:
20 | node-version: "20"
21 |
22 | - name: Setup pnpm
23 | uses: pnpm/action-setup@v4
24 | with:
25 | version: latest
26 |
27 | - name: Install dependencies
28 | run: pnpm install
29 |
30 | - name: Build dist
31 | run: pnpm prod
32 |
33 | - name: Install zip
34 | uses: montudor/action-zip@v1
35 |
36 | - name: Release
37 | run: npx semantic-release
38 | env:
39 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.8.3](https://github.com/stdword/logseq13-missing-commands/compare/v1.8.2...v1.8.3) (2024-11-09)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * bug with properties got shown after block update ([b730a54](https://github.com/stdword/logseq13-missing-commands/commit/b730a54cf26969ba163a8abbcaa7fcef97490a03))
7 |
8 | ## [1.8.2](https://github.com/stdword/logseq13-missing-commands/compare/v1.8.1...v1.8.2) (2024-09-16)
9 |
10 |
11 | ### Bug Fixes
12 |
13 | * keep cursor position ast the start/end of the blocks in movement commands ([b96b05b](https://github.com/stdword/logseq13-missing-commands/commit/b96b05baa220570a199e70bc4c8676436095da52))
14 | * set heading command in selection mode ([2efc368](https://github.com/stdword/logseq13-missing-commands/commit/2efc3680dd1538a444d5ac7a7d32ac45291642fe))
15 |
16 | ## [1.8.1](https://github.com/stdword/logseq13-missing-commands/compare/v1.8.0...v1.8.1) (2024-04-25)
17 |
18 |
19 | ### Bug Fixes
20 |
21 | * **features:** ⌥ + mouse click on ref wihen ref contains «-» sign ([0eeea74](https://github.com/stdword/logseq13-missing-commands/commit/0eeea744d7b056ac2d7f8be0c88bc49d1b1df2de))
22 |
23 | # [1.8.0](https://github.com/stdword/logseq13-missing-commands/compare/v1.7.0...v1.8.0) (2024-04-25)
24 |
25 |
26 | ### Bug Fixes
27 |
28 | * **commands:** toggle heading: preserve edited content ([d4db38e](https://github.com/stdword/logseq13-missing-commands/commit/d4db38ece9a486ef4ca120e7b11869e540e177d4))
29 |
30 |
31 | ### Features
32 |
33 | * **commands:** split by commas ([a0acf35](https://github.com/stdword/logseq13-missing-commands/commit/a0acf355c91e49ac12a5219ea5a2051aa9fcf297))
34 | * **commands:** trim lines punctiation & split by semicolon ([11a9d7e](https://github.com/stdword/logseq13-missing-commands/commit/11a9d7e73977abb9a8f55f789bd5d800b33f0182))
35 |
36 | # [1.7.0](https://github.com/stdword/logseq13-missing-commands/compare/v1.6.0...v1.7.0) (2024-01-31)
37 |
38 |
39 | ### Bug Fixes
40 |
41 | * **commands:** magic split: add case with ordered lists in 1.2 style (no dot in the end) ([61b065a](https://github.com/stdword/logseq13-missing-commands/commit/61b065a5909ac16e84d2f96e1becd3fa9353223c))
42 |
43 |
44 | ### Features
45 |
46 | * **features:** mouse ref click: plugins integration — shorten my links & awesome links ([c84160b](https://github.com/stdword/logseq13-missing-commands/commit/c84160b0f3d81dbcced32386445e213304ab751d))
47 | * **views:** tabular view: add hidden header mode with #.tabular0 ([fe0fca0](https://github.com/stdword/logseq13-missing-commands/commit/fe0fca0db8862703a90d583777442f2522b1add2))
48 |
49 | # [1.6.0](https://github.com/stdword/logseq13-missing-commands/compare/v1.5.0...v1.6.0) (2024-01-24)
50 |
51 |
52 | ### Bug Fixes
53 |
54 | * click alignment for tags ([df407aa](https://github.com/stdword/logseq13-missing-commands/commit/df407aad08a74be54ac210b9f3923b70ca6899d1))
55 |
56 |
57 | ### Features
58 |
59 | * **commands:** magic formatting: magic quotes ([f8282c6](https://github.com/stdword/logseq13-missing-commands/commit/f8282c687064d0485612162c74af11ffbed9d7b4))
60 | * **commands:** magic formatting: magic reference & magic tag commands ([cfbd402](https://github.com/stdword/logseq13-missing-commands/commit/cfbd402ade3053b60213a03438deaf818eda334d))
61 | * **features:** mouse click with opt/alt on page ref ([89ebb29](https://github.com/stdword/logseq13-missing-commands/commit/89ebb29ccc667a5d4dbb67b6f523b98d54112f1a))
62 |
63 | # [1.5.0](https://github.com/stdword/logseq13-missing-commands/compare/v1.4.0...v1.5.0) (2024-01-13)
64 |
65 |
66 | ### Bug Fixes
67 |
68 | * **commands:** change italics style from *text* to _text_ ([511bbb2](https://github.com/stdword/logseq13-missing-commands/commit/511bbb2df32d95722769eb7871d47a8ea4993f90))
69 | * **commands:** not applying markup on empty lines without selected text in edit mode ([16ba455](https://github.com/stdword/logseq13-missing-commands/commit/16ba4557b97e487a902ad256a86b473852336375))
70 | * **commands:** prevent overwriting during update of editing block ([3d6d834](https://github.com/stdword/logseq13-missing-commands/commit/3d6d834f385aefc93055932be45f302e844d26d3))
71 | * **views:** tabular view ([c03b7e6](https://github.com/stdword/logseq13-missing-commands/commit/c03b7e632cadf1eea61f4c897ffc3f9782a4f30f))
72 |
73 |
74 | ### Features
75 |
76 | * **command:** remove HTML tags ([e312691](https://github.com/stdword/logseq13-missing-commands/commit/e312691d8b3fc22d881cc06435876dd269a04bc4))
77 | * **commands:** magic bold ([c8b6314](https://github.com/stdword/logseq13-missing-commands/commit/c8b63146fda7bc09094ca6bf6a921a88369efba5))
78 | * **commands:** magic formatting: add alternative markup styles ([b21a7a0](https://github.com/stdword/logseq13-missing-commands/commit/b21a7a03ba6ac48b384d04e9995c223db40e4720))
79 | * **commands:** magic italics, underline, strikethrough, highlight, code ([2afc7cf](https://github.com/stdword/logseq13-missing-commands/commit/2afc7cf93d9704eecfb2d08c67ab6b2554944586))
80 | * **commands:** parse youtube timestamps ([8d5002a](https://github.com/stdword/logseq13-missing-commands/commit/8d5002aea39ac6e75acf12e41c17b0592f2783b8))
81 | * **views:** columns view, box view, gallery view ([5998d9b](https://github.com/stdword/logseq13-missing-commands/commit/5998d9b37e5517566d6af26274f7e3248dff20a5))
82 | * **views:** columns, gallery and border views ([05bc386](https://github.com/stdword/logseq13-missing-commands/commit/05bc3866eaadb77fc81e697d7039377a43e25757))
83 |
84 | # [1.4.0](https://github.com/stdword/logseq13-missing-commands/compare/v1.3.2...v1.4.0) (2024-01-04)
85 |
86 |
87 | ### Bug Fixes
88 |
89 | * **views:** tabular view style fix ([cbdbc5d](https://github.com/stdword/logseq13-missing-commands/commit/cbdbc5d26d07dc1386cc6eb53f89a27897ab09b0))
90 |
91 |
92 | ### Features
93 |
94 | * **commands:** change case set of commands ([7af5ffb](https://github.com/stdword/logseq13-missing-commands/commit/7af5ffb0ba381d7b5255fec8ffd290aadcb2944e))
95 | * **commands:** possibility to specify spare space size ([4f1e77d](https://github.com/stdword/logseq13-missing-commands/commit/4f1e77d8a3dbc2fdaad46cb9409eabae5f2e6610))
96 | * **feature:** cSS: spare blocks ([335eda8](https://github.com/stdword/logseq13-missing-commands/commit/335eda8b3690c6895c4830fdddb71689209ca438))
97 | * **views:** hide dotted refs in two styles ([68f515c](https://github.com/stdword/logseq13-missing-commands/commit/68f515c2868a3d76ae3ea8cfe9c172a462edee19))
98 | * **views:** tabular view ([90490a5](https://github.com/stdword/logseq13-missing-commands/commit/90490a5472ac846e2b491c63e583d1fb43e69452))
99 |
100 | ## [1.3.2](https://github.com/stdword/logseq13-missing-commands/compare/v1.3.1...v1.3.2) (2023-12-31)
101 |
102 |
103 | ### Bug Fixes
104 |
105 | * **commands:** add non-nested version for remove new lines command ([790066b](https://github.com/stdword/logseq13-missing-commands/commit/790066b8ec2940a19c3aeda37c5585ac20028224))
106 |
107 | ## [1.3.1](https://github.com/stdword/logseq13-missing-commands/compare/v1.3.0...v1.3.1) (2023-12-30)
108 |
109 |
110 | ### Bug Fixes
111 |
112 | * **commands:** remove new lines command improve: add spaces ([f47b19d](https://github.com/stdword/logseq13-missing-commands/commit/f47b19db34dd1301250ffe0a7b588a1320887995))
113 |
114 | # [1.3.0](https://github.com/stdword/logseq13-missing-commands/compare/v1.2.0...v1.3.0) (2023-12-30)
115 |
116 |
117 | ### Features
118 |
119 | * **commands:** remove new lines ([277bcf8](https://github.com/stdword/logseq13-missing-commands/commit/277bcf86754f0b7b3a3a99d48469d13096df1d69))
120 | * **commands:** split & join by sentences ([d30801e](https://github.com/stdword/logseq13-missing-commands/commit/d30801e1073b8733c9df5e9aaf6f9127362e81e2))
121 | * **features:** home-End processings for edit mode & Tab-trigger + access to page name on Search ([eb9476b](https://github.com/stdword/logseq13-missing-commands/commit/eb9476b139add07993444f1cf58560da3ee66520))
122 | * support • as bullet list ([b006d3f](https://github.com/stdword/logseq13-missing-commands/commit/b006d3fcb2f7ee36d0b89f99010780846aeb0e49))
123 |
124 | # [1.2.0](https://github.com/stdword/logseq13-missing-commands/compare/v1.1.0...v1.2.0) (2023-12-21)
125 |
126 |
127 | ### Bug Fixes
128 |
129 | * bug in batch inserting with cutting off spaces ([888ec13](https://github.com/stdword/logseq13-missing-commands/commit/888ec132792369a478903d27c0ed8d38bc549a04))
130 | * bug with inserting under parent when blokc is numbered; fix wrong command key ([9ad9f9e](https://github.com/stdword/logseq13-missing-commands/commit/9ad9f9e2345b736588ab48c1a427ba8c4e44c718))
131 | * bug with splittting & wrong commands keys ([43d61f9](https://github.com/stdword/logseq13-missing-commands/commit/43d61f93d562769635d5d802dfe4260f61628070))
132 | * bugs with indentation and fake bullets ([c2b7b7c](https://github.com/stdword/logseq13-missing-commands/commit/c2b7b7c0696f4d635e54920f0bda13033de24bf1))
133 | * change order of the commands ([d241027](https://github.com/stdword/logseq13-missing-commands/commit/d24102749f20a39df1174fcd05e08a084129f667))
134 | * **commands:** editNextBlock: bug with children iterations ([8d873fc](https://github.com/stdword/logseq13-missing-commands/commit/8d873fcca823f7bfe4a359728555169c3520aaea))
135 | * empty & numbering bugs in inserting ([b42f249](https://github.com/stdword/logseq13-missing-commands/commit/b42f24927ae8098a5de7dc642e1c51a70145135e))
136 | * fix properties handling in all transform commands ([04cfb78](https://github.com/stdword/logseq13-missing-commands/commit/04cfb785fa02cae7af7be1dd5caf2823c0287002))
137 | * join via lines (nested) multylines bug ([9404062](https://github.com/stdword/logseq13-missing-commands/commit/9404062c56d6eb9c6705e454180eea5300569c56))
138 | * join via lines command ([bb88c94](https://github.com/stdword/logseq13-missing-commands/commit/bb88c94f57691e9634a9995a2cfdbfe927031cbd))
139 | * join with commas command ([f92439d](https://github.com/stdword/logseq13-missing-commands/commit/f92439d84250ecb69252d2321c54895581295c31))
140 | * magic split initial state bug ([f2cbfdc](https://github.com/stdword/logseq13-missing-commands/commit/f2cbfdc1e179e3bda5e66b592ca4f2557d961e19))
141 | * regexps for unusual numbering when splitting; plugin description ([d4946ad](https://github.com/stdword/logseq13-missing-commands/commit/d4946adc5ff7dd5e840611f37534281aef3a51bf))
142 | * try to avoid empty parent bug ([b3731eb](https://github.com/stdword/logseq13-missing-commands/commit/b3731ebbd3415c0e97930a441b5050824b8d3e3b))
143 | * updating properties and inserting children bug in split commands ([f2d16ee](https://github.com/stdword/logseq13-missing-commands/commit/f2d16eeb5c30b4f4303c1c009b1fd5e322581563))
144 | * **windows:** adapt shortcuts for better ux ([f34d082](https://github.com/stdword/logseq13-missing-commands/commit/f34d0828306038921de65cb56b1b1f44ad4d6a30))
145 |
146 |
147 | ### Features
148 |
149 | * add support for roman numbers & refacatoring ([38f3a31](https://github.com/stdword/logseq13-missing-commands/commit/38f3a31d359d2312e146153ec671dacabe11e868))
150 | * check for properties before joining ([cee24f7](https://github.com/stdword/logseq13-missing-commands/commit/cee24f7afec69ef3a9d8a09abdb311b6901db6c2))
151 | * **commands:** join to paragraphs ([3a3d3ce](https://github.com/stdword/logseq13-missing-commands/commit/3a3d3ce474dfa16dea6f39c10a747eb1fb4f0c05))
152 | * **commands:** join via space, commas & new lines ([447e5c9](https://github.com/stdword/logseq13-missing-commands/commit/447e5c9dfd1c8f91087546145f33c1e8ab487ced))
153 | * **commands:** magic join ([17949cc](https://github.com/stdword/logseq13-missing-commands/commit/17949ccac961fff9e69d5fad7874ba4afc1269f9))
154 | * **commands:** magic split command ([68a2ab2](https://github.com/stdword/logseq13-missing-commands/commit/68a2ab26821b1bc215082a6b8125fdb24c9d5fb5))
155 | * **commands:** make split by paragraphs be nested ([d454644](https://github.com/stdword/logseq13-missing-commands/commit/d4546444c452dc0998349727e0625d4939abbc99))
156 | * **commands:** split by lines ([aee7e2e](https://github.com/stdword/logseq13-missing-commands/commit/aee7e2edca89ff896cadc75286b04439fdec82b4))
157 | * **commands:** wplit by words ([a0052c6](https://github.com/stdword/logseq13-missing-commands/commit/a0052c6e6844c23cdd212b3f9de254fcada6f48d))
158 | * different icons for windows & other platforms ([4a660f5](https://github.com/stdword/logseq13-missing-commands/commit/4a660f5c32668c5a706b9e5a3b429492acf40f80))
159 | * save cursor positions for edirPreviousBlock & editNextBlock commands ([0c22fbf](https://github.com/stdword/logseq13-missing-commands/commit/0c22fbf1d0fa3fd9d89d176da7c4c135d3d6241c))
160 | * support different ordered lists types & remove split by paragraph command ([8b403ad](https://github.com/stdword/logseq13-missing-commands/commit/8b403adbf32d93d8ac44a1e9112f92d0ba62ea33))
161 |
162 | # [1.1.0](https://github.com/stdword/logseq13-missing-commands/compare/v1.0.0...v1.1.0) (2023-12-12)
163 |
164 |
165 | ### Bug Fixes
166 |
167 | * change commands order in keymap ([2234b3b](https://github.com/stdword/logseq13-missing-commands/commit/2234b3bfef25547d892462221d5f50249373ab90))
168 |
169 |
170 | ### Features
171 |
172 | * **commands:** fast outdenting ([cc636ea](https://github.com/stdword/logseq13-missing-commands/commit/cc636eaee9ee67da9ff91d59987f055770cdf3af))
173 | * **commands:** forced block swap ([3b0cf16](https://github.com/stdword/logseq13-missing-commands/commit/3b0cf1636e885b1ec843d90e82e96652fd7e8853))
174 | * **commands:** navigation commands set ([e35705e](https://github.com/stdword/logseq13-missing-commands/commit/e35705eaa1158981a0b0422d356f13e5f64bb9f6))
175 | * **commands:** revrese & shuffle ([ba7aad7](https://github.com/stdword/logseq13-missing-commands/commit/ba7aad70c54461f77cbf2353b12d43b6ddf8e83c))
176 | * **commands:** sort blocks ([d938448](https://github.com/stdword/logseq13-missing-commands/commit/d938448a0f9d24e1584db2cb644a983b4c24947c))
177 | * **commands:** split by paragraphs ([6016952](https://github.com/stdword/logseq13-missing-commands/commit/601695283c6f586ab268f8a33caf2dcc55e096d1))
178 |
179 | # 1.0.0 (2023-12-10)
180 |
181 |
182 | ### Features
183 |
184 | * auto heading command ([cae4edf](https://github.com/stdword/logseq13-missing-commands/commit/cae4edfed768843e0a117b48e353a09c3a10a59b))
185 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Missing Commands, Views & Features
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | [](https://github.com/stdword/logseq13-missing-commands/releases)
19 | [](https://github.com/stdword/logseq13-missing-commands/releases)
20 | [](https://github.com/stdword/logseq13-missing-commands#from-logseq-marketplace-recommended-way)
21 |
22 |
23 |
24 | A part of the
Logseq13 family of plugins
25 |
26 |
27 | ## Summary
28 |
29 | Missing, but helpful _commands_, _views_ & _features_ for [Logseq](https://logseq.com)
30 |
31 | _Designed to be very productive with keyboard_ ❤️
32 |
33 |
34 |
35 | > ℹ️ Some commands has default shortcut and some not.
36 | >
37 | > To find out the shortcut for the particular command (or bind your own) use [this](https://github.com/stdword/logseq13-missing-commands/tree/main?tab=readme-ov-file#how-to-change-default-shortcut-for-the-particular-command) instruction.
38 | >
39 | > Any command could be called from Commands Palette, but consider [this](https://github.com/stdword/logseq13-missing-commands/tree/main?tab=readme-ov-file#any-command-from-the-command-palette-doesnt-work-why) Logseq bug.
40 |
41 |
42 |
43 | > ❗️ Some parts of this plugin heavily rely on Logseq's Document Object Model (DOM) structure. This means that every Logseq update could potentially break specific plugin functions. If you notice anything unusual, please create an issue with details.
44 |
45 |
46 |
47 | > ⚠️ GitHub may need some time to load all demo animations (GIF) in collapsed blocks on this page.
48 |
49 |
50 |
51 | ## 1) ⛓️ Features
52 |
53 |
54 | TAB-trigger on Search
55 | To fill the input with selected search item. Just press the tab key to speed up the input values.
56 | 
57 | |
58 |
59 | Fast access to current page name on Search
60 | Helpfull, when you need to access subpages of the current page. Just press the ← arrow key on empty search input.
61 | 
62 | |
63 |
64 | Go to the block start (end) with double-pressing the «Home» («End») key
65 | Just like in Sublime Text editor. MacOS's ⌘ ← / ⌘ → and Windows's fn ← / fn → are also supported.
66 | Restriction: This feature only works for natural lines of block, which have a «new line» character or «\n». It does not work with lines created due to the size of the layout. In such cases, the only way to proceed is to press Esc to exit edit mode and then use the ← or → arrow key to re-enter it.
67 | 
68 | |
69 |
70 | Spare space between 1-level blocks
71 | Increase the space between 1-level blocks in order to clearly separate them from each other.
72 | Motivation: blocks on the first level represent the most general parts of the information, which usually stand separately: headings, categories, clients, code snippets, links, etc.
73 | 
74 | |
75 |
76 | Edit block on mouse click on page reference or tag with ⌥ (or Alt for Windows) key
77 | Restriction: this feature only works for the first page reference or tag. There is no way to recognize the others if they are the same.
78 | 
79 | |
80 |
81 |
82 |
83 |
84 | ## 2) 🔧 Blocks reordering
85 |
86 |
87 | Toggle auto heading
88 | Without accessing block context menu.
89 | 
90 | |
91 |
92 | Sort / reverse / shuffle blocks
93 | Note: To sort in descending order use sort and then reverse commands.
94 | Note: Sort and reverse commands available via block context menu. Shuffle command only via Command Palette
95 | 
96 | |
97 |
98 |
99 |
100 | ## 3) 🔧 Fast navigation
101 |
102 |
103 | Go to (↑) previous / (↓) next block
104 | Instantly goes to next / prev block. Even with multiline blocks.
105 | Note: cursor position saves from block to block.
106 |
107 | before & after
108 |
109 |
110 |
111 | |
112 |
113 | Go to (↖︎) parent / (↘︎) last child block
114 | Navigating whole block tree throught diagonal — jumping between the parent and the last child block.
115 | 
116 | |
117 |
118 | Go to |↑| previous / |↓| next sibling block
119 | Jumping between sibling blocks only.
120 | Note: cursor position saves from block to block.
121 | Note: we cannot leave current parent.
122 | Note: the difference from prevous command is skipping all child blocks.
123 | 
124 | |
125 |
126 |
127 |
128 | ## 4) 🔧 Blocks movements
129 |
130 |
131 | Outdent (⇤) children of the block
132 | Perform outdent (indent to the left) for every child of the particular block.
133 | Note: standard Logseq commands ⇧⇥ can acheive this, but it required to select all child blocks manually one by one before using it.
134 | 
135 | |
136 |
137 | Move block (⤒) on top / (⤓) on bottom of siblings
138 | Instantly makes block the first (or the last) child of the parent.
139 | Note: standard Logseq commands ⌘⇧↑ or ⌘⇧↓ can acheive this, but via one step at a time.
140 | 
141 | |
142 |
143 |
144 |
145 | ## 5) 🔧 Splitting & Joining blocks
146 |
147 |
148 | Magic Split & Magic Join
149 | Search block content for ordered / unordered lists and split it to corresponding blocks structure.
150 | Note: supported numeration style: 1. 1) (1) 1.2) for arabic & roman numbers and letters from alphabet.
151 | Note: supported bullets style: - + * .
152 |
153 | Split & Join
154 |
155 |
156 |
157 | |
158 |
159 | Split by lines / Join via new lines
160 | Simple command to pick out each line of block to separate block (and vica-versa).
161 | Note: There are two types of join command: with respect to block structure and without it.
162 |
163 | Split & Join
164 |
165 |
166 |
167 | |
168 |
169 | Split by words / Join via spaces
170 | Get all words from the text and place it at the separate blocks (and vice-versa).
171 | Note: Words could contain letters, ' , _ & - characters.
172 |
173 | Split & Join
174 |
175 |
176 |
177 | |
178 |
179 | Split by commas or semicolons / Join via commas
180 | Split the text by commas (or semicolons with the separate command).
181 | 
182 |
183 | Join separate blocks via commas.
184 | 
185 |
186 | Note: Joining can respect the root node with colon «:».
187 | 
188 | |
189 |
190 | Split / Join sentences
191 | Split paragraph of text by sentences (one block = one sentence). And join the blocks to single paragraph.
192 | Note: Split removes the dots at the end. Join returns the dots back.
193 |
194 | Split & Join
195 |
196 |
197 |
198 | |
199 |
200 |
201 |
202 | ## 6) 🔧 Updating blocks
203 |
204 |
205 |
206 | Magic Bold / Italics / Underlne / Hightlight / Strikethrough / Code / Reference / Tag / Quotes
207 | Apply various formatting in a smart way: auto-select whole words, recognize Logseq special syntax, smart undo formatting, and work out of editing mode.
208 | To use it in edit mode:
209 | - Go to the Keymap (
g s ) → Formatting section and replace standard Logseq commands (Bold, Highlight, Italics, Strikethrough) with magic ones.
210 | - Bind Magic underline, Magic `code`, Magic [[reference]], Magic #tag and Magic "quotes" commands to shortcuts of your choice (e.g. ⌘U, ⌥~, etc.).
211 |
212 | Note: command uses «_» for italics to prevent this cases.
213 | 
214 | 
215 | 
216 | |
217 |
218 | Remove new lines
219 | Remove all «new line» characters from text. Helpful for work with OCR texts.
220 | Note: command adds spaces when it's necessary.
221 | 
222 | |
223 |
224 | Trim lines punctuation ". , ;"
225 | Remove any of «. , ;» characters from the end of every line of text.
226 | Note: command removes only one punctuation character.
227 |
228 | 
229 | |
230 |
231 | Lower / upper / title letters case
232 | Note: title case command has two variations — title words and title sentences.
233 | 
234 | |
235 |
236 | Remove HTML tags
237 | Remove all HTML tags from the block, leaving only the text content.
238 | Note: there is no exceptions — everything between «<» and «>» will be removed.
239 | 
240 | |
241 |
242 | Parse YouTube timestamps
243 | Transform copied from YouTube timestamps to Logseq format.
244 | 
245 | |
246 |
247 |
248 |
249 |
250 | ## 7) 🔭 Views
251 |
252 |
253 | Hide references started with «.»
254 | Hide any page and tag references that start with the dot: «.», assuming that these are special reserved references that do not need to be shown.
255 | Note: there are two ways of hiding:
256 |
257 | Hide by wrapping to «…» only & Hide completely and show on block hover
258 |
259 |
260 |
261 | |
262 |
263 | Tabular view
264 | Use the #.tabular reference in a block to apply a Tana-like tabular view for all its children.
265 | 
266 |
267 | Note: it could be nested — #.tabular inside another #.tabular . However, only two-level depth is supported.
268 | 
269 |
270 | Use the #.tabular0 reference in another tabular row to skip the immediate children.
271 | 
272 |
273 | Use the #.tabular0 reference to hide heading block.
274 | 
275 |
276 | FAQ: How to return back double square brackets for page references in the left column?
277 | Add following code to custom.css :
278 |
279 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .block-content-wrapper .page-reference .bracket {
280 | display: inline-flex;
281 | }
282 |
283 | |
284 |
285 | Columns view
286 | Use the #.columns reference to organize child blocks to columns of the same width.
287 | Note: 1 column = 1 block.
288 |
289 | Use the #.columns-N reference to organize child blocks to N columns of the same width, where N = 2…6.
290 | Note: 1 column = 1 or more blocks.
291 |
292 | Use the #.columns-fit reference to organize child blocks to columns with different width (based on content).
293 | Note: 1 column = 1 block.
294 |
295 | 
296 | |
297 |
298 | Gallery view
299 | Use the #.gallery reference to organize child blocks containing images to gallery.
300 | Note: image sizes automatically fills whole space for width. There is only one row of images.
301 | 
302 |
303 | Use the #.gallery-wN reference to organize child blocks containing images as fixed-width (based on N) images.
304 | Note: there can be multiple rows of images.
305 | 
306 |
307 | Use the #.gallery-hN reference to organize child blocks containing images as fixed-height (based on N) images.
308 | Note: there can be multiple rows of images.
309 | 
310 | |
311 |
312 | Border view
313 | Use the #.border & #.border-child references to organize borders around the blocks.
314 | Note: these references can be combined.
315 | 
316 | |
317 |
318 |
319 |
320 |
321 | ## If you ❤️ what I'm doing — consider to support my work
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 | ## Installation
330 | ### From Logseq Marketplace (recommended way):
331 |
332 |
333 | - Click «...» and open the «Plugins» section (or press `t p`)
334 | - Click on the «Marketplace»
335 | - On the «Plugins» tab search for «Missing Commands & Views» plugin and click install
336 | - If you want to change default shortcuts commands — go to «Keymap» (`g s`)
337 |
338 | ### Manual way (in case of any troubles with recommended way)
339 | 1. *In Logseq*: Enable «Developer mode» in «...» → «Settings» → «Advanced»
340 | 2. Download the latest plugin release in a raw .zip archive from [here](https://github.com/stdword/logseq13-missing-commands/releases)
341 | 4. Unzip it
342 | 5. *In Logseq*: Go to the «...» → «Plugins», click «Load unpacked plugin» and point to the unzipped plugin folder
343 | 6. ⚠️ The important point here is: every new plugin release should be updated manually
344 |
345 |
346 | ## FAQ
347 | ### Any command from the _Command Palette_ doesn't work! Why?
348 | The reason is the bug in Logseq's interaction with the _Command Palette_:
349 | - If you select the command **with your mouse**, it cannot detect the currently selected blocks or the currently editing block.
350 | - If you select the command **with your keyboard** (using Enter), it cannot detect the currently editing block, but it can detect the currently selected blocks.
351 | - If you want to execute a particular command for the currently editing block — [bind a shortcut](https://github.com/stdword/logseq13-missing-commands/tree/main?tab=readme-ov-file#how-to-change-default-shortcut-for-the-particular-command) to it.
352 |
353 | ### How to change default shortcut for the particular command?
354 | 1. Open «Settings» → «Keymap» (or press `g s`).
355 | 2. Copy this emoji «🪚» (for Windows use «🔪») and insert it to search input.
356 | 3. Change any shortcut you want
357 |
358 | ### Why I cannot revert the result of particular command with one _Undo_ action?
359 | This is a restriction of the Logseq API: there is no way to execute complex commands in a single _Undo_. Therefore, the plugin attempts (when it makes sense) to minimize the count of _Undo_ actions by removing the entire block tree instead of removing each block independently.
360 |
361 | ### Why there is strange «ø» charactear appears sometimes during _Undo_ command?
362 | 
363 |
364 | The reason is [this](https://github.com/logseq/logseq/issues/10729) bug in Logseq plugin API. The plugin uses «ø» character intentionally as a workaround for this issue. When the bug is resolved, this workaround will no longer be necessary.
365 |
366 |
367 | ## Additional helpful plugins with the same vibe
368 | - [Shallow Copy](https://github.com/MateuszMyalski/logseq-plugin-shallow-copy) by `MateuszMyalski`
369 | - [Side Block](https://github.com/YU000jp/logseq-plugin-side-block) by `YU000jp`
370 | - [Custom Files](https://github.com/cannibalox/logseq-custom-files) by `cannibalox`
371 | - [LogTools](https://github.com/cannibalox/logtools) by `cannibalox`
372 | - [Awesome Content](https://github.com/yoyurec/logseq-awesome-content) by `yoyurec`
373 |
374 |
375 | ## Credits
376 | Some parts of this plugin based on reviewed and refined works of another authors:
377 |
378 | - Auto heading based on [Another Embed](https://github.com/sethyuan/logseq-plugin-another-embed) by `sethyuan`
379 | - Tabular view based on _«Tabular Journals»_ by `nmartin84` (there is no such repo anymore)
380 | - Gallery, box & columns views based on [LogTools](https://github.com/cannibalox/logtools) by `cannibalox`
381 | - Columns view based on [Awesome Content](https://github.com/yoyurec/logseq-awesome-content) by `yoyurec`
382 | - Magic formatting based on [Obsidian: Smarter MD Hotkeys](https://github.com/chrisgrieser/obsidian-smarter-md-hotkeys) by `chrisgrieser`
383 |
384 | + Icon created by Nuricon
385 |
386 |
387 | ## License
388 | [MIT License](https://github.com/stdword/logseq13-missing-commands/blob/main/LICENSE)
389 |
--------------------------------------------------------------------------------
/assets/coffee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdword/logseq13-missing-commands/22429b3c9031e5ad1629b95262e85069100de0a5/assets/coffee.png
--------------------------------------------------------------------------------
/assets/logseq-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdword/logseq13-missing-commands/22429b3c9031e5ad1629b95262e85069100de0a5/assets/logseq-logo.png
--------------------------------------------------------------------------------
/assets/logseq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdword/logseq13-missing-commands/22429b3c9031e5ad1629b95262e85069100de0a5/assets/logseq.png
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stdword/logseq13-missing-commands/22429b3c9031e5ad1629b95262e85069100de0a5/icon.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | logseq13-missing-commands
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logseq13-missing-commands",
3 | "version": "1.8.3",
4 | "description": "Sort blocks, TAB-trigger on search, split by sentences, parse text structure, blocks navigation, etc.",
5 | "author": "stdword",
6 | "repository": "https://github.com/stdword/logseq13-missing-commands.git",
7 | "license": "MIT",
8 | "logseq": {
9 | "id": "logseq13-missing-commands",
10 | "title": "Missing Commands & Views",
11 | "icon": "./icon.png",
12 | "main": "./dist/index.html"
13 | },
14 | "scripts": {
15 | "preinstall": "npx only-allow pnpm",
16 | "clean": "rm -r ./dist/* || true",
17 | "dev": "vite",
18 | "build": "tsc && vite build --mode=dev",
19 | "prod": "tsc && pnpm run clean && vite build"
20 | },
21 | "dependencies": {
22 | "@logseq/libs": "^0.0.15",
23 | "markdown-it": "^14.0.0"
24 | },
25 | "devDependencies": {
26 | "@semantic-release/changelog": "^6.0.3",
27 | "@semantic-release/exec": "^6.0.3",
28 | "@semantic-release/git": "^10.0.1",
29 | "@types/markdown-it": "^13.0.7",
30 | "@types/node": "^20.10.4",
31 | "conventional-changelog-conventionalcommits": "^7.0.2",
32 | "cz-conventional-changelog": "^3.3.0",
33 | "semantic-release": "^22.0.10",
34 | "typescript": "^5.3.3",
35 | "vite": "^5.0.7",
36 | "vite-plugin-logseq": "^1.1.2"
37 | },
38 | "config": {
39 | "commitizen": {
40 | "path": "cz-conventional-changelog"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | var PLUGIN_NAME = 'logseq13-missing-commands'
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/app.ts:
--------------------------------------------------------------------------------
1 | import { BlockEntity, SettingSchemaDesc } from '@logseq/libs/dist/LSPlugin.user'
2 |
3 | import {
4 | ICON,
5 |
6 | toggleAutoHeadingCommand,
7 | outdentChildrenCommand,
8 | editNextBlockCommand, editPreviousBlockCommand,
9 | parentBlockCommand, lastChildBlockCommand,
10 | moveToBottomOfSiblingsCommand, moveToTopOfSiblingsCommand,
11 | nextSiblingBlockCommand, previousSiblingBlockCommand,
12 | reverseBlocksCommand, shuffleBlocksCommand, sortBlocksCommand,
13 |
14 | joinBlocksCommand, splitBlocksCommand, updateBlocksCommand,
15 |
16 | magicJoinCommand, magicSplit,
17 | joinAsSentences_Map, joinViaCommas_Attach, joinViaSpaces_Attach,
18 | joinViaNewLines_Attach, joinViaNewLines_Map,
19 |
20 | splitByLines, splitBySentences, splitByWords,
21 | removeNewLines, removeHTML, parseYoutubeTimestamp,
22 | lowerCase, upperCase, titleCaseWords, titleCaseSentences, splitByCommas, trimLinePunctuation, splitBySemicolons,
23 | } from './commands'
24 | import { MARKUP, magicQuotes, magicWrap } from './commands/magic_markup'
25 | import { improveCursorMovementFeature, improveMouseRefClick, improveSearchFeature, spareBlocksFeature } from './features'
26 | import { borderView, columnsView, galleryView, hideDotRefs, tabularView } from './views'
27 | import { getChosenBlocks, p, scrollToBlock } from './utils'
28 |
29 |
30 | const DEV = process.env.NODE_ENV === 'development'
31 |
32 | const defaultSpareBlocksSpace = 20
33 | const defaultMagicQuotes = '""'
34 |
35 | const settingsSchema: SettingSchemaDesc[] = [
36 | {
37 | key: 'headingCommands',
38 | type: 'heading',
39 | title: '🔧 Commands',
40 | description: `
41 | See detailed description about every command in
42 |
43 | documentation.
44 |
45 | To change shortcut for the particular command:
46 |
47 | - Open «Settings» → «Keymap».
48 | - Copy this emoji «🪚» (for Windows use «🔪») and insert it to search input.
49 | - Change any shortcut you want
50 |
51 | `.trim(),
52 | default: null,
53 | },
54 | {
55 | key: 'magicQuotes',
56 | title: 'Use this quotes for «Magic quotes» command',
57 | description: `
58 | Default is ${defaultMagicQuotes}
.
59 | `.trim(),
60 | type: 'string',
61 | default: defaultMagicQuotes,
62 | },
63 | {
64 | key: 'headingFeatures',
65 | type: 'heading',
66 | title: '⛓️ Features',
67 | description: `
68 | See detailed description about every feature in
69 |
70 | documentation.
71 |
72 | `.trim(),
73 | default: null,
74 | },
75 | {
76 | key: 'enableHomeEnd',
77 | title: 'Enable improved «Home» / «End» keys processing?',
78 | description: `
79 | 1. Double-press the Home
/ End
key (in edit mode) to go to the block start / end.
80 | 2. MacOS's ⌘ ←
/ ⌘ →
and Windows's fn ←
/ fn →
are also supported.
81 | Restriction: This feature only works for natural lines of block, which have a «new line» character («\\n»). It does not work with lines created due to the size of the layout. In such cases, the only way to proceed is to press Esc
to exit edit mode and then use the ←
or →
arrow key to re-enter it.
82 | `.trim(),
83 | type: 'enum',
84 | enumPicker: 'radio',
85 | enumChoices: ['Yes', 'No'],
86 | default: 'Yes',
87 | },
88 | {
89 | key: 'enableSearchImprovements',
90 | title: 'Enable improved keys processing on Search?',
91 | description: `
92 | 1. Press Tab
to fill the input with selected search item.
93 | 2. Press ←
arrow (with empty input) to fill the input with the current page name.
94 | `.trim(),
95 | type: 'enum',
96 | enumPicker: 'radio',
97 | enumChoices: ['Yes', 'No'],
98 | default: 'Yes',
99 | },
100 | {
101 | key: 'enableMouseRefClick',
102 | title: 'Enable block editing on mouse click to [[reference]]?',
103 | description: `
104 | With ⌥
(or Alt
for Windows) pressed.
105 | Restriction: this feature only works for the first page reference or tag. There is no way to recognize the others if they are the same.
106 | `.trim(),
107 | type: 'enum',
108 | enumPicker: 'radio',
109 | enumChoices: ['Yes', 'No'],
110 | default: 'Yes',
111 | },
112 | {
113 | key: 'spareBlocksSpace',
114 | title: 'Spare space between 1-level blocks',
115 | description: `
116 | Increase the space to clearly separate blocks from each other.
117 | Motivation: blocks on the 1-level represent the most general parts of the information, which usually stand separately: headings, categories, clients, code snippets, links, etc.
118 | In pixels: default is ${defaultSpareBlocksSpace}
. Set to 0
to disable.
119 | `.trim(),
120 | type: 'number',
121 | default: defaultSpareBlocksSpace,
122 | },
123 | {
124 | key: 'headingViews',
125 | type: 'heading',
126 | title: '🔭 Views',
127 | description: `
128 | See detailed description about every view in
129 |
130 | documentation.
131 |
132 | `.trim(),
133 | default: null,
134 | },
135 | {
136 | key: 'hideDotRefs',
137 | title: 'Hide references started with «.»?',
138 | description: `
139 | Hide any page and tag references that start with the dot: «.», assuming that these are special reserved references that do not need to be shown.
140 | `.trim(),
141 | type: 'enum',
142 | enumPicker: 'select',
143 | enumChoices: ['Hide completely and show on block hover', 'Hide by wrapping to «…» only', 'No'],
144 | default: 'Hide by wrapping to «…» only',
145 | },
146 | {
147 | key: 'enableTabularView',
148 | title: 'Enable tabular view?',
149 | description: `
150 |
151 | - Use the
#.tabular
reference in a block to apply a Tana-like tabular view for all its children.
152 | It could be subsequent: #.tabular
inside another #.tabular
. However, only two subsequent levels are supported.
153 | - Use the
#.tabular0
reference in another tabular row to skip the immediate children.
154 | - Use the
#.tabular0
reference to hide heading block.
155 |
156 | `.trim(),
157 | type: 'enum',
158 | enumPicker: 'radio',
159 | enumChoices: ['Yes', 'No'],
160 | default: 'Yes',
161 | },
162 | {
163 | key: 'enableColumnsView',
164 | title: 'Enable columns view?',
165 | description: `
166 |
167 | - Use the
#.columns
reference to organize child blocks
168 | to columns of the same width.
169 | 1 column = 1 block.
170 | - Use the
#.columns-N
reference to organize child blocks
171 | to N columns of the same width, where N = 2…6.
172 | 1 column = 1 or more blocks.
173 | - Use the
#.columns-fit
reference to organize child blocks
174 | to columns with different width (based on content).
175 | 1 column = 1 block.
176 |
177 | `.trim(),
178 | type: 'enum',
179 | enumPicker: 'radio',
180 | enumChoices: ['Yes', 'No'],
181 | default: 'Yes',
182 | },
183 | {
184 | key: 'enableGalleryView',
185 | title: 'Enable gallery view?',
186 | description: `
187 |
188 | - Use the
#.gallery
reference to organize child blocks containing images to gallery.
189 | Image sizes automatically fills whole space for width. There is only one row of images.
190 | - Use the
#.gallery-wN
reference to organize child blocks containing images as fixed-width (based on N) images.
191 | There can be multiple rows of images.
192 | - Use the
#.gallery-hN
reference to organize child blocks containing images as fixed-height (based on N) images.
193 | There can be multiple rows of images.
194 |
195 | `.trim(),
196 | type: 'enum',
197 | enumPicker: 'radio',
198 | enumChoices: ['Yes', 'No'],
199 | default: 'Yes',
200 | },
201 | {
202 | key: 'enableBorderView',
203 | title: 'Enable border view?',
204 | description: `
205 | Use the #.border
& #.border-child
references to organize borders around the blocks.
206 | These references can be combined.
207 | `.trim(),
208 | type: 'enum',
209 | enumPicker: 'radio',
210 | enumChoices: ['Yes', 'No'],
211 | default: 'Yes',
212 | },
213 | ]
214 | const settings_: any = settingsSchema.reduce((r, v) => ({ ...r, [v.key]: v}), {})
215 |
216 |
217 | async function init() {
218 | if (DEV) {
219 | logseq.UI.showMsg(
220 | `[:div [:b "Missing Commands"] [:p "HMR"] ]`,
221 | 'info',
222 | {timeout: 3000},
223 | )
224 | }
225 |
226 | logseq.useSettingsSchema(settingsSchema)
227 |
228 | console.info(p`Loaded`)
229 | }
230 | async function postInit(settings) {
231 | await onAppSettingsChanged(settings, undefined)
232 | }
233 | async function onAppSettingsChanged(current, old) {
234 | if (current.magicQuotes !== old?.magicQuotes) {
235 | if (current.magicQuotes.length === 0)
236 | current.magicQuotes = defaultMagicQuotes
237 | if (current.magicQuotes.length === 1)
238 | current.magicQuotes += current.magicQuotes
239 | if (current.magicQuotes.length > 2)
240 | current.magicQuotes = current.magicQuotes.slice(0, 2)
241 |
242 | if (current.magicQuotes !== logseq.settings!.magicQuotes)
243 | logseq.settings!.magicQuotes = current.magicQuotes
244 | }
245 |
246 | if (!old || current.enableHomeEnd !== old.enableHomeEnd) {
247 | improveCursorMovementFeature(false)
248 | if (current.enableHomeEnd === 'Yes')
249 | improveCursorMovementFeature(true)
250 | }
251 |
252 | if (!old || current.enableSearchImprovements !== old.enableSearchImprovements) {
253 | improveSearchFeature(false)
254 | if (current.enableSearchImprovements === 'Yes')
255 | improveSearchFeature(true)
256 | }
257 |
258 | if (!old || current.enableMouseRefClick !== old.enableMouseRefClick) {
259 | improveMouseRefClick(false)
260 | if (current.enableMouseRefClick === 'Yes')
261 | improveMouseRefClick(true)
262 | }
263 |
264 | if (!old || current.spareBlocksSpace !== old.spareBlocksSpace)
265 | spareBlocksFeature(current.spareBlocksSpace)
266 |
267 | if (!old || current.hideDotRefs !== old.hideDotRefs)
268 | if (current.hideDotRefs === 'No')
269 | hideDotRefs(false)
270 | else if (current.hideDotRefs === settings_.hideDotRefs.default)
271 | hideDotRefs(true, false) // wrap only
272 | else
273 | hideDotRefs(true, true) // wrap & hide
274 |
275 | if (!old || current.enableTabularView !== old.enableTabularView)
276 | tabularView(current.enableTabularView === 'Yes')
277 |
278 | if (!old || current.enableColumnsView !== old.enableColumnsView)
279 | columnsView(current.enableColumnsView === 'Yes')
280 |
281 | if (!old || current.enableGalleryView !== old.enableGalleryView)
282 | galleryView(current.enableGalleryView === 'Yes')
283 |
284 | if (!old || current.enableBorderView !== old.enableBorderView)
285 | borderView(current.enableBorderView === 'Yes')
286 | }
287 |
288 |
289 | async function main() {
290 | await init()
291 |
292 | const settings = logseq.settings!
293 | const settingsOff = logseq.onSettingsChanged(onAppSettingsChanged)
294 |
295 | logseq.beforeunload(async () => {
296 | improveCursorMovementFeature(false)
297 | improveSearchFeature(false)
298 | settingsOff()
299 | })
300 |
301 |
302 | function setting_storeChildBlocksIn() {
303 | return false // the last one
304 | }
305 |
306 |
307 | // Decoration
308 | logseq.App.registerCommandPalette({
309 | label: ICON + ' Toggle auto heading', key: 'mc-1-auto-heading',
310 | keybinding: {mac: 'mod+1', binding: 'ctrl+1', mode: 'global'},
311 | }, (e) => toggleAutoHeadingCommand({togglingBasedOnFirstBlock: true}) )
312 |
313 |
314 | // Splitting
315 | logseq.App.registerCommandPalette({
316 | label: ICON + ' Split by words', key: 'mc-5-split-1-by-words',
317 | // @ts-expect-error
318 | keybinding: {},
319 | }, (e) => splitBlocksCommand(splitByWords, setting_storeChildBlocksIn()))
320 | logseq.App.registerCommandPalette({
321 | label: ICON + ' Split by words (with nested)', key: 'mc-5-split-2-by-words-nested',
322 | // @ts-expect-error
323 | keybinding: {},
324 | }, (e) => splitBlocksCommand(splitByWords, setting_storeChildBlocksIn(), true))
325 |
326 | logseq.App.registerCommandPalette({
327 | label: ICON + ' Split by lines', key: 'mc-5-split-3-by-lines',
328 | // @ts-expect-error
329 | keybinding: {},
330 | }, (e) => splitBlocksCommand(splitByLines, setting_storeChildBlocksIn()))
331 | logseq.App.registerCommandPalette({
332 | label: ICON + ' Split by lines (with nested)', key: 'mc-5-split-4-by-lines-nested',
333 | // @ts-expect-error
334 | keybinding: {},
335 | }, (e) => splitBlocksCommand(
336 | splitByLines, setting_storeChildBlocksIn(), true))
337 |
338 | logseq.App.registerCommandPalette({
339 | label: ICON + ' Magic Split', key: 'mc-5-split-5-magic',
340 | // @ts-expect-error
341 | keybinding: {},
342 | }, (e) => splitBlocksCommand(magicSplit, setting_storeChildBlocksIn(), false) )
343 | logseq.App.registerCommandPalette({
344 | label: ICON + ' Magic Split (with nested)', key: 'mc-5-split-6-magic-nested',
345 | // @ts-expect-error
346 | keybinding: {},
347 | }, (e) => splitBlocksCommand(magicSplit, setting_storeChildBlocksIn(), true) )
348 |
349 | logseq.App.registerCommandPalette({
350 | label: ICON + ' Split by sentences', key: 'mc-5-split-7-by-sentences',
351 | // @ts-expect-error
352 | keybinding: {},
353 | }, (e) => splitBlocksCommand(splitBySentences, setting_storeChildBlocksIn()))
354 | logseq.App.registerCommandPalette({
355 | label: ICON + ' Split by sentences (with nested)', key: 'mc-5-split-8-by-sentences-nested',
356 | // @ts-expect-error
357 | keybinding: {},
358 | }, (e) => splitBlocksCommand(
359 | splitBySentences, setting_storeChildBlocksIn(), true))
360 |
361 | logseq.App.registerCommandPalette({
362 | label: ICON + ' Split by commas', key: 'mc-5-split-9-by-commas',
363 | // @ts-expect-error
364 | keybinding: {},
365 | }, (e) => splitBlocksCommand(splitByCommas, setting_storeChildBlocksIn()))
366 | logseq.App.registerCommandPalette({
367 | label: ICON + ' Split by commas (with nested)', key: 'mc-5-split-10-by-commas',
368 | // @ts-expect-error
369 | keybinding: {},
370 | }, (e) => splitBlocksCommand(splitByCommas, setting_storeChildBlocksIn(), true))
371 |
372 | logseq.App.registerCommandPalette({
373 | label: ICON + ' Split by semicolons', key: 'mc-5-split-11-by-semicolons',
374 | // @ts-expect-error
375 | keybinding: {},
376 | }, (e) => splitBlocksCommand(splitBySemicolons, setting_storeChildBlocksIn()))
377 | logseq.App.registerCommandPalette({
378 | label: ICON + ' Split by semicolons (with nested)', key: 'mc-5-split-12-by-semicolons-nested',
379 | // @ts-expect-error
380 | keybinding: {},
381 | }, (e) => splitBlocksCommand(splitBySemicolons, setting_storeChildBlocksIn(), true))
382 |
383 |
384 | // Joining
385 | logseq.App.registerCommandPalette({
386 | label: ICON + ' Join via spaces', key: 'mc-6-join-1-spaces',
387 | // @ts-expect-error
388 | keybinding: {},
389 | }, (e) => joinBlocksCommand(false, joinViaSpaces_Attach))
390 |
391 | logseq.App.registerCommandPalette({
392 | label: ICON + ' Join together via commas (with respect to root block)', key: 'mc-6-join-2-commas',
393 | // @ts-expect-error
394 | keybinding: {},
395 | }, (e) => joinBlocksCommand(false, joinViaCommas_Attach))
396 | logseq.App.registerCommandPalette({
397 | label: ICON + ' Join independently via commas (with respect to root block)', key: 'mc-6-join-3-commas-independently',
398 | // @ts-expect-error
399 | keybinding: {},
400 | }, (e) => joinBlocksCommand(true, joinViaCommas_Attach))
401 |
402 | logseq.App.registerCommandPalette({
403 | label: ICON + ' Join via new lines', key: 'mc-6-join-4-lines',
404 | // @ts-expect-error
405 | keybinding: {},
406 | }, (e) => joinBlocksCommand(false, joinViaNewLines_Attach))
407 | logseq.App.registerCommandPalette({
408 | label: ICON + ' Join via new lines (keep nested structure)', key: 'mc-6-join-5-lines-nested',
409 | // @ts-expect-error
410 | keybinding: {},
411 | }, (e) => joinBlocksCommand(false, joinViaNewLines_Attach, joinViaNewLines_Map))
412 |
413 | logseq.App.registerCommandPalette({
414 | label: ICON + ' Magic Join', key: 'mc-6-join-6-magic',
415 | // @ts-expect-error
416 | keybinding: {},
417 | }, (e) => magicJoinCommand(false))
418 | logseq.App.registerCommandPalette({
419 | label: ICON + ' Magic Join (independently)', key: 'mc-6-join-7-magic-independently',
420 | // @ts-expect-error
421 | keybinding: {},
422 | }, (e) => magicJoinCommand(true))
423 |
424 | logseq.App.registerCommandPalette({
425 | label: ICON + ' Join as sentences', key: 'mc-6-join-8-sentences',
426 | // @ts-expect-error
427 | keybinding: {},
428 | }, (e) => joinBlocksCommand(false, joinViaSpaces_Attach, joinAsSentences_Map, {shouldHandleSingleBlock: true}))
429 | logseq.App.registerCommandPalette({
430 | label: ICON + ' Join as sentences (independently)', key: 'mc-6-join-9-sentences-independently',
431 | // @ts-expect-error
432 | keybinding: {},
433 | }, (e) => joinBlocksCommand(true, joinViaSpaces_Attach, joinAsSentences_Map, {shouldHandleSingleBlock: true}))
434 |
435 |
436 | // Updates
437 | logseq.App.registerCommandPalette({
438 | label: ICON + ' Remove new lines', key: 'mc-7-update-1-remove-new-lines',
439 | // @ts-expect-error
440 | keybinding: {},
441 | }, (e) => updateBlocksCommand(removeNewLines))
442 | logseq.App.registerCommandPalette({
443 | label: ICON + ' Remove new lines (with nested)', key: 'mc-7-update-2-remove-new-lines-nested',
444 | // @ts-expect-error
445 | keybinding: {},
446 | }, (e) => updateBlocksCommand(removeNewLines, true))
447 |
448 | logseq.App.registerCommandPalette({
449 | label: ICON + ' Lower case', key: 'mc-7-update-3-lower-case',
450 | // @ts-expect-error
451 | keybinding: {},
452 | }, (e) => updateBlocksCommand(lowerCase))
453 | logseq.App.registerCommandPalette({
454 | label: ICON + ' Lower case (with nested)', key: 'mc-7-update-4-lower-case-nested',
455 | // @ts-expect-error
456 | keybinding: {},
457 | }, (e) => updateBlocksCommand(lowerCase, true))
458 |
459 | logseq.App.registerCommandPalette({
460 | label: ICON + ' Upper case', key: 'mc-7-update-5-upper-case',
461 | // @ts-expect-error
462 | keybinding: {},
463 | }, (e) => updateBlocksCommand(upperCase))
464 | logseq.App.registerCommandPalette({
465 | label: ICON + ' Upper case (with nested)', key: 'mc-7-update-6-upper-case-nested',
466 | // @ts-expect-error
467 | keybinding: {},
468 | }, (e) => updateBlocksCommand(upperCase, true))
469 |
470 | logseq.App.registerCommandPalette({
471 | label: ICON + ' Title case words', key: 'mc-7-update-7-title-case-words',
472 | // @ts-expect-error
473 | keybinding: {},
474 | }, (e) => updateBlocksCommand(titleCaseWords))
475 | logseq.App.registerCommandPalette({
476 | label: ICON + ' Title case words (with nested)', key: 'mc-7-update-8-title-case-words-nested',
477 | // @ts-expect-error
478 | keybinding: {},
479 | }, (e) => updateBlocksCommand(titleCaseWords, true))
480 |
481 | logseq.App.registerCommandPalette({
482 | label: ICON + ' Title case sentences', key: 'mc-7-update-9-title-case-sentences',
483 | // @ts-expect-error
484 | keybinding: {},
485 | }, (e) => updateBlocksCommand(titleCaseSentences))
486 | logseq.App.registerCommandPalette({
487 | label: ICON + ' Title case sentences (with nested)', key: 'mc-7-update-10-title-case-sentences-nested',
488 | // @ts-expect-error
489 | keybinding: {},
490 | }, (e) => updateBlocksCommand(titleCaseSentences, true))
491 |
492 | logseq.App.registerCommandPalette({
493 | label: ICON + ' Remove HTML tags', key: 'mc-7-update-11-remove-html',
494 | // @ts-expect-error
495 | keybinding: {},
496 | }, (e) => updateBlocksCommand(removeHTML))
497 | logseq.App.registerCommandPalette({
498 | label: ICON + ' Remove HTML tags (with nested)', key: 'mc-7-update-12-remove-html-nested',
499 | // @ts-expect-error
500 | keybinding: {},
501 | }, (e) => updateBlocksCommand(removeHTML, true))
502 |
503 | logseq.App.registerCommandPalette({
504 | label: ICON + ' Parse YouTube timestamps (with nested)', key: 'mc-7-update-13-parse-yt-ts',
505 | // @ts-expect-error
506 | keybinding: {},
507 | }, (e) => updateBlocksCommand(parseYoutubeTimestamp, true))
508 |
509 |
510 | // Formatting
511 | logseq.App.registerCommandPalette({
512 | label: ICON + ' Magic **bold**', key: 'mc-7-update-14-magic-bold',
513 | // @ts-expect-error
514 | keybinding: {},
515 | }, (e) => updateBlocksCommand(
516 | (content, level, block, parent) => {return magicWrap(block, content, MARKUP.bold)},
517 | false, false))
518 | logseq.App.registerCommandPalette({
519 | label: ICON + ' Magic _italics_', key: 'mc-7-update-15-magic-italics',
520 | // @ts-expect-error
521 | keybinding: {},
522 | }, (e) => updateBlocksCommand(
523 | (content, level, block, parent) => {return magicWrap(block, content, MARKUP.italics)},
524 | false, false))
525 | logseq.App.registerCommandPalette({
526 | label: ICON + ' Magic ~~strikethrough~~', key: 'mc-7-update-16-magic-strikethrough',
527 | // @ts-expect-error
528 | keybinding: {},
529 | }, (e) => updateBlocksCommand(
530 | (content, level, block, parent) => {return magicWrap(block, content, MARKUP.strikethrough)},
531 | false, false))
532 | logseq.App.registerCommandPalette({
533 | label: ICON + ' Magic ==highlight==', key: 'mc-7-update-17-magic-highlight',
534 | // @ts-expect-error
535 | keybinding: {},
536 | }, (e) => updateBlocksCommand(
537 | (content, level, block, parent) => {return magicWrap(block, content, MARKUP.highlight)},
538 | false, false))
539 | logseq.App.registerCommandPalette({
540 | label: ICON + ' Magic underline', key: 'mc-7-update-18-magic-underline',
541 | // @ts-expect-error
542 | keybinding: {},
543 | }, (e) => updateBlocksCommand(
544 | (content, level, block, parent) => {return magicWrap(block, content, MARKUP.underline)},
545 | false, false))
546 | logseq.App.registerCommandPalette({
547 | label: ICON + ' Magic `code`', key: 'mc-7-update-19-magic-code',
548 | // @ts-expect-error
549 | keybinding: {},
550 | }, (e) => updateBlocksCommand(
551 | (content, level, block, parent) => {return magicWrap(block, content, MARKUP.code)},
552 | false, false))
553 | logseq.App.registerCommandPalette({
554 | label: ICON + ' Magic [[reference]]', key: 'mc-7-update-20-magic-ref',
555 | // @ts-expect-error
556 | keybinding: {},
557 | }, (e) => updateBlocksCommand(
558 | (content, level, block, parent) => {return magicWrap(block, content, MARKUP.ref)},
559 | false, false))
560 | logseq.App.registerCommandPalette({
561 | label: ICON + ' Magic #tag', key: 'mc-7-update-21-magic-tag',
562 | // @ts-expect-error
563 | keybinding: {},
564 | }, (e) => updateBlocksCommand(
565 | (content, level, block, parent) => {return magicWrap(block, content, MARKUP.tag)},
566 | false, false))
567 | logseq.App.registerCommandPalette({
568 | label: ICON + ' Magic "quotes"', key: 'mc-7-update-22-magic-quotes',
569 | // @ts-expect-error
570 | keybinding: {},
571 | }, (e) => updateBlocksCommand(
572 | (content, level, block, parent) => {return magicQuotes(block, content, settings.magicQuotes)},
573 | false, false))
574 |
575 | logseq.App.registerCommandPalette({
576 | label: ICON + ' Trim lines punctuation ". , ;"', key: 'mc-7-update-23-trim-punctuation',
577 | // @ts-expect-error
578 | keybinding: {},
579 | }, (e) => updateBlocksCommand(trimLinePunctuation))
580 | logseq.App.registerCommandPalette({
581 | label: ICON + ' Trim lines punctuation ". , ;" (with nested)', key: 'mc-7-update-24-trim-punctuation-nested',
582 | // @ts-expect-error
583 | keybinding: {},
584 | }, (e) => updateBlocksCommand(trimLinePunctuation, true))
585 |
586 |
587 | // Navigation
588 | logseq.App.registerCommandPalette({
589 | label: ICON + ' Go to (↖︎) parent block', key: 'mc-2-edit-block-1-deep-dive-out',
590 | keybinding: {mac: 'mod+alt+left', binding: 'ctrl+alt+left', mode: 'global'},
591 | }, async (e) => parentBlockCommand() )
592 |
593 | logseq.App.registerCommandPalette({
594 | label: ICON + ' Go to (↘︎) last child block', key: 'mc-2-edit-block-2-deep-dive-in',
595 | keybinding: {mac: 'mod+alt+right', binding: 'ctrl+alt+right', mode: 'global'},
596 | }, async (e) => lastChildBlockCommand() )
597 |
598 | logseq.App.registerCommandPalette({
599 | label: ICON + ' Go to |↑| previous sibling block', key: 'mc-2-edit-block-5-prev-sibling',
600 | keybinding: {mac: 'ctrl+shift+up', binding: 'meta+alt+up', mode: 'global'},
601 | }, async (e) => previousSiblingBlockCommand() )
602 |
603 | logseq.App.registerCommandPalette({
604 | label: ICON + ' Go to |↓| next sibling block', key: 'mc-2-edit-block-6-next-sibling',
605 | keybinding: {mac: 'ctrl+shift+down', binding: 'meta+alt+down', mode: 'global'},
606 | }, async (e) => nextSiblingBlockCommand() )
607 |
608 | logseq.App.registerCommandPalette({
609 | label: ICON + ' Go to (↑) previous block', key: 'mc-2-edit-block-3-step-up',
610 | keybinding: {mac: 'mod+alt+up', binding: 'ctrl+alt+up', mode: 'global'},
611 | }, async (e) => editPreviousBlockCommand() )
612 |
613 | logseq.App.registerCommandPalette({
614 | label: ICON + ' Go to (↓) next block', key: 'mc-2-edit-block-4-step-down',
615 | keybinding: {mac: 'mod+alt+down', binding: 'ctrl+alt+down', mode: 'global'},
616 | }, async (e) => editNextBlockCommand() )
617 |
618 |
619 | // Movements
620 | logseq.App.registerCommandPalette({
621 | label: ICON + ' Move block (⤒) on top of siblings', key: 'mc-3-move-block-1-on-top',
622 | keybinding: {mac: 'mod+alt+shift+up', binding: 'ctrl+alt+shift+up', mode: 'global'},
623 | }, async (e) => moveToTopOfSiblingsCommand() )
624 |
625 | logseq.App.registerCommandPalette({
626 | label: ICON + ' Move block (⤓) on bottom of siblings', key: 'mc-3-move-block-2-on-bottom',
627 | keybinding: {mac: 'mod+alt+shift+down', binding: 'ctrl+alt+shift+down', mode: 'global'},
628 | }, async (e) => moveToBottomOfSiblingsCommand() )
629 |
630 | logseq.App.registerCommandPalette({
631 | label: ICON + ' Outdent (⇤) children of the block', key: 'mc-3-move-block-3-outdent',
632 | keybinding: {mac: 'ctrl+shift+tab', binding: 'ctrl+shift+tab', mode: 'global'},
633 | }, async (e) => outdentChildrenCommand() )
634 |
635 |
636 | // Transformations
637 | logseq.App.registerCommandPalette({
638 | label: ICON + ' Sort blocks', key: 'mc-4-transform-1-sort-blocks',
639 | // @ts-expect-error
640 | keybinding: {},
641 | }, (e) => sortBlocksCommand() )
642 | logseq.Editor.registerBlockContextMenuItem(
643 | ICON + ' Sort blocks', async (e) => sortBlocksCommand(e.uuid) )
644 |
645 | logseq.App.registerCommandPalette({
646 | label: ICON + ' Reverse blocks', key: 'mc-4-transform-2-reverse-blocks',
647 | // @ts-expect-error
648 | keybinding: {},
649 | }, (e) => reverseBlocksCommand() )
650 | logseq.Editor.registerBlockContextMenuItem(
651 | ICON + ' Reverse blocks', async (e) => reverseBlocksCommand(e.uuid) )
652 |
653 | logseq.App.registerCommandPalette({
654 | label: ICON + ' Shuffle blocks', key: 'mc-4-transform-3-shuffle-blocks',
655 | // @ts-expect-error
656 | keybinding: {},
657 | }, (e) => shuffleBlocksCommand() )
658 |
659 |
660 | await postInit(settings)
661 | }
662 |
663 |
664 | export const App = (logseq: any) => {
665 | logseq.ready(main).catch(console.error)
666 | }
667 |
--------------------------------------------------------------------------------
/src/commands/index.ts:
--------------------------------------------------------------------------------
1 | import '@logseq/libs'
2 | import { BlockEntity, IBatchBlock } from '@logseq/libs/dist/LSPlugin'
3 |
4 | import markdownit from 'markdown-it'
5 |
6 | import {
7 | PropertiesUtils, WalkBlock, checkPropertyExistenceInTree, ensureChildrenIncluded,
8 | filterOutChildBlocks, getBlocksWithReferences,
9 | getChosenBlocks, getEditingCursorSelection, insertBatchBlockBefore, isWindows, lettersToNumber,
10 | numberToLetters, numberToRoman, p, reduceBlockTree,
11 | reduceTextWithLength, scrollToBlock, setEditingCursorSelection, sleep, transformBlocksTreeByReplacing,
12 | transformSelectedBlocksWithMovements, unique, walkBlockTree, walkBlockTreeAsync,
13 | } from '../utils'
14 |
15 |
16 | // there is no `saw` emoji in Windows — use `kitchen knife`: it has the same colors
17 | export const ICON = isWindows ? '🔪' : '🪚'
18 |
19 | const md = markdownit('zero').enable([
20 | 'paragraph',
21 | 'text',
22 | 'list',
23 | // 'newline',
24 | ], true)
25 |
26 |
27 | export async function toggleAutoHeadingCommand(opts: {togglingBasedOnFirstBlock: boolean}) {
28 | const HEADING_REGEX = /^#+ /
29 | const PROPERTY = PropertiesUtils.headingProperty
30 |
31 | const [ blocks ] = await getChosenBlocks()
32 | if (blocks.length === 0)
33 | return
34 |
35 | const firstBlockHeading = blocks[0].properties?.heading || HEADING_REGEX.test(blocks[0].content)
36 |
37 | for (const block of blocks) {
38 | if (HEADING_REGEX.test(block.content)) {
39 | block.content = block.content.replace(HEADING_REGEX, '')
40 |
41 | // ensure there is no `heading::`
42 | PropertiesUtils.deleteProperty(block, PROPERTY)
43 |
44 | if (opts.togglingBasedOnFirstBlock && !firstBlockHeading)
45 | block.content += '\nheading:: true'
46 |
47 | await logseq.Editor.updateBlock(block.uuid, block.content)
48 | await logseq.Editor.exitEditingMode(true)
49 | continue
50 | }
51 |
52 | const heading = opts.togglingBasedOnFirstBlock ? firstBlockHeading : block.properties?.heading
53 | if (!heading)
54 | await logseq.Editor.upsertBlockProperty(block.uuid, PROPERTY, true)
55 | else
56 | await logseq.Editor.removeBlockProperty(block.uuid, PROPERTY)
57 |
58 | // ensure currently edited content will be saved
59 | block.content = block.content.replace(/\n?^heading:: true$/m, '')
60 | await logseq.Editor.updateBlock(block.uuid, block.content)
61 | }
62 | }
63 |
64 | export async function transformSelectedBlocksCommand(
65 | blocks: BlockEntity[],
66 | transformCallback: (blocks: BlockEntity[]) => BlockEntity[],
67 | isSelectedState: boolean,
68 | ) {
69 | // CASE: all transformed blocks relates to one root block
70 | if (blocks.length === 1) {
71 | const tree = await ensureChildrenIncluded(blocks[0])
72 | if (!tree.children || tree.children.length === 0)
73 | return // nothing to transform
74 |
75 | const newRoot = await transformBlocksTreeByReplacing(tree, transformCallback)
76 | if (newRoot) { // successfully replaced
77 | if (isSelectedState)
78 | await logseq.Editor.selectBlock(newRoot.uuid)
79 | else
80 | await logseq.Editor.editBlock(newRoot.uuid)
81 |
82 | return
83 | }
84 |
85 | // fallback to array of blocks
86 | blocks = tree.children as BlockEntity[]
87 | }
88 |
89 |
90 | // CASE: selected blocks from different parents
91 | transformSelectedBlocksWithMovements(blocks, transformCallback)
92 | }
93 |
94 | export async function sortBlocksCommand(contextBlockUUID: string | null = null) {
95 | let blocks: BlockEntity[]
96 | let isSelectedState = true
97 | if (contextBlockUUID)
98 | blocks = [(await logseq.Editor.getBlock(contextBlockUUID))!]
99 | else
100 | [blocks, isSelectedState] = await getChosenBlocks()
101 |
102 | if (blocks.length === 0) {
103 | await logseq.UI.showMsg(
104 | `[:div
105 | [:b "${ICON} Sort Blocks Command"]
106 | [:p "Select some blocks to use the command"]]`,
107 | 'warning',
108 | {timeout: 10000},
109 | )
110 | return
111 | }
112 |
113 | const comparer = (a: string, b: string) => a.localeCompare(b, 'en', { numeric: true })
114 | const sortBlocks = (blocks) => Array
115 | .from(blocks as BlockEntity[])
116 | .sort((a, b) => comparer(a.content, b.content))
117 |
118 | transformSelectedBlocksCommand(blocks, sortBlocks, isSelectedState)
119 | }
120 |
121 | export async function reverseBlocksCommand(contextBlockUUID: string | null = null) {
122 | let blocks: BlockEntity[]
123 | let isSelectedState = true
124 | if (contextBlockUUID)
125 | blocks = [(await logseq.Editor.getBlock(contextBlockUUID))!]
126 | else
127 | [blocks, isSelectedState] = await getChosenBlocks()
128 |
129 | if (blocks.length === 0) {
130 | await logseq.UI.showMsg(
131 | `[:div
132 | [:b "${ICON} Reverse Blocks Command"]
133 | [:p "Select some blocks to use the command"]]`,
134 | 'warning',
135 | {timeout: 10000},
136 | )
137 | return
138 | }
139 |
140 | const reverseBlocks = (blocks) => Array
141 | .from(blocks as BlockEntity[])
142 | .reverse()
143 |
144 | transformSelectedBlocksCommand(blocks, reverseBlocks, isSelectedState)
145 | }
146 |
147 | export async function shuffleBlocksCommand(contextBlockUUID: string | null = null) {
148 | let blocks: BlockEntity[]
149 | let isSelectedState = true
150 | if (contextBlockUUID)
151 | blocks = [(await logseq.Editor.getBlock(contextBlockUUID))!]
152 | else
153 | [blocks, isSelectedState] = await getChosenBlocks()
154 |
155 | if (blocks.length === 0) {
156 | await logseq.UI.showMsg(
157 | `[:div
158 | [:b "${ICON} Shuffle Blocks Command"]
159 | [:p "Select some blocks to use the command"]]`,
160 | 'warning',
161 | {timeout: 10000},
162 | )
163 | return
164 | }
165 |
166 | const shuffleBlocks = (blocks) => Array
167 | .from(blocks as BlockEntity[])
168 | .sort(() => Math.random() - 0.5)
169 |
170 | transformSelectedBlocksCommand(blocks, shuffleBlocks, isSelectedState)
171 | }
172 |
173 |
174 | export async function parentBlockCommand() {
175 | const [blocks] = await getChosenBlocks()
176 | const [first] = blocks
177 | if (!first)
178 | return
179 |
180 | if (first.parent.id === first.page.id)
181 | return
182 |
183 | const parentBlock = await logseq.Editor.getBlock(first.parent.id) as BlockEntity
184 | await logseq.Editor.editBlock(parentBlock.uuid)
185 | }
186 |
187 | export async function lastChildBlockCommand() {
188 | const [blocks] = await getChosenBlocks()
189 | const [first] = blocks
190 | if (!first)
191 | return
192 |
193 | const tree = await logseq.Editor.getBlock(first.uuid, {includeChildren: true}) as BlockEntity
194 | if (!tree.children || tree.children.length === 0)
195 | return
196 |
197 | const lastChild = tree.children!.at(-1)! as BlockEntity
198 | await logseq.Editor.editBlock(lastChild.uuid)
199 | }
200 |
201 | export async function previousSiblingBlockCommand() {
202 | const [blocks] = await getChosenBlocks()
203 | const [first] = blocks
204 | if (!first)
205 | return
206 |
207 | const prevBlock = await logseq.Editor.getPreviousSiblingBlock(first.uuid)
208 | if (!prevBlock)
209 | return
210 |
211 | const cursorPosition = (await logseq.Editor.getEditingCursorPosition())?.pos
212 | const content = await logseq.Editor.getEditingBlockContent()
213 | const isPositionOnTheEnd = cursorPosition === undefined || cursorPosition === content.length
214 |
215 | await logseq.Editor.editBlock(
216 | prevBlock.uuid,
217 | !isPositionOnTheEnd ? {pos: cursorPosition} : undefined
218 | )
219 | }
220 |
221 | export async function nextSiblingBlockCommand() {
222 | const [blocks] = await getChosenBlocks()
223 | const [first] = blocks
224 | if (!first)
225 | return
226 |
227 | const nextBlock = await logseq.Editor.getNextSiblingBlock(first.uuid)
228 | if (!nextBlock)
229 | return
230 |
231 | const cursorPosition = (await logseq.Editor.getEditingCursorPosition())?.pos
232 | const content = await logseq.Editor.getEditingBlockContent()
233 | const isPositionOnTheEnd = cursorPosition === undefined || cursorPosition === content.length
234 |
235 | await logseq.Editor.editBlock(
236 | nextBlock.uuid,
237 | !isPositionOnTheEnd ? {pos: cursorPosition} : undefined
238 | )
239 | }
240 |
241 | export async function editPreviousBlockCommand() {
242 | const [blocks] = await getChosenBlocks()
243 | let [current] = blocks
244 | if (!current)
245 | return
246 |
247 | let prevBlock = await logseq.Editor.getPreviousSiblingBlock(current.uuid)
248 | if (prevBlock) {
249 | let iteration = prevBlock
250 | while (true) {
251 | if (!iteration['collapsed?']) {
252 | const tree = await logseq.Editor.getBlock(iteration.uuid, {includeChildren: true}) as BlockEntity
253 | if (tree.children && tree.children.length !== 0) {
254 | iteration = tree.children.at(-1) as BlockEntity
255 | continue
256 | }
257 | }
258 |
259 | prevBlock = iteration
260 | break
261 | }
262 | } else {
263 | if (current.parent.id === current.page.id) {
264 | // no prev block at all → go to the start of current
265 | await logseq.Editor.editBlock(current.uuid, {pos: 0})
266 | return
267 | }
268 |
269 | const parent = await logseq.Editor.getBlock(current.parent.id) as BlockEntity
270 | prevBlock = parent
271 | }
272 |
273 | const cursorPosition = (await logseq.Editor.getEditingCursorPosition())?.pos
274 | const content = await logseq.Editor.getEditingBlockContent()
275 | const isPositionOnTheEnd = cursorPosition === undefined || cursorPosition === content.length
276 |
277 | await logseq.Editor.editBlock(
278 | (prevBlock as BlockEntity).uuid,
279 | !isPositionOnTheEnd ? {pos: cursorPosition} : undefined
280 | )
281 | }
282 |
283 | export async function editNextBlockCommand() {
284 | const [blocks] = await getChosenBlocks()
285 | let [current] = blocks
286 | if (!current)
287 | return
288 |
289 | let nextBlock: BlockEntity | null = null
290 | if (!current['collapsed?']) {
291 | const tree = await logseq.Editor.getBlock(current.uuid, {includeChildren: true}) as BlockEntity
292 | if (tree.children && tree.children.length !== 0)
293 | nextBlock = tree.children[0] as BlockEntity
294 | }
295 |
296 | if (!nextBlock) {
297 | let iteration = current
298 | while (true) {
299 | if (!nextBlock) {
300 | nextBlock = await logseq.Editor.getNextSiblingBlock(iteration.uuid)
301 | if (nextBlock)
302 | break
303 | }
304 |
305 | if (iteration.parent.id === iteration.page.id)
306 | break
307 |
308 | const parent = await logseq.Editor.getBlock(iteration.parent.id) as BlockEntity
309 | iteration = parent
310 |
311 | nextBlock = await logseq.Editor.getNextSiblingBlock(parent.uuid)
312 | if (nextBlock)
313 | break
314 | }
315 | }
316 |
317 | if (!nextBlock) {
318 | // no next block at all → go to the end of current
319 | await logseq.Editor.editBlock(current.uuid)
320 | return
321 | }
322 |
323 | const cursorPosition = (await logseq.Editor.getEditingCursorPosition())?.pos
324 | const content = await logseq.Editor.getEditingBlockContent()
325 | const isPositionOnTheEnd = cursorPosition === undefined || cursorPosition === content.length
326 |
327 | await logseq.Editor.editBlock(
328 | (nextBlock as BlockEntity).uuid,
329 | !isPositionOnTheEnd ? {pos: cursorPosition} : undefined
330 | )
331 | }
332 |
333 |
334 | export async function moveToTopOfSiblingsCommand() {
335 | const [blocks] = await getChosenBlocks()
336 | const [first] = blocks
337 | if (!first)
338 | return
339 |
340 | // already on top
341 | if (first.parent.id === first.left.id || first.left.id === first.page.id)
342 | return
343 |
344 | let topmost = first
345 | while (true) {
346 | const prev = await logseq.Editor.getPreviousSiblingBlock(topmost.uuid)
347 | if (!prev)
348 | break
349 | topmost = prev
350 | }
351 |
352 | await logseq.Editor.moveBlock(first.uuid, topmost.uuid, {before: true, children: false})
353 | await scrollToBlock(first)
354 | }
355 |
356 | export async function moveToBottomOfSiblingsCommand() {
357 | const [blocks] = await getChosenBlocks()
358 | const [first] = blocks
359 | if (!first)
360 | return
361 |
362 | let bottommost = first
363 | while (true) {
364 | const next = await logseq.Editor.getNextSiblingBlock(bottommost.uuid)
365 | if (!next)
366 | break
367 | bottommost = next
368 | }
369 |
370 | // already on bottom
371 | if (first.id === bottommost.id)
372 | return
373 |
374 | await logseq.Editor.moveBlock(first.uuid, bottommost.uuid, {before: false, children: false})
375 | await scrollToBlock(first)
376 | }
377 |
378 | export async function outdentChildrenCommand() {
379 | const [blocks] = await getChosenBlocks()
380 | for (const block of blocks) {
381 | const tree = await logseq.Editor.getBlock(block.uuid, {includeChildren: true})
382 | if (!tree)
383 | continue
384 |
385 | for (const child of (tree.children ?? []).toReversed()) {
386 | await logseq.Editor.moveBlock(
387 | (child as BlockEntity).uuid,
388 | block.uuid,
389 | {before: false, children: false},
390 | )
391 | }
392 | }
393 | }
394 |
395 |
396 | export function splitByLines(text: string): IBatchBlock[] {
397 | const textBlocks = text.split(/\n/)
398 | return textBlocks
399 | .map((tb) => {return {content: tb}})
400 | }
401 |
402 | export function splitByWords(text: string): IBatchBlock[] {
403 | const textBlocks = text.split(/[^\p{Lowercase_Letter}'-_]+/iu)
404 | return textBlocks
405 | .filter((tb) => !!tb)
406 | .map((tb) => {return {content: tb}})
407 | }
408 |
409 | export function splitByCommas(text: string): IBatchBlock[] {
410 | const textBlocks = text.split(/,\s*/)
411 | return textBlocks
412 | .filter((tb) => !!tb)
413 | .map((tb) => {return {content: tb}})
414 | }
415 |
416 | export function splitBySemicolons(text: string): IBatchBlock[] {
417 | const textBlocks = text.split(/;\s*/)
418 | return textBlocks
419 | .filter((tb) => !!tb)
420 | .map((tb) => {return {content: tb}})
421 | }
422 |
423 | export function splitBySentences(text: string): IBatchBlock[] {
424 | const textBlocks = text.split(/(?<=[.!?…])\s+/)
425 | return textBlocks
426 | .map((tb) => tb.endsWith('.') ? tb.slice(0, -1) : tb)
427 | .map((tb) => {return {content: tb}})
428 | }
429 |
430 | export function magicSplit(text: string): IBatchBlock[] {
431 | // add special types of ordered lists for parser to recognize it
432 | // (1) → 1)
433 | text = text.replaceAll(/^(\s*)\((\d{1,3}\)\s)/gm, '$1$2')
434 | // 1.2.3. → 1)
435 | text = text.replaceAll(/^(\s*)((\d|\p{Lowercase_Letter}){1,3}\.){2,10}\s/gmiu, '$11) ') // $11 means $1 and 1
436 | // 1.2.3 → 1)
437 | text = text.replaceAll(/^(\s*)((\d|\p{Lowercase_Letter}){1,3}\.){1,10}(\d|\p{Lowercase_Letter}){1,3}\s/gmiu, '$11) ') // $11 means $1 and 1
438 | // (1.2.3) → 1) and 1.2.3) → 1)
439 | text = text.replaceAll(/^(\s*)\(?((\d|\p{Lowercase_Letter}){1,3}\.){1,10}(\d|\p{Lowercase_Letter}){1,3}\)\s/gmiu, '$11) ')
440 | // a) → 1) and (a) → 1)
441 | text = text.replaceAll(/^(\s*)\(?(\p{Lowercase_Letter}{1,3})\)\s/gmiu, '$11) ')
442 | // a. → 1)
443 | text = text.replaceAll(/^(\s*)(\p{Lowercase_Letter}{1,3})\.\s/gmiu, '$11) ')
444 |
445 | // add special types of unordered lists for parser to recognize it
446 | // • → -
447 | text = text.replaceAll(/^(\s*)•\s*/gmu, '$1- ')
448 |
449 | const tokens = md.parse(text, {})
450 | console.debug(p`Parsed tokens`, Array.from(tokens))
451 | console.debug(p`HTML-view`, md.renderer.render(tokens, {}, {}))
452 |
453 | const results: IBatchBlock[] = []
454 |
455 | type State = {container: IBatchBlock, isForNumbering: boolean}
456 | const statesStack: State[] = []
457 |
458 | const initialState: State = {
459 | container: {content: '', children: results},
460 | isForNumbering: false,
461 | }
462 | let state = initialState
463 |
464 | function createBlock(content: string, opts: { numbering: boolean } = { numbering: false }) {
465 | const properties: {[name: string]: string} = {}
466 | if (opts.numbering)
467 | properties[PropertiesUtils.numberingProperty_] = 'number'
468 | return {content, children: [], properties}
469 | }
470 |
471 | while (true) {
472 | if (tokens.length === 0)
473 | break
474 |
475 | let token = tokens.shift()!
476 |
477 | switch (token!.type) {
478 | case 'paragraph_open': {
479 | const message = 'paragraph_open, inline, paragraph_close always comes along'
480 |
481 | token = tokens.shift()!
482 | if (token.type !== 'inline')
483 | throw new Error(message)
484 |
485 | state.container.children!.push(
486 | createBlock(token.content, {numbering: state.isForNumbering})
487 | )
488 |
489 | token = tokens.shift()!
490 | if (token.type !== 'paragraph_close')
491 | throw new Error(message)
492 |
493 | break
494 | }
495 |
496 | case 'ordered_list_open':
497 | case 'bullet_list_open': {
498 | const isForNumbering = token.type === 'ordered_list_open'
499 |
500 | // items of ordered list are always child items
501 | // so try to get the parent block here
502 | const lastBlock = state.container.children!.at(-1)
503 | if (lastBlock) {
504 | statesStack.push(state)
505 | state = {container: lastBlock, isForNumbering}
506 | }
507 | else
508 | state.isForNumbering = isForNumbering
509 |
510 | break
511 | }
512 |
513 | case 'ordered_list_close':
514 | case 'bullet_list_close':
515 | if (statesStack.length === 0) {
516 | state = initialState
517 | state.isForNumbering = false
518 | }
519 | else
520 | state = statesStack.pop()!
521 |
522 | break
523 |
524 | case 'list_item_open':
525 | case 'list_item_close':
526 | break
527 |
528 | default:
529 | throw new Error(`Unknown token: ${token.type}`)
530 | }
531 | }
532 |
533 | return results
534 | }
535 |
536 | export async function splitBlocksCommand(
537 | splitCallback: (content: string) => IBatchBlock[],
538 | keepChildrenInFirstBlock: boolean = true,
539 | recursive: boolean = false,
540 | ) {
541 | let [ blocks, isSelectedState ] = await getChosenBlocks()
542 | if (blocks.length === 0)
543 | return
544 |
545 | blocks = unique(blocks, (b) => b.uuid)
546 | if (recursive) {
547 | blocks = await Promise.all(
548 | blocks.map(async (b) => {
549 | return (
550 | await logseq.Editor.getBlock(b.uuid, {includeChildren: true})
551 | )!
552 | })
553 | )
554 | blocks = filterOutChildBlocks(blocks)
555 | }
556 |
557 | for (const block of blocks) {
558 | async function processBlock(b) {
559 | const block = b as BlockEntity
560 |
561 | const content = PropertiesUtils.deleteAllProperties(block.content)
562 | const batch = splitCallback(content)
563 |
564 | let head: IBatchBlock, tail: IBatchBlock[]
565 | if (keepChildrenInFirstBlock)
566 | [head, tail] = [batch[0], batch.slice(1)]
567 | else
568 | [tail, head] = [batch.slice(0, -1), batch.at(-1)!]
569 |
570 | if (content !== head.content) { // has changes?
571 | const properties = PropertiesUtils.fromCamelCaseAll(block.properties)
572 | Object.assign(properties, head.properties ?? {})
573 |
574 | await logseq.Editor.updateBlock(block.uuid, head.content, {properties})
575 | }
576 |
577 | if (head.children && head.children.length !== 0)
578 | await logseq.Editor.insertBatchBlock(
579 | block.uuid, head.children, {sibling: false})
580 |
581 | if (tail.length !== 0) {
582 | if (keepChildrenInFirstBlock)
583 | await logseq.Editor.insertBatchBlock(
584 | block.uuid, tail, {before: false, sibling: true})
585 | else
586 | await insertBatchBlockBefore(block, tail)
587 | }
588 | }
589 |
590 | if (recursive)
591 | await walkBlockTreeAsync(block as IBatchBlock, async (b, level) => processBlock(b))
592 | else
593 | await processBlock(block)
594 | }
595 |
596 | if (isSelectedState) {
597 | await sleep(20)
598 | await logseq.Editor.exitEditingMode()
599 | }
600 | }
601 |
602 |
603 | export function joinAsSentences_Map(content, level): string {
604 | if (content && /(? string,
643 | joinMapCallback?: (content: string, level: number, block: BlockEntity, parent?: BlockEntity) => string,
644 | opts: {shouldHandleSingleBlock: boolean} = {shouldHandleSingleBlock: false},
645 | ) {
646 | let [ blocks, isSelectedState ] = await getChosenBlocks()
647 | if (blocks.length === 0)
648 | return
649 |
650 | blocks = unique(blocks, (b) => b.uuid)
651 | blocks = await Promise.all(
652 | blocks.map(async (b) => {
653 | return (
654 | await logseq.Editor.getBlock(b.uuid, {includeChildren: true})
655 | )!
656 | })
657 | )
658 | blocks = filterOutChildBlocks(blocks)
659 |
660 | if (blocks.length === 0)
661 | return
662 |
663 | independentMode = independentMode || blocks.length === 1
664 |
665 |
666 | // it is important to check if any block in the tree has references
667 | // (Logseq replaces references with it's text)
668 | let noWarnings = true
669 | for (const block of blocks) {
670 | let blocksWithReferences = await getBlocksWithReferences(block)
671 |
672 | // root can have references (in independent mode), others — not
673 | const blocksWithReferences_noRoot = blocksWithReferences.filter((b) => b.uuid !== block.uuid)
674 | block._rootHasReferences = blocksWithReferences.length !== blocksWithReferences_noRoot.length
675 |
676 | if (independentMode)
677 | blocksWithReferences = blocksWithReferences_noRoot
678 |
679 | if (blocksWithReferences.length !== 0) {
680 | const html = blocksWithReferences.map((b) => {
681 | let content = PropertiesUtils.deleteAllProperties(b.content)
682 | content = reduceTextWithLength(content, 35)
683 | return `[:li [:i "${content}"]]`
684 | })
685 | await logseq.UI.showMsg(
686 | `[:div
687 | [:b "${ICON} Join Blocks Command"]
688 | [:p "There are blocks that have references from another blocks: "
689 | [:ul ${html}]
690 | ]
691 | [:p "Remove references or select only blocks without them."]
692 | ]`,
693 | 'warning',
694 | {timeout: 20000},
695 | )
696 | noWarnings = false
697 | }
698 | }
699 | if (!noWarnings)
700 | return
701 |
702 |
703 | // check for properties
704 | for (const block of blocks) {
705 | const names = checkPropertyExistenceInTree(block as IBatchBlock, {skipRoot: independentMode})
706 | if (names.length !== 0) {
707 | const html = names.map((name) => {
708 | return `[:li [:i "${PropertiesUtils.fromCamelCase(name)}"]]`
709 | })
710 | await logseq.UI.showMsg(
711 | `[:div
712 | [:b "${ICON} Join Blocks Command"]
713 | [:p "There are blocks that have properties (command can't handle them): "
714 | [:ul ${html}]
715 | ]
716 | [:p "Remove properties or select only blocks without them."]
717 | ]`,
718 | 'warning',
719 | {timeout: 20000},
720 | )
721 | noWarnings = false
722 | }
723 | }
724 | if (!noWarnings)
725 | return
726 |
727 |
728 | function reduceTree(block: IBatchBlock, { startLevel }: { startLevel: number }) {
729 | const preparedTree = walkBlockTree(block, (b, level, p, data) => {
730 | const content = PropertiesUtils.deleteAllProperties(b.content)
731 | return joinMapCallback
732 | ? joinMapCallback(
733 | content, level, b as BlockEntity, p as BlockEntity | undefined)
734 | : content
735 | }, startLevel)
736 |
737 | const reducedContent = reduceBlockTree(preparedTree, (b, level, children, data) => {
738 | return joinAttachCallback(b.content, level, children, b.data.node as BlockEntity)
739 | }, startLevel)
740 |
741 | return reducedContent
742 | }
743 |
744 | if (independentMode) {
745 | for (const block of blocks) {
746 | // ensure .children always is array
747 | if (!block.children)
748 | block.children = []
749 |
750 | if (block.children.length === 0) {
751 | if (!opts.shouldHandleSingleBlock)
752 | continue // nothing to join
753 | }
754 |
755 | const content = reduceTree(block as IBatchBlock, {startLevel: 0})
756 | const properties = PropertiesUtils.fromCamelCaseAll(block.properties)
757 |
758 | if (block._rootHasReferences || block.children.length === 0) {
759 | await logseq.Editor.updateBlock(block.uuid, content, {properties})
760 | for (const child of block.children)
761 | await logseq.Editor.removeBlock((child as BlockEntity).uuid)
762 | } else {
763 | await insertBatchBlockBefore(block, {content, properties})
764 | await logseq.Editor.removeBlock(block.uuid)
765 | }
766 | }
767 | } else {
768 | const top = blocks[0]
769 | const pseudoRoot: IBatchBlock = {content: '', children: blocks as IBatchBlock[]}
770 | const content = reduceTree(pseudoRoot, {startLevel: 0})
771 | const properties = PropertiesUtils.fromCamelCaseAll(top.properties)
772 |
773 | await insertBatchBlockBefore(top, {content, properties})
774 | for (const block of blocks)
775 | await logseq.Editor.removeBlock(block.uuid)
776 | }
777 |
778 | if (isSelectedState) {
779 | await sleep(20)
780 | await logseq.Editor.exitEditingMode()
781 | }
782 | }
783 |
784 | export function magicJoinCommand(independentMode: boolean) {
785 | const bulletPrefix = '* '
786 | const numberingPrefixGetters = [
787 | (n) => `${n}) `,
788 | (n) => `${numberToLetters(n)}) `,
789 | (n) => `${numberToRoman(n)}) `,
790 | ]
791 |
792 | return joinBlocksCommand(
793 | independentMode,
794 | (content, level, children, root) => {
795 | if (!root)
796 | throw new Error('assertion')
797 |
798 | let numbering = 1
799 | function resolvePrefix(content: string, block: BlockEntity) {
800 | const prefix = block._prefixGetter(numbering)
801 |
802 | if (block._isOrdered)
803 | numbering++
804 | else // start again on non-numbered child
805 | numbering = 1
806 |
807 | const filler = ' '.repeat(prefix.length)
808 | return prefix + content.replaceAll(/\n^/gm, '\n' + filler)
809 | }
810 |
811 | if (level == 0)
812 | content = resolvePrefix(content, root)
813 |
814 |
815 | const divider = '\n'
816 | const dividerParagraph = '\n\n'
817 |
818 | if (content && children.length !== 0)
819 | content += (level < 1 ? dividerParagraph : divider)
820 |
821 |
822 | content += children.map((child, index) => {
823 | child = resolvePrefix(child, root.children![index] as BlockEntity)
824 |
825 | let chosenDivider = (level < 1 ? dividerParagraph : divider)
826 | if (level === 0) {
827 | const childBlock = root.children![index] as BlockEntity
828 | const prevChildBlock = root.children![index - 1] as BlockEntity
829 | if (childBlock._isOrdered) {
830 | // reduce divider for items in ordered list
831 | chosenDivider = divider
832 | }
833 | else if (prevChildBlock && prevChildBlock._isOrdered) {
834 | // increase upper divider for the last item in ordered list
835 | child = divider + child
836 | }
837 | }
838 |
839 | return child + chosenDivider
840 | }).join('').trimEnd()
841 |
842 | if (level === 1) {
843 | // Logseq markdown syntax require additional new line after block with children
844 | // to display next block as a separate paragraph
845 | if (children.length !== 0)
846 | content += '\n'
847 | }
848 |
849 | return content
850 | },
851 | (content, level, block, parent) => {
852 | const properties = block.properties ?? {}
853 | const isOrdered = properties[PropertiesUtils.numberingProperty] === 'number'
854 |
855 | // there shouldn't be a numbering after joining
856 | delete properties[PropertiesUtils.numberingProperty]
857 |
858 | if (isOrdered) {
859 | const type = (
860 | (parent?._numberingType + 1) || 0
861 | ) % numberingPrefixGetters.length
862 |
863 | // don't save type for level 0 as the root item moves to the child level
864 | // and they need the one level of numbering
865 | if (level !== 0)
866 | block._numberingType = type
867 |
868 | block._isOrdered = true
869 | block._prefixGetter = numberingPrefixGetters[type]
870 | }
871 | else {
872 | if (level <= 1)
873 | block._prefixGetter = (_) => ''
874 | else
875 | block._prefixGetter = (_) => bulletPrefix
876 | }
877 |
878 | return content
879 | },
880 | )
881 | }
882 |
883 |
884 | export async function updateBlocksCommand(
885 | callback: (content: string, level: number, block: BlockEntity, parent?: BlockEntity) => string,
886 | recursive: boolean = false,
887 | cleanPropertiesBefore: boolean = true,
888 | ) {
889 | let [ blocks, isSelectedState ] = await getChosenBlocks()
890 | if (blocks.length === 0)
891 | return
892 |
893 | blocks = unique(blocks, (b) => b.uuid)
894 | if (recursive) {
895 | blocks = await Promise.all(
896 | blocks.map(async (b) => {
897 | return (
898 | await logseq.Editor.getBlock(b.uuid, {includeChildren: true})
899 | )!
900 | })
901 | )
902 | blocks = filterOutChildBlocks(blocks)
903 | }
904 |
905 | if (blocks.length === 0)
906 | return
907 |
908 | if (!isSelectedState)
909 | blocks[0]._selectPosition = getEditingCursorSelection()!
910 |
911 | // it is important to check if any block in the tree has references
912 | // (Logseq replaces references with it's text)
913 | if (recursive)
914 | for (const block of blocks) {
915 | const blocksWithReferences = await getBlocksWithReferences(block)
916 | block._treeHasReferences = blocksWithReferences.length !== 0
917 | }
918 |
919 | for (const block of blocks) {
920 | // ensure .children is always an array
921 | if (!block.children)
922 | block.children = []
923 |
924 | // skip child nodes in non-recursive mode
925 | if (!recursive)
926 | block.children = []
927 |
928 | const newTree = walkBlockTree(block as WalkBlock, (b, level, p, data) => {
929 | const properties = PropertiesUtils.fromCamelCaseAll(data.node.properties)
930 | const propertiesOrder = PropertiesUtils.getPropertyNames(b.content)
931 |
932 | let content = b.content
933 | if (cleanPropertiesBefore)
934 | content = PropertiesUtils.deleteAllProperties(content)
935 | const newContent = callback(content, level, data.node, p as BlockEntity | undefined)
936 | if (content === newContent)
937 | data.leftIntact = true
938 | content = newContent
939 |
940 | if (cleanPropertiesBefore)
941 | for (const property of propertiesOrder)
942 | content += `\n${property}:: ${properties[property]}`
943 |
944 | return content
945 | })
946 |
947 | if (block._treeHasReferences || block.children.length === 0) {
948 | walkBlockTreeAsync(newTree, async (b, level) => {
949 | if (!b.data.leftIntact)
950 | await logseq.Editor.updateBlock(b.data.node.uuid, b.content)
951 | })
952 | } else {
953 | await insertBatchBlockBefore(block, newTree)
954 | await logseq.Editor.removeBlock(block.uuid)
955 | }
956 | }
957 |
958 | if (isSelectedState) {
959 | await sleep(20)
960 | await logseq.Editor.exitEditingMode()
961 | } else {
962 | await sleep(20)
963 | const sole = blocks[0]
964 | setEditingCursorSelection(sole._selectPosition[0], sole._selectPosition[1])
965 | }
966 | }
967 |
968 | export function trimLinePunctuation(content, level, block, parent) {
969 | return content
970 | .replaceAll(/[.;,](?=\n|$)/g, '')
971 | }
972 |
973 | export function removeNewLines(content, level, block, parent) {
974 | return content
975 | .replaceAll(/\n+/g, '\n')
976 | .replaceAll(/(?<=[^\S\n])\n/g, '') // remove \n when there are spaces before
977 | .replaceAll(/(? m[0].toUpperCase() + m.slice(1).toLowerCase(),
993 | )
994 | }
995 |
996 | export function titleCaseSentences(content, level, block, parent) {
997 | return content
998 | .toLowerCase()
999 | .replace( // first letter of text
1000 | /^([^\p{Lowercase_Letter}]*)(\p{Lowercase_Letter})/iu,
1001 | (m, gap, letter) => gap + letter.toUpperCase(),
1002 | )
1003 | .replaceAll( // first letter after end of sentence
1004 | /(?<=[.!?…]\s+)\p{Lowercase_Letter}/giu,
1005 | (m) => m.toUpperCase(),
1006 | )
1007 | }
1008 |
1009 | export function removeHTML(content, level, block, parent) {
1010 | return content
1011 | .replaceAll(/<\w+\s*[^>]*>/g, '')
1012 | .replaceAll(/<\/\w+\s*>/g, '')
1013 | }
1014 |
1015 | export function parseYoutubeTimestamp(content, level, block, parent) {
1016 | function fromTimestamp(ts) {
1017 | const time = parseInt(ts)
1018 |
1019 | const iso = new Date(time * 1000).toISOString()
1020 | // '1970-01-01T00:14:25.000Z'
1021 |
1022 | if (time < 3600) // MM:SS
1023 | return iso.substring(14, 19)
1024 | else // HH:MM:SS
1025 | return iso.substring(11, 19)
1026 | }
1027 |
1028 | const replacer = (m, h, ts) => `{{youtube-timestamp ${fromTimestamp(ts)}}}`
1029 | return content
1030 | .replaceAll(
1031 | /\[(\d+:)?\d+:\d+\]\(https:\/\/www\.youtube\.com\/watch\?.+?&t=(\d+)s\)/g,
1032 | replacer,
1033 | )
1034 | .replaceAll(
1035 | /\[(\d+:)?\d+:\d+\]\(https:\/\/www\.youtube\.com\/watch\?t=(\d+)s.*?\)/g,
1036 | replacer,
1037 | )
1038 | }
1039 |
--------------------------------------------------------------------------------
/src/commands/magic_markup.ts:
--------------------------------------------------------------------------------
1 | import '@logseq/libs'
2 | import { BlockEntity } from '@logseq/libs/dist/LSPlugin'
3 |
4 |
5 | class MarkUp implements Iterable<[string, string]> {
6 | public wrap: [string, string]
7 | public alternativeWrap: [string, string]
8 | public unwrap: [string, string][]
9 |
10 | public wrappedWith?: string
11 | public alternativeWrapWhenMatch?: RegExp
12 |
13 | constructor({ wrap, unwrap, alternativeWrapWhenMatch, alternativeWrapIndex }: {
14 | wrap: [string, string]
15 | unwrap: ([string, string] | null)[]
16 | alternativeWrapWhenMatch?: RegExp
17 | alternativeWrapIndex?: number
18 | }) {
19 | this.wrap = wrap
20 | this.unwrap = unwrap.map((item) => item === null ? wrap : item)
21 | if (!this.unwrap.includes(wrap))
22 | this.unwrap.splice(0, 0, wrap)
23 | this.wrappedWith = undefined
24 | this.alternativeWrapWhenMatch = alternativeWrapWhenMatch
25 | this.alternativeWrap = this.unwrap[alternativeWrapIndex ?? 1]
26 | }
27 |
28 | [Symbol.iterator]() {
29 | let index = 0
30 | return {
31 | next: () => {
32 | return {
33 | done: index >= this.unwrap.length,
34 | value: this.unwrap[index++],
35 | }
36 | }
37 | }
38 | }
39 | getWrapFor(selection: string) {
40 | if (this.alternativeWrapWhenMatch && this.alternativeWrapWhenMatch.test(selection))
41 | return this.alternativeWrap
42 | return this.wrap
43 | }
44 | getUnwrapFor(line: string, selectPosition: [number, number]): [string, string] | null {
45 | // Assertion: selectPosition should be already trimmed, so markup is always OUTside
46 |
47 | for (const markup of this.unwrap)
48 | if (MarkUp.isMarkedUpWith(line, selectPosition, markup))
49 | return markup
50 | return null
51 | }
52 | static isMarkedUpWith(line: string, selectPosition: [number, number], markup: [string, string]): boolean {
53 | const [start, end] = selectPosition
54 | const [frontMarkup, backMarkup] = markup
55 |
56 | if (start < frontMarkup.length)
57 | return false
58 | if (end + backMarkup.length > line.length)
59 | return false
60 |
61 | const charsBefore = line.slice(start - frontMarkup.length, start)
62 | const charsAfter = line.slice(end, end + backMarkup.length)
63 | return charsBefore === frontMarkup && charsAfter === backMarkup
64 | }
65 | }
66 |
67 | export const MARKUP: {[type: string]: MarkUp } = {
68 | bold: new MarkUp({
69 | wrap: ['**', '**'],
70 | unwrap: [['', '']] }),
71 | italics: new MarkUp({
72 | wrap: ['_', '_'],
73 | unwrap: [['*', '*'], ['', '']] }),
74 | strikethrough: new MarkUp({
75 | wrap: ['~~', '~~'],
76 | unwrap: [['', '']] }),
77 | highlight: new MarkUp({
78 | wrap: ['==', '=='],
79 | unwrap: [['', '']] }),
80 | underline: new MarkUp({
81 | wrap: ['', ''],
82 | unwrap: [['', '']] }),
83 | code: new MarkUp({
84 | wrap: ['`', '`'],
85 | unwrap: [['', '
']] }),
86 | ref: new MarkUp({
87 | wrap: ['[[', ']]'],
88 | unwrap: [['#[[', ']]'], null, ['#', '']] }),
89 | tag: new MarkUp({
90 | wrap: ['#', ''],
91 | unwrap: [['#[[', ']]'], ['[[', ']]']],
92 | alternativeWrapWhenMatch: /\s+/,
93 | alternativeWrapIndex: 1, }),
94 | }
95 |
96 | const TRIM_BEGIN = [
97 | /^\s+/, // spaces at the beginning
98 |
99 | /^#{1,6} /, // headings
100 |
101 | /^\s*[-+*] /, // list item
102 | /^\s*[-+*] \[.\] /, // list item with markdown task
103 | /^>/, // quote
104 |
105 | // logseq tasks
106 | /^(LATER|TODO) (\[#(A|B|C)\])?/,
107 | /^(NOW|DOING) (\[#(A|B|C)\])?/,
108 | /^DONE (\[#(A|B|C)\])?/,
109 | /^(WAIT|WAITING) (\[#(A|B|C)\])?/,
110 | /^(CANCELED|CANCELLED) (\[#(A|B|C)\])?/,
111 |
112 | /^\[#(A|B|C)\]/, // non-task blocks with priorities
113 |
114 | /^\s*[^\s:;,^@#~"`/|\\(){}[\]]+:: /u, // property without value
115 | /^\s*DEADLINE: <[^>]+>$/,
116 | /^\s*SCHEDULED: <[^>]+>$/,
117 | ]
118 | const TRIM_BEGIN_SELECTION_ADDITION = [
119 | /^\s*[^\s:;,^@#~"`/|\\(){}[\]]+:: .*$/u, // property with value
120 | ]
121 | const TRIM_BEFORE: (string | RegExp)[] = [
122 | /^\(\([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\)\)/, // block ref
123 | /^{{\w+.*?}}/, // macro call
124 |
125 | /^\s/,
126 | ]
127 | const TRIM_AFTER: (string | RegExp)[] = [
128 | /\!\[.*?\]\(.+?\){.*?}$/, // link to image
129 | /\!\[.*?\]\(.+?\)$/, // link to image
130 |
131 | // /\]\(.+?\)$/, // link to page
132 | /\]\(\[\[.+?\]\]\)$/, // link to page
133 |
134 | /\]\(\(\([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\)\)\)$/, // link to block
135 | /\(\([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\)\)$/, // block ref
136 |
137 | /{{\w+.*?}}$/, // macro call
138 |
139 | /\s$/,
140 |
141 | /\]\($/, // to not break markdown links
142 | ]
143 | const EXPAND_WHEN_OUTSIDE = [
144 | // this is OR groups
145 | // order is expansion direction
146 |
147 | [ // order is exceptional priority
148 | ['$', ''],
149 | ['', '$'],
150 | ['', '€'],
151 | ],
152 |
153 | [
154 | ['\"', '\"'],
155 | ["'", "'"],
156 | ["«", "»"],
157 | ],
158 |
159 | [
160 | ['#[[', ']]'],
161 | ['#', ''],
162 | ['[[', ']]'],
163 | ],
164 | ]
165 |
166 |
167 | function trim(line: string, markup: MarkUp, selectPosition: [number, number], isSelectionMode: boolean) {
168 | let [start, end] = selectPosition
169 |
170 | let trimBegin = TRIM_BEGIN
171 | if (!isSelectionMode)
172 | trimBegin = TRIM_BEGIN_SELECTION_ADDITION.concat(trimBegin)
173 |
174 | // begin: once
175 | for (const re of trimBegin) {
176 | const m = line.match(re)
177 | if (m) {
178 | const match = m[0]
179 | start = Math.max(start, match.length)
180 | break
181 | }
182 | }
183 |
184 | let selection = line.slice(start, end)
185 |
186 | const trimBefore = Array.from(TRIM_BEFORE)
187 | const trimAfter = Array.from(TRIM_AFTER)
188 |
189 | for (const [frontMarkup, backMarkup] of markup) {
190 | trimBefore.push(frontMarkup)
191 | trimAfter.push(backMarkup)
192 | }
193 |
194 | // before
195 | while (true) {
196 | let wasTrimmed = false
197 | trimBefore.forEach(strOrRE => {
198 | if (!strOrRE)
199 | return
200 |
201 | if (typeof strOrRE === 'string') {
202 | if (selection.startsWith(strOrRE)) {
203 | selection = selection.slice(strOrRE.length)
204 | start += strOrRE.length
205 | wasTrimmed = true
206 | }
207 | } else {
208 | const m = strOrRE.exec(selection)
209 | if (m) {
210 | selection = selection.slice(m[0].length)
211 | start += m[0].length
212 | wasTrimmed = true
213 | }
214 | }
215 | })
216 | if (!wasTrimmed || selection.length === 0)
217 | break
218 | }
219 |
220 | // after
221 | while (true) {
222 | let wasTrimmed = false
223 | trimAfter.forEach((strOrRE) => {
224 | if (!strOrRE)
225 | return
226 |
227 | if (typeof strOrRE === 'string') {
228 | if (selection.endsWith(strOrRE)) {
229 | selection = selection.slice(0, -strOrRE.length)
230 | end -= strOrRE.length
231 | wasTrimmed = true
232 | }
233 | } else {
234 | const m = strOrRE.exec(selection)
235 | if (m) {
236 | selection = selection.slice(0, -m[0].length)
237 | end -= m[0].length
238 | wasTrimmed = true
239 | }
240 | }
241 | })
242 | if (!wasTrimmed || !selection.length)
243 | break
244 | }
245 |
246 | selectPosition[0] = start
247 | selectPosition[1] = end
248 | }
249 |
250 | function wordAtPosition(line: string, position: number) {
251 | const wordLeftRegexp = /(?!_)[\p{Letter}\p{Number}'_-]*$/u
252 | const wordRightRegexp = /^[\p{Letter}\p{Number}'_-]*(? {
279 | for (const pair of group) {
280 | const [L, R] = pair
281 |
282 | let skipPair = false
283 | for (const [frontMarkup, backMarkup] of markup)
284 | if (L === frontMarkup || R === backMarkup) {
285 | // allow undoing of the syntax
286 | skipPair = true
287 | break
288 | }
289 | if (skipPair)
290 | break
291 |
292 | const trimLastSpace = Boolean(pair[2])
293 |
294 | if (MarkUp.isMarkedUpWith(line, [start, end], [L, R])) {
295 | start -= L.length
296 | end += R.length
297 | setSelection()
298 | break
299 | }
300 | }
301 | })
302 | }
303 |
304 | function applyMarkup(line: string, markup: MarkUp, selectPosition?: [number, number]): string {
305 | if (!line && !selectPosition)
306 | return ''
307 |
308 | const isSelectionMode = Boolean(selectPosition)
309 | if (!selectPosition)
310 | selectPosition = [0, line.length]
311 |
312 | trim(line, markup, selectPosition, isSelectionMode)
313 | if (isSelectionMode)
314 | expand(line, markup, selectPosition)
315 |
316 | let [start, end] = selectPosition
317 | if (start > end)
318 | return line // no changes
319 |
320 | if (start === end) {
321 | if (!isSelectionMode)
322 | return line
323 | }
324 |
325 | let selection = line.slice(start, end)
326 | const wrappedWith = markup.getUnwrapFor(line, selectPosition)
327 | if (wrappedWith) {
328 | const [frontMarkup, backMarkup] = wrappedWith
329 | selectPosition[0] -= frontMarkup.length
330 | selectPosition[1] -= frontMarkup.length
331 | return line.slice(0, start - frontMarkup.length) + selection + line.slice(end + backMarkup.length)
332 | } else {
333 | const [frontMarkup, backMarkup] = markup.getWrapFor(selection)
334 | selectPosition[0] += frontMarkup.length
335 | selectPosition[1] += frontMarkup.length
336 | return line.slice(0, start) + frontMarkup + selection + backMarkup + line.slice(end)
337 | }
338 | }
339 |
340 | export function magicWrap(block: BlockEntity, content: string, markup: MarkUp) {
341 | if (!block._selectPosition)
342 | return content.split('\n').map((line) => applyMarkup(line, markup)).join('\n')
343 |
344 | let lineStartPosition = 0
345 | let newLineStartPosition = 0
346 |
347 | const selectStart = block._selectPosition[0]
348 | const selectEnd = block._selectPosition[1]
349 | const selectReversedEnd = content.length - selectEnd
350 |
351 | const wrapped = content.split('\n').map((line, index) => {
352 | if (index !== 0) {
353 | // \n sign
354 | lineStartPosition++
355 | newLineStartPosition++
356 | }
357 |
358 | const lineEndPosition = lineStartPosition + line.length
359 |
360 | // line completely before OR after selection OR on the edge
361 | if (
362 | lineEndPosition < selectStart ||
363 | selectEnd < lineStartPosition ||
364 | lineEndPosition === selectStart && selectEnd !== selectStart ||
365 | lineStartPosition === selectEnd && selectEnd !== selectStart
366 | ) {
367 | lineStartPosition += line.length
368 | newLineStartPosition += line.length
369 | return line
370 | }
371 |
372 | let wholeLine = false // line inside selection
373 | if (selectStart < lineStartPosition && lineEndPosition < selectEnd)
374 | wholeLine = true
375 |
376 | const selectPositionLine: [number, number] = [
377 | Math.max(selectStart - lineStartPosition, 0),
378 | Math.min(selectEnd - lineStartPosition, line.length),
379 | ]
380 | const newLine = applyMarkup(line, markup, wholeLine ? undefined : selectPositionLine)
381 |
382 | if (!wholeLine) {
383 | // first line of selection
384 | if (lineStartPosition <= selectStart && selectStart <= lineEndPosition) {
385 | block._selectPosition[0] = newLineStartPosition + selectPositionLine[0]
386 | }
387 | // last line of selection
388 | if (lineStartPosition <= selectEnd && selectStart <= lineEndPosition) {
389 | block._selectPosition[1] = newLineStartPosition + selectPositionLine[1]
390 | }
391 | }
392 |
393 | lineStartPosition += line.length
394 | newLineStartPosition += newLine.length
395 | return newLine
396 | }).join('\n')
397 |
398 | return wrapped
399 | }
400 |
401 | export function magicQuotes(block: BlockEntity, content: string, quotes: string) {
402 | const wrap: [string, string] = [quotes[0], quotes[1]]
403 | const unwrap: ([string, string] | null)[] = [
404 | ['«', '»'], ['"', '"'], ["'", "'"], ['“', '“'], ['‘', '‘']]
405 |
406 | for (const [i, pair] of Object.entries(unwrap)) {
407 | if (pair && pair[0] == wrap[0] && pair[1] == wrap[1]) {
408 | unwrap.splice(Number(i), 1)
409 | break
410 | }
411 | }
412 |
413 | console.log('TRACING', {wrap, unwrap})
414 |
415 | const quotesMarkup = new MarkUp({wrap, unwrap})
416 | return magicWrap(block, content, quotesMarkup)
417 | }
418 |
--------------------------------------------------------------------------------
/src/css/border_view.css:
--------------------------------------------------------------------------------
1 | /* border view: borders around blocks */
2 | /* (based on logtools) */
3 |
4 | .ls-block[data-refs-self*='".border"'] {
5 | box-shadow: 2px 2px 3px #0000003b;
6 | border: 1px solid var(--ls-border-color);
7 | border-radius: var(--ls-border-radius-medium);
8 | background-color: var(--ls-primary-background-color);
9 | margin-right: 0.5em !important;
10 | margin-bottom: 0.2em !important;
11 | margin-left: 0.5em !important;
12 | padding: 0.6em 1em 0.7em 0em !important
13 | }
14 | .ls-block[data-refs-self*='".border"'].selected {
15 | background-color: var(--ls-block-highlight-color);
16 | }
17 |
18 | .ls-block[data-refs-self*='".border"'] > .block-main-container {
19 | /* - ls-block::margin-left - ls-block::border-width */
20 | margin-left: calc(-0.5em - 1px) !important;
21 | }
22 | .ls-block[data-refs-self*='".border"'] > .block-children-container {
23 | /* block-children-container::margin-left - ls-block::margin-left - ls-block::border-width */
24 | margin-left: calc(29px - 0.5em - 1px) !important;
25 | }
26 |
27 | .ls-block[data-refs-self*='".border-child"'] > .block-children-container > .block-children > .ls-block {
28 | box-shadow: 2px 2px 3px #0000003b;
29 | border: 1px solid var(--ls-border-color);
30 | border-radius: var(--ls-border-radius-low);
31 | background-color: var(--ls-tertiary-background-color);
32 | margin: 0.5em 1em 0.5em 0em !important;
33 | padding: 0.5em 1em 0.5em 0em !important;
34 | }
35 | .ls-block[data-refs-self*='".border-child"'] > .block-children-container > .block-children > .ls-block.selected {
36 | background-color: var(--ls-block-highlight-color);
37 | }
38 | .ls-block[data-refs-self*='".border-child"'] > .block-children-container > .block-children {
39 | border-left-width: 0px !important;
40 | }
41 |
--------------------------------------------------------------------------------
/src/css/columns_view.css:
--------------------------------------------------------------------------------
1 | /* columns view */
2 | /* (based on logtools & awesome-content) */
3 |
4 | /* .columns-N: split exactly by N columns */
5 |
6 | .ls-block[data-refs-self*='".columns-2"'] > .block-children-container > .block-children {
7 | column-count: 2;
8 | width: 100%;
9 | }
10 | .ls-block[data-refs-self*='".columns-3"'] > .block-children-container > .block-children {
11 | column-count: 3;
12 | width: 100%;
13 | }
14 | .ls-block[data-refs-self*='".columns-4"'] > .block-children-container > .block-children {
15 | column-count: 4;
16 | width: 100%;
17 | }
18 | .ls-block[data-refs-self*='".columns-5"'] > .block-children-container > .block-children {
19 | column-count: 5;
20 | width: 100%;
21 | }
22 | .ls-block[data-refs-self*='".columns-6"'] > .block-children-container > .block-children {
23 | column-count: 6;
24 | width: 100%;
25 | }
26 |
27 |
28 | /* .columns: split by children count */
29 |
30 | .ls-block[data-refs-self*='".columns"'] > .block-children-container {
31 | width: 100%;
32 | }
33 | .ls-block[data-refs-self*='".columns"'] > .block-children-container > .block-children {
34 | display: inline-flex;
35 | position: relative;
36 | overflow-x: auto;
37 | overflow-y: hidden;
38 | }
39 | .ls-block[data-refs-self*='".columns"'] > .block-children-container > .block-children > .ls-block {
40 | display: inline-block;
41 | padding: 0;
42 |
43 | width: inherit;
44 | min-width: 200px;
45 | margin-right: 10px;
46 | }
47 |
48 | /* remove left border */
49 | .ls-block[data-refs-self*='".columns"'] > .block-children-container > .block-children > .ls-block,
50 | .ls-block[data-refs-self*='".columns-fit"'] > .block-children-container > .block-children > .ls-block {
51 | margin-left: 0px;
52 | }
53 | .ls-block[data-refs-self*='".columns"'] > .block-children-container > .block-children,
54 | .ls-block[data-refs-self*='".columns-fit"'] > .block-children-container > .block-children {
55 | border-left-width: 0px !important;
56 | }
57 |
58 |
59 | /* .columns-fit: fit width of the columns */
60 |
61 | .ls-block[data-refs-self*='".columns-fit"'] > .block-children-container > .block-children > .ls-block {
62 | min-width: 100px;
63 | }
64 | [data-refs-self*='".columns-fit"'] > .block-children-container > .block-children {
65 | display: inline-flex;
66 | position: relative;
67 | border: 0;
68 | }
69 | [data-refs-self*='".columns-fit"'] .pr-2 {
70 | padding: 0;
71 | }
72 |
--------------------------------------------------------------------------------
/src/css/gallery_view.css:
--------------------------------------------------------------------------------
1 | /* gallery view: for images */
2 | /* (based on logtools) */
3 |
4 | [data-refs-self*='".gallery'] > .block-children-container {
5 | width: 100%;
6 | }
7 |
8 | [data-refs-self*='".gallery'] > .block-children-container > .block-children {
9 | display: inline-flex;
10 | margin: 0px 0px 0px 10px;
11 | border-left: none;
12 | }
13 | [data-refs-self*='".gallery'] > .block-children-container > .block-children > .ls-block {
14 | margin: 0px 10px 0px 0px;
15 | min-width: 80px;
16 | }
17 |
18 | /* changing content sizes */
19 | [data-refs-self*='".gallery'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper {
20 | width: 100%;
21 | }
22 | [data-refs-self*='".gallery'] > .block-children-container > .block-children > .ls-block > .block-children-container {
23 | margin-left: -10px;
24 | }
25 |
26 | /* hide unnecessary parts */
27 | [data-refs-self*='".gallery'] > .block-children-container .block-content .asset-container {
28 | margin-top: 0rem;
29 | }
30 |
31 | [data-refs-self*='".gallery'] > .block-children-container > .block-children > .ls-block > div {
32 | padding-right: 0;
33 | }
34 | [data-refs-self*='".gallery'] > .block-children-container > .block-children > .ls-block > div > .block-control-wrap {
35 | display: none;
36 | }
37 | [data-refs-self*='".gallery'] > .block-children-container > .block-children > .ls-block > .block-children-container > .block-children-left-border {
38 | display: none;
39 | }
40 | [data-refs-self*='".gallery'] > .block-children-container > .block-children > .ls-block > .block-children-container > .block-children {
41 | border-left: 0px;
42 | }
43 |
44 |
45 | /* in case fixed width or height (note «-») */
46 | [data-refs-self*='".gallery-'] > .block-children-container > .block-children > .ls-block {
47 | min-width: unset;
48 | }
49 | [data-refs-self*='".gallery-'] > .block-children-container > .block-children {
50 | flex-wrap: wrap;
51 | }
52 |
53 |
54 | /* .gallery-wN: force width of the columns */
55 | [data-refs-self*='".gallery-w1"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
56 | [data-refs-self*='".gallery-w1"'] > .block-children-container > .block-children > .ls-block img {
57 | width: 100px !important;
58 | }
59 | [data-refs-self*='".gallery-w2"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
60 | [data-refs-self*='".gallery-w2"'] > .block-children-container > .block-children > .ls-block img {
61 | width: 120px !important;
62 | }
63 | [data-refs-self*='".gallery-w3"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
64 | [data-refs-self*='".gallery-w3"'] > .block-children-container > .block-children > .ls-block img {
65 | width: 150px !important;
66 | }
67 | [data-refs-self*='".gallery-w4"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
68 | [data-refs-self*='".gallery-w4"'] > .block-children-container > .block-children > .ls-block img {
69 | width: 200px !important;
70 | }
71 | [data-refs-self*='".gallery-w5"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
72 | [data-refs-self*='".gallery-w5"'] > .block-children-container > .block-children > .ls-block img {
73 | width: 250px !important;
74 | }
75 | [data-refs-self*='".gallery-w6"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
76 | [data-refs-self*='".gallery-w6"'] > .block-children-container > .block-children > .ls-block img {
77 | width: 300px !important;
78 | }
79 | [data-refs-self*='".gallery-w7"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
80 | [data-refs-self*='".gallery-w7"'] > .block-children-container > .block-children > .ls-block img {
81 | width: 350px !important;
82 | }
83 | [data-refs-self*='".gallery-w8"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
84 | [data-refs-self*='".gallery-w8"'] > .block-children-container > .block-children > .ls-block img {
85 | width: 400px !important;
86 | }
87 | [data-refs-self*='".gallery-w9"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
88 | [data-refs-self*='".gallery-w9"'] > .block-children-container > .block-children > .ls-block img {
89 | width: 800px !important;
90 | }
91 |
92 |
93 | /* .gallery-hN: force height of the columns */
94 | [data-refs-self*='".gallery-h1"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
95 | [data-refs-self*='".gallery-h1"'] > .block-children-container > .block-children > .ls-block img {
96 | height: 100px !important;
97 | width: auto !important;
98 | }
99 | [data-refs-self*='".gallery-h2"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
100 | [data-refs-self*='".gallery-h2"'] > .block-children-container > .block-children > .ls-block img {
101 | height: 120px !important;
102 | width: auto !important;
103 | }
104 | [data-refs-self*='".gallery-h3"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
105 | [data-refs-self*='".gallery-h3"'] > .block-children-container > .block-children > .ls-block img {
106 | height: 150px !important;
107 | width: auto !important;
108 | }
109 | [data-refs-self*='".gallery-h4"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
110 | [data-refs-self*='".gallery-h4"'] > .block-children-container > .block-children > .ls-block img {
111 | height: 200px !important;
112 | width: auto !important;
113 | }
114 | [data-refs-self*='".gallery-h5"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
115 | [data-refs-self*='".gallery-h5"'] > .block-children-container > .block-children > .ls-block img {
116 | height: 250px !important;
117 | width: auto !important;
118 | }
119 | [data-refs-self*='".gallery-h6"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
120 | [data-refs-self*='".gallery-h6"'] > .block-children-container > .block-children > .ls-block img {
121 | height: 300px !important;
122 | width: auto !important;
123 | }
124 | [data-refs-self*='".gallery-h7"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
125 | [data-refs-self*='".gallery-h7"'] > .block-children-container > .block-children > .ls-block img {
126 | height: 350px !important;
127 | width: auto !important;
128 | }
129 | [data-refs-self*='".gallery-h8"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
130 | [data-refs-self*='".gallery-h8"'] > .block-children-container > .block-children > .ls-block img {
131 | height: 400px !important;
132 | width: auto !important;
133 | }
134 | [data-refs-self*='".gallery-h9"'] > .block-children-container > .block-children > .ls-block > div > .block-content-wrapper,
135 | [data-refs-self*='".gallery-h9"'] > .block-children-container > .block-children > .ls-block img {
136 | height: 600px !important;
137 | width: auto !important;
138 | }
139 |
--------------------------------------------------------------------------------
/src/css/hide_dot_refs__hover.css:
--------------------------------------------------------------------------------
1 | /* hide tags, started with "." and show on hover */
2 |
3 |
4 | a.tag[data-ref^="."] {
5 | display: none;
6 | }
7 |
8 | .block-main-container:hover a.tag[data-ref^="."] {
9 | display: unset;
10 | }
11 |
--------------------------------------------------------------------------------
/src/css/hide_dot_refs__wrap.css:
--------------------------------------------------------------------------------
1 | /* hide tags, started with "." */
2 | /* (based on logtools plugin) */
3 |
4 | a.tag[data-ref^="."]::before {
5 | content: "…";
6 | font-size: 0.7rem;
7 | line-height: 1.0rem;
8 | }
9 |
10 | a.tag[data-ref^="."]:hover::before {
11 | content: "";
12 | padding-right: 0.7rem;
13 | }
14 |
15 | a.tag[data-ref^="."]:hover {
16 | line-height: 1.3rem;
17 | font-size: 1.0rem !important;
18 | transition: 0s;
19 |
20 | color: var(--ls-primary-text-color) !important;
21 | border: 1px solid var(--ls-primary-text-color);
22 | }
23 |
24 | a.tag[data-ref^="."] {
25 | font-family: monospace;
26 | font-size: 0px !important;
27 | transition: 0s;
28 |
29 | color: var(--ls-primary-text-color) !important;
30 | background-color: unset !important;
31 |
32 | border: none;
33 | border-radius: 3px;
34 | padding: 0 2px;
35 | }
36 |
--------------------------------------------------------------------------------
/src/css/spare_blocks.css:
--------------------------------------------------------------------------------
1 | /* make a spare space between 1-level blocks */
2 | .page:has(> div > div > div > .page-title) div.ls-block[level="1"]:not(:first-child),
3 | .page:has(> div > div > div > div > .journal-title) div.ls-block[level="1"]:not(:first-child) {
4 | margin-top: ${size}px;
5 | }
6 | .whiteboard div.ls-block[level="1"]:not(:first-child) {
7 | margin-top: 0px !important;
8 | }
9 | [data-refs-self*=".tabular"] > .block-children-container > .block-children > div.ls-block[level="1"]:not(:first-child) {
10 | margin-top: 0px !important;
11 | }
12 | .embed-page div.ls-block[level="1"]:not(:first-child) {
13 | margin-top: 0px !important;
14 | }
15 | .embed-block div.ls-block[level="1"]:not(:first-child) {
16 | margin-top: 0px !important;
17 | }
18 | #right-sidebar-container div.ls-block[level="1"]:not(:first-child) {
19 | margin-top: 0px !important;
20 | }
21 | .references-blocks div.ls-block[level="1"]:not(:first-child) {
22 | margin-top: 0px !important;
23 | }
24 | .references.page-unlinked div.ls-block[level="1"]:not(:first-child) {
25 | margin-top: 0px !important;
26 | }
27 |
--------------------------------------------------------------------------------
/src/css/tabular_view.css:
--------------------------------------------------------------------------------
1 | /* Tana-like tabular view */
2 | /* based on «Tabular Journals» by nmartin84 */
3 |
4 | /* header */
5 | .ls-block[data-refs-self*='".tabular"'] > .block-main-container > .block-content-wrapper > div > div > .block-content > .block-content-inner > div > :is(h1,h2) {
6 | border-bottom: none;
7 | margin: 0px;
8 | padding: 0px;
9 | }
10 |
11 | @keyframes hideAnimation {
12 | to {
13 | display: none;
14 | }
15 | }
16 | .ls-block[data-refs-self*='".tabular0"'] > .block-main-container {
17 | animation: hideAnimation 0s forwards;
18 | animation-delay: 1s;
19 | }
20 | .ls-block[data-refs-self*='".tabular0"'].selected > .block-main-container,
21 | .ls-block[data-refs-self*='".tabular0"'] > .block-main-container:hover,
22 | .ls-block[data-refs-self*='".tabular0"'] > .block-main-container:has(> .editor-wrapper),
23 | .ls-block[data-refs-self*='".tabular0"'] > .block-main-container:has(+ .block-children-container > .block-children-left-border:hover) {
24 | display: flex;
25 | animation: none;
26 | }
27 |
28 | .ls-block[data-refs-self*='".tabular0"'] > .block-children-container {
29 | margin-left: 0px;
30 | }
31 |
32 | /* table alignment */
33 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children {
34 | border-left: 0px;
35 |
36 | display: flex;
37 | flex-direction: column;
38 | }
39 |
40 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block:first-child {
41 | border-top: 1px solid var(--ls-guideline-color);
42 | }
43 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block {
44 | display: flex;
45 | border-bottom: 1px solid var(--ls-guideline-color);
46 | padding: 0px;
47 |
48 | flex-grow: 1;
49 | width: 100%;
50 | }
51 |
52 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container {
53 | padding: 2px 0px;
54 | }
55 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container.items-baseline {
56 | align-items: center;
57 | }
58 |
59 | /* left: size & header */
60 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .block-content-wrapper > div > div > .block-content > .block-content-inner {
61 | font-weight: 500;
62 | }
63 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .block-content-wrapper > div > div > .block-content > .block-content-inner,
64 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .block-content-wrapper > div > div > .block-content > .block-body,
65 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .editor-wrapper > .editor-inner {
66 | min-width: 165px;
67 | max-width: 165px;
68 | }
69 |
70 | /* left: plain text */
71 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .block-content-wrapper > div > div > .block-content > .block-body {
72 | font-size: 85%;
73 | opacity: 0.7;
74 | font-weight: 400;
75 | }
76 |
77 | /* left: hide brackets */
78 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .block-content-wrapper .page-reference .bracket {
79 | display: none;
80 | }
81 |
82 | /* right: adapt margins */
83 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-children-container {
84 | margin-left: 0px;
85 | width: 100%;
86 | }
87 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-children-container > .block-children {
88 | padding-bottom: 10px;
89 | }
90 |
91 | /* sub tabular */
92 | /* table alignment */
93 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children {
94 | border-left: 1px solid var(--ls-guideline-color);
95 | padding-top: 0px;
96 | padding-bottom: 0px;
97 | }
98 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block:first-child {
99 | border-top: 0px;
100 | }
101 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block:last-child {
102 | margin-bottom: -1px;
103 | border-bottom: 0px;
104 | }
105 | /* left: size */
106 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .block-content-wrapper > div > div > .block-content > .block-content-inner,
107 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .block-content-wrapper > div > div > .block-content > .block-body,
108 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-main-container > .editor-wrapper > .editor-inner {
109 | min-width: 115px;
110 | max-width: 115px;
111 | }
112 | /* right: adapt margins */
113 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block > .block-children-container > .block-children {
114 | padding-bottom: 7px;
115 | }
116 |
117 | /* zero sub tabular */
118 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular0"'] > .block-main-container {
119 | animation: none; /* to prevent animating when #.tabular0 is the root */
120 | border-right: 1px solid var(--ls-guideline-color);
121 | }
122 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular0"'] > .block-children-container {
123 | margin-left: -1px;
124 | }
125 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular0"'] > .block-children-container > .block-children-left-border {
126 | display: none;
127 | }
128 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular0"'] > .block-children-container > .block-children {
129 | border-left: 0px;
130 | }
131 | .ls-block[data-refs-self*='".tabular'] > .block-children-container > .block-children > .ls-block[data-refs-self*='".tabular0"'] > .block-children-container > .block-children > .ls-block > .block-main-container {
132 | display: none;
133 | }
134 |
--------------------------------------------------------------------------------
/src/entry.ts:
--------------------------------------------------------------------------------
1 | import '@logseq/libs'
2 |
3 | import { App } from './app'
4 |
5 |
6 | App(logseq)
7 |
--------------------------------------------------------------------------------
/src/features.ts:
--------------------------------------------------------------------------------
1 | import { PropertiesUtils, escapeForRegExp, provideStyle, setNativeValue } from './utils'
2 |
3 | import spareBlocksStyle from './css/spare_blocks.css?inline'
4 |
5 |
6 | /**
7 | * Sublime Text-like double ⌘→ or ⌘← to move cursor to the end / start of the text area
8 | */
9 | function improveCursorMovement_KeyDownListener(e: KeyboardEvent) {
10 | if (!e.target)
11 | return
12 |
13 | const target = e.target as HTMLInputElement
14 | if (target.tagName !== 'TEXTAREA')
15 | return
16 |
17 | if (!['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(e.key))
18 | return
19 |
20 |
21 | const homeEvent = (
22 | (e.key === 'ArrowLeft' && e.metaKey && !e.altKey && !e.ctrlKey) ||
23 | (e.key === 'Home')
24 | )
25 | const endEvent = (
26 | (e.key === 'ArrowRight' && e.metaKey && !e.altKey && !e.ctrlKey) ||
27 | (e.key === 'End')
28 | )
29 |
30 | if (target.selectionEnd === 0 || target.selectionStart === target.value.length)
31 | return
32 |
33 | if (endEvent && target.value[target.selectionEnd as number] === '\n') {
34 | if (e.shiftKey)
35 | target.selectionEnd = -1
36 | else
37 | target.selectionStart = -1
38 | } else if (homeEvent && target.value[(target.selectionStart as number) - 1] === '\n')
39 | target.selectionStart = 0
40 |
41 | // @ts-expect-error
42 | target.scrollIntoViewIfNeeded()
43 | }
44 | export function improveCursorMovementFeature(toggle: boolean) {
45 | if (toggle)
46 | parent.document.addEventListener('keydown', improveCursorMovement_KeyDownListener)
47 | else
48 | parent.document.removeEventListener('keydown', improveCursorMovement_KeyDownListener)
49 | }
50 |
51 | /**
52 | * TAB-trigger and access current page name on Search
53 | */
54 | async function improveSearch_KeyDownListener(e: KeyboardEvent) {
55 | if (!e.target)
56 | return
57 |
58 | const target = e.target as HTMLInputElement
59 | if (target.tagName !== 'INPUT' || target.id !== 'search')
60 | return
61 |
62 | if (!['ArrowLeft', 'Tab'].includes(e.key))
63 | return
64 |
65 | if ( !(
66 | !e.altKey && !e.shiftKey && !e.metaKey && !e.ctrlKey
67 | ))
68 | return
69 |
70 | if (e.key === 'ArrowLeft') {
71 | if (!target.value) {
72 | e.preventDefault()
73 | const page = await logseq.Editor.getCurrentPage()
74 | if (page)
75 | setNativeValue(target, page.originalName, true)
76 | }
77 | return
78 | }
79 |
80 | e.preventDefault()
81 |
82 | const panel = target.closest('.cp__cmdk')!
83 | const active = panel.querySelector('.\\!opacity-100')
84 | if (!active)
85 | return
86 |
87 | // skip blocks results
88 | if (!active.parentElement!.classList.contains('search-results'))
89 | return
90 |
91 | const label = active!.querySelector('.text-sm.font-medium')!.childNodes[0] as HTMLElement
92 |
93 | let text: string = ''
94 | if (label.tagName === 'DIV') { // page with alias
95 | const walker = document.createTreeWalker(label, NodeFilter.SHOW_TEXT, null)!
96 | text = walker.nextNode()!.textContent!
97 | } else if (label.tagName === 'SPAN') { // normal page, command, create command
98 | text = label.textContent!
99 | }
100 |
101 | if (e.key === 'Tab')
102 | if (target.value.toLowerCase() !== text.toLowerCase())
103 | setNativeValue(target, text, true)
104 | }
105 | export function improveSearchFeature(toggle: boolean) {
106 | if (toggle)
107 | parent.document.addEventListener('keydown', improveSearch_KeyDownListener, true)
108 | else
109 | parent.document.removeEventListener('keydown', improveSearch_KeyDownListener, true)
110 | }
111 |
112 | /**
113 | * Edit block on mouse click to reference with ALT/OPT pressed
114 | */
115 | async function improveMouseRefClick_MouseUpListener(e: MouseEvent) {
116 | if (!e.target)
117 | return
118 |
119 | if (! (e.altKey && !e.shiftKey && !e.metaKey && !e.ctrlKey))
120 | return
121 |
122 | let target = e.target as HTMLElement
123 | if (target.classList.contains('awLi-icon')) // Awesome Links: icon element
124 | target = target.parentElement!
125 | if (target.classList.contains('shml-anchor')) // Shorten My Links: anchor element
126 | target = target.parentElement!
127 | if (target.tagName !== 'A')
128 | return
129 |
130 | const isTag = target.classList.contains('tag')
131 | if (!target.classList.contains('page-ref') && !isTag)
132 | return
133 |
134 | const block = target.closest('.block-content') as HTMLElement
135 | // @ts-expect-error
136 | const uuid = block.attributes.blockid.value
137 | if (!uuid)
138 | return
139 |
140 | e.stopPropagation()
141 |
142 | // get block content
143 | let content = (await logseq.Editor.getBlock(uuid))!.content
144 | content = PropertiesUtils.deletePropertyFromString(content, 'id')
145 | //
146 |
147 | // get ref text and its width per char
148 | const textNode = target.childNodes[target.childNodes.length - 1]
149 | let text = textNode.textContent ?? ''
150 |
151 | const rect = target.getBoundingClientRect()
152 | let textWidth = rect.width
153 | for (const child of target.childNodes) {
154 | const e = child as HTMLElement
155 | if (child.nodeType !== document.TEXT_NODE) {
156 | textWidth -= e.getBoundingClientRect().width
157 | if (e.classList.contains('awLi-icon'))
158 | textWidth -= 5 // additional margin for icon
159 | }
160 | }
161 |
162 | const widthPerChar = textWidth / (text.length || 1)
163 | if (isTag)
164 | text = text.slice(1)
165 | //
166 |
167 | // find ref position in block
168 | const escapedText = escapeForRegExp(text)
169 | const refText = `\\[\\[${escapedText}\\]\\]`
170 |
171 | let startRefPos = -1
172 | if (isTag) {
173 | startRefPos = content.search(new RegExp(`#${escapedText}`, 'u'))
174 |
175 | if (startRefPos === -1) {
176 | startRefPos = content.search(new RegExp(`#${refText}`, 'u'))
177 | if (startRefPos !== -1)
178 | startRefPos += 2 // for [[
179 | }
180 |
181 | if (startRefPos !== -1)
182 | startRefPos += 1 // for #
183 | } else {
184 | startRefPos = content.search(new RegExp(refText, 'u'))
185 | if (startRefPos !== -1)
186 | startRefPos += 2 // for [[
187 | }
188 |
189 | if (startRefPos === -1) {
190 | // Shorten My Links plugin integration
191 | const shortenRefText = `/${escapedText}\\]\\]`
192 | startRefPos = content.search(new RegExp(shortenRefText, 'u'))
193 | if (startRefPos !== -1)
194 | startRefPos += 1 // for /
195 | }
196 |
197 | if (startRefPos === -1)
198 | startRefPos = 0
199 | //
200 |
201 | // find relative click position
202 | const relativeToEndPos = rect.right - e.x
203 | const charPosRight = Math.round(relativeToEndPos / widthPerChar)
204 | const charsOffset = Math.max(0, text.length - charPosRight)
205 | //
206 |
207 | await logseq.Editor.editBlock(uuid, {pos: startRefPos + charsOffset})
208 | }
209 | export function improveMouseRefClick(toggle: boolean) {
210 | if (toggle)
211 | parent.document.addEventListener('mouseup', improveMouseRefClick_MouseUpListener, true)
212 | else
213 | parent.document.removeEventListener('mouseup', improveMouseRefClick_MouseUpListener, true)
214 | }
215 |
216 | /**
217 | * CSS: Make spare space between 1-level blocks
218 | */
219 | export function spareBlocksFeature(size: number) {
220 | const key = 'spare-blocks'
221 | if (size > 0)
222 | provideStyle(key, spareBlocksStyle.replace('${size}', size.toString()))
223 | else
224 | provideStyle(key)
225 | }
226 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './logseq'
2 | export * from './other'
3 |
--------------------------------------------------------------------------------
/src/utils/logseq.ts:
--------------------------------------------------------------------------------
1 | import { BlockEntity, IBatchBlock, PageEntity } from '@logseq/libs/dist/LSPlugin.user'
2 |
3 | import { f, indexOfNth, p, sleep, unique } from './other'
4 |
5 |
6 | export async function getChosenBlocks(): Promise<[BlockEntity[], boolean]> {
7 | const selected = await logseq.Editor.getSelectedBlocks()
8 | if (selected)
9 | return [selected, true]
10 |
11 | const uuid = await logseq.Editor.checkEditing()
12 | if (!uuid)
13 | return [[], false]
14 |
15 | const editingBlock = await logseq.Editor.getBlock(uuid as string) as BlockEntity
16 |
17 | // to get ahead of Logseq block content saving process
18 | editingBlock.content = await logseq.Editor.getEditingBlockContent()
19 |
20 | return [ [editingBlock], false ]
21 | }
22 |
23 | /**
24 | * Sets the current editing block cursor position.
25 | * There is no need to check boundaries.
26 | * Negative indexing is supported.
27 | *
28 | * @param `pos`: new cursor position
29 | * @usage
30 | * setEditingCursorPosition(0) — set to the start
31 | * setEditingCursorPosition(-1) — set to the end
32 | * setEditingCursorPosition(-2) — set before the last char
33 | */
34 | export function setEditingCursorPosition(pos: number) {
35 | return setEditingCursorSelection(pos, pos)
36 | }
37 |
38 | function adjustIndexForLength(i, len) {
39 | if (i > len)
40 | i = len
41 | if (i < (-len - 1))
42 | i = -len - 1
43 | if (i < 0)
44 | i += len + 1
45 | return i
46 | }
47 |
48 | export function setEditingCursorSelection(start: number, end: number) {
49 | const editorElement = top!.document.getElementsByClassName('editor-wrapper')[0] as HTMLDivElement
50 | if (!editorElement)
51 | return false
52 | const textAreaElement = top!.document.getElementById(
53 | editorElement.id.replace(/^editor-/, '')
54 | ) as HTMLTextAreaElement
55 | if (!textAreaElement)
56 | return false
57 |
58 | const length = textAreaElement.value.length
59 | start = adjustIndexForLength(start, length)
60 | end = adjustIndexForLength(end, length)
61 |
62 | textAreaElement.selectionStart = start
63 | textAreaElement.selectionEnd = end
64 | return true
65 | }
66 |
67 | export function getEditingCursorSelection() {
68 | const editorElement = top!.document.getElementsByClassName('editor-wrapper')[0] as HTMLDivElement
69 | if (!editorElement)
70 | return null
71 |
72 | const textAreaElement = top!.document.getElementById(
73 | editorElement.id.replace(/^editor-/, '')
74 | ) as HTMLTextAreaElement
75 | if (!textAreaElement)
76 | return null
77 |
78 | return [textAreaElement.selectionStart, textAreaElement.selectionEnd]
79 | }
80 |
81 |
82 | export class PropertiesUtils {
83 | static readonly idProperty = 'id'
84 | static readonly headingProperty = 'heading'
85 | static readonly numberingProperty = 'logseq.orderListType'
86 | static readonly numberingProperty_ = 'logseq.order-list-type'
87 |
88 | // source: https://github.com/logseq/logseq/blob/master/deps/graph-parser/src/logseq/graph_parser/property.cljs#L81
89 | // logseq.* prefix need to be checked separately
90 | static readonly systemBlockProperties = [
91 | 'id', 'heading', 'collapsed',
92 | 'created-at', 'created_at',
93 | 'updated-at', 'last-modified-at', 'last_modified_at',
94 | ]
95 |
96 | static propertyContentFormat = f`\n?^[^\\S]*${'name'}::.*$`
97 | static propertyRestrictedChars = '\\s:;,^@#~"`/|\\(){}[\\]'
98 |
99 | static toCamelCase(text: string): string {
100 | text = text.toLowerCase()
101 | text = text.replaceAll(/(?<=-)(\w)/g, (m, ch) => ch.toUpperCase())
102 | text = text.replaceAll(/(?<=_)(\w)/g, (m, ch) => ch.toUpperCase())
103 | text = text.replaceAll('-', '')
104 | text = text.replaceAll('_', '')
105 | if (text)
106 | text = text[0].toLowerCase() + text.slice(1)
107 | return text
108 | }
109 | static fromCamelCase(text: string): string {
110 | return text.replaceAll(
111 | /\p{Uppercase_Letter}\p{Lowercase_Letter}/gu,
112 | (m) => '-' + m.toLowerCase(),
113 | )
114 | }
115 | static fromCamelCaseAll(properties: Record | undefined) {
116 | return Object.fromEntries(
117 | Object.entries(properties ?? {})
118 | .map(([k, v]) => [PropertiesUtils.fromCamelCase(k), v])
119 | )
120 | }
121 |
122 | static hasProperty(blockContent: string, name: string): boolean {
123 | // case when properties in content use different style of naming
124 | // logseq-prop-name
125 | // logseq_prop_name
126 | // logseq_prop-name
127 | // all this names is the same for logseq
128 | for (const n of [name, name.replaceAll('-', '_'), name.replaceAll('_', '-')]) {
129 | const propRegexp = PropertiesUtils.propertyContentFormat({name: n})
130 | const exists = new RegExp(propRegexp, 'gim').test(blockContent)
131 | if (exists)
132 | return true
133 | }
134 | return false
135 | }
136 | static deleteProperty(block: BlockEntity, name: string): void {
137 | const nameCamelCased = PropertiesUtils.toCamelCase(name)
138 |
139 | if (block.properties)
140 | delete block.properties[nameCamelCased]
141 | if (block.propertiesTextValues)
142 | delete block.propertiesTextValues[nameCamelCased]
143 |
144 | block.content = PropertiesUtils.deletePropertyFromString(block.content, name)
145 | }
146 | static deletePropertyFromString(content: string, name: string): string {
147 | // case when properties in content use different style of naming
148 | // logseq-prop-name
149 | // logseq_prop_name
150 | // logseq_prop-name
151 | // all this names is the same for logseq → we should erase all
152 | for (const n of [name, name.replaceAll('-', '_'), name.replaceAll('_', '-')]) {
153 | const propRegexp = PropertiesUtils.propertyContentFormat({name: n})
154 | content = content.replaceAll(new RegExp(propRegexp, 'gim'), '')
155 | }
156 | return content
157 | }
158 | static deleteAllProperties(content: string): string {
159 | for (const name of PropertiesUtils.getPropertyNames(content))
160 | content = PropertiesUtils.deletePropertyFromString(content, name)
161 | return content
162 | }
163 | static getPropertyNames(text: string): string[] {
164 | const propertyNames: string[] = []
165 | const propertyLine = new RegExp(PropertiesUtils.propertyContentFormat({
166 | name: `([^${PropertiesUtils.propertyRestrictedChars}]+)`
167 | }), 'gim')
168 | text.replaceAll(propertyLine, (m, name) => {propertyNames.push(name); return m})
169 | return propertyNames
170 | }
171 | }
172 |
173 | /**
174 | * Scroll to block if it has disappeared from view
175 | */
176 | export async function scrollToBlock(block: BlockEntity) {
177 | const position = (await logseq.Editor.getEditingCursorPosition())?.pos // editing mode?
178 | const view = await logseq.App.queryElementRect(`.ls-block[blockid="${block.uuid}"]`)
179 | if (view && (view.top < 0 || view.bottom > top!.window.innerHeight)) {
180 | const page = await logseq.Editor.getPage(block.page.id) as PageEntity
181 | logseq.Editor.scrollToBlockInPage(page.name, block.uuid)
182 |
183 | // .scrollToBlockInPage exists editing mode — return to it if necessary
184 | if (position) {
185 | await sleep(250)
186 | await logseq.Editor.editBlock(block.uuid, {pos: position})
187 | }
188 | }
189 | }
190 |
191 | export async function ensureChildrenIncluded(node: BlockEntity): Promise {
192 | // @ts-expect-error
193 | if (node.children?.at(0)?.uuid)
194 | return node
195 | return (await logseq.Editor.getBlock(node.uuid, {includeChildren: true}))!
196 | }
197 |
198 | export async function getBlocksWithReferences(root: BlockEntity): Promise {
199 | const blocksWithPersistedID = findPropertyInTree(root as IBatchBlock, PropertiesUtils.idProperty)
200 | const blocksAndItsReferences = (await Promise.all(
201 | blocksWithPersistedID.map(async (b): Promise<[BlockEntity, Number[]]> => {
202 | const block = b as BlockEntity
203 | const references = await findBlockReferences(block.uuid)
204 | return [block, references]
205 | })
206 | ))
207 | const blocksWithReferences = blocksAndItsReferences.filter(([b, rs]) => (rs.length !== 0))
208 | return blocksWithReferences.map(([b, rs]) => {
209 | b._references = rs
210 | return b
211 | })
212 | }
213 |
214 | export async function transformBlocksTreeByReplacing(
215 | root: BlockEntity,
216 | transformChildrenCallback: (blocks: BlockEntity[]) => BlockEntity[],
217 | ): Promise {
218 | root = await ensureChildrenIncluded(root)
219 | if (!root || !root.children || root.children.length === 0)
220 | return null // nothing to replace
221 |
222 | // METHOD: blocks removal to replace whole tree
223 | // but it is important to check if any block in the tree has references
224 | // (Logseq replaces references with it's text)
225 | const blocksWithReferences = await getBlocksWithReferences(root)
226 | if (blocksWithReferences.length !== 0)
227 | return null // blocks removal cannot be used
228 |
229 | const transformedBlocks = transformChildrenCallback(root.children as BlockEntity[])
230 | walkBlockTree({content: '', children: transformedBlocks as IBatchBlock[]}, (b, level) => {
231 | b.properties = PropertiesUtils.fromCamelCaseAll(b.properties ?? {})
232 | })
233 |
234 | // root is the first block in page
235 | if (root.left.id === root.page.id) {
236 | const page = await logseq.Editor.getPage(root.page.id)
237 | await logseq.Editor.removeBlock(root.uuid)
238 |
239 | // logseq bug: cannot use sibling next to root to insert whole tree to a page
240 | // so insert root of a tree separately from children
241 | const properties = PropertiesUtils.fromCamelCaseAll(root.properties)
242 | let prepended = await logseq.Editor.insertBlock(
243 | page!.uuid, root.content,
244 | {properties, before: true, customUUID: root.uuid},
245 | )
246 | if (!prepended) {
247 | // logseq bug: for empty pages need to change `before: true → false`
248 | prepended = (await logseq.Editor.insertBlock(
249 | page!.uuid, root.content,
250 | {properties, before: false, customUUID: root.uuid},
251 | ))!
252 | }
253 |
254 | await logseq.Editor.insertBatchBlock(
255 | prepended.uuid, transformedBlocks as IBatchBlock[],
256 | {before: false, sibling: false, keepUUID: true},
257 | )
258 | return prepended
259 | }
260 |
261 | // use root to insert whole tree at once
262 | const oldChildren = root.children
263 | root.children = transformedBlocks
264 |
265 | // root is the first child for its parent
266 | if (root.left.id === root.parent.id) {
267 | let parentRoot = (await logseq.Editor.getBlock(root.parent.id))!
268 | await logseq.Editor.removeBlock(root.uuid)
269 | await logseq.Editor.insertBatchBlock(
270 | parentRoot.uuid, root as IBatchBlock,
271 | {before: true, sibling: false, keepUUID: true},
272 | )
273 |
274 | // restore original object
275 | root.children = oldChildren
276 |
277 | parentRoot = (await logseq.Editor.getBlock(parentRoot.uuid, {includeChildren: true}))!
278 | return parentRoot.children![0] as BlockEntity
279 | }
280 |
281 | // root is not first child of parent and is not first block on page: it has sibling
282 | const preRoot = (await logseq.Editor.getPreviousSiblingBlock(root.uuid))!
283 | await logseq.Editor.removeBlock(root.uuid)
284 | await logseq.Editor.insertBatchBlock(
285 | preRoot.uuid, root as IBatchBlock,
286 | {before: false, sibling: true, keepUUID: true},
287 | )
288 |
289 | // restore original object
290 | root.children = oldChildren
291 |
292 | return (await logseq.Editor.getNextSiblingBlock(preRoot.uuid))!
293 | }
294 |
295 | export async function transformSelectedBlocksWithMovements(
296 | blocks: BlockEntity[],
297 | transformCallback: (blocks: BlockEntity[]) => BlockEntity[],
298 | ) {
299 | // METHOD: blocks movement
300 |
301 | // Logseq sorts selected blocks, so the first is the most upper one
302 | let insertionPoint = blocks[0]
303 |
304 | // Logseq bug: selected blocks can be duplicated (but sorted!)
305 | // just remove duplication
306 | blocks = unique(blocks, (b) => b.uuid)
307 |
308 | const transformed = transformCallback(blocks)
309 | for (const block of transformed) {
310 | // Logseq don't add movement to history if there was no movement at all
311 | // so we don't have to save API calls: just call .moveBlock on EVERY block
312 | await logseq.Editor.moveBlock(block.uuid, insertionPoint.uuid, {before: false})
313 | insertionPoint = block
314 | }
315 | }
316 |
317 | export async function walkBlockTreeAsync(
318 | root: WalkBlock,
319 | callback: (b: WalkBlock, lvl: number, data?: any) => Promise,
320 | level: number = 0,
321 | ): Promise {
322 | const data = {node: root as IBatchBlock}
323 | return {
324 | data,
325 | content: (await callback(root, level, data)) ?? '',
326 | children: await Promise.all(
327 | (root.children || []).map(
328 | async (b) => await walkBlockTreeAsync(b, callback, level + 1)
329 | ))
330 | }
331 | }
332 |
333 | export type WalkBlock = IBatchBlock & {data?: any}
334 | export function walkBlockTree(
335 | root: WalkBlock,
336 | callback: (b: WalkBlock, lvl: number, parent?: WalkBlock, data?: any) => string | void,
337 | level: number = 0,
338 | parent?: WalkBlock,
339 | ): WalkBlock {
340 | const data = {node: root as IBatchBlock}
341 | const content = callback(root, level, parent, data) ?? ''
342 | return {
343 | data,
344 | content,
345 | children: (root.children || []).map(
346 | (b) => walkBlockTree(b, callback, level + 1, root)
347 | ),
348 | }
349 | }
350 |
351 | export function reduceBlockTree(
352 | root: WalkBlock,
353 | callback: (b: WalkBlock, lvl: number, children: string[], data?: any) => string,
354 | level: number = 0,
355 | ): string {
356 | const children = (root.children || []).map(
357 | (b) => reduceBlockTree(b as WalkBlock, callback, level + 1)
358 | )
359 | return callback(root, level, children, root.data) ?? ''
360 | }
361 |
362 | export function findPropertyInTree(tree: IBatchBlock, propertyName: string): IBatchBlock[] {
363 | const found: IBatchBlock[] = []
364 | walkBlockTree(tree, (node, level) => {
365 | if (PropertiesUtils.hasProperty(node.content, propertyName))
366 | found.push(node)
367 | })
368 | return found
369 | }
370 | export function checkPropertyExistenceInTree(
371 | tree: IBatchBlock,
372 | {skipRoot = false, onlyUser = true }: { skipRoot?: boolean, onlyUser?: boolean },
373 | ): string[] {
374 | const found: string[] = []
375 | walkBlockTree(tree, (node, level) => {
376 | if (skipRoot && level === 0)
377 | return
378 | for (const name of Object.keys(node.properties ?? {})) {
379 | if (!found.includes(name))
380 | found.push(name)
381 | }
382 | })
383 | if (onlyUser)
384 | return found
385 | .filter((p) => !PropertiesUtils.systemBlockProperties.includes(p))
386 | .filter((p) => !/^logseq\..*/.test(p))
387 | return found
388 | }
389 |
390 | export async function findBlockReferences(uuid: string): Promise {
391 | const results = await logseq.DB.datascriptQuery(`
392 | [:find (pull ?b [:db/id])
393 | :where
394 | [?b :block/content ?c]
395 | [(clojure.string/includes? ?c "((${uuid}))")]
396 | ]`)
397 | if (!results)
398 | return []
399 | return results.flat().map((item) => item.id)
400 | }
401 |
402 | export function filterOutChildBlocks(blocks: BlockEntity[]): BlockEntity[] {
403 | const filtered: BlockEntity[] = []
404 |
405 | const uuids: string[] = []
406 | for (const block of blocks) {
407 | if (uuids.includes(block.uuid))
408 | continue
409 |
410 | walkBlockTree(block as IBatchBlock, (b, level) => {
411 | const block = b as BlockEntity
412 | if (!uuids.includes(block.uuid))
413 | uuids.push(block.uuid)
414 | })
415 |
416 | filtered.push(block)
417 | }
418 |
419 | return filtered
420 | }
421 |
422 | /**
423 | * Reason: logseq bug — `before: true` doesn't work for batch inserting
424 | */
425 | export async function insertBatchBlockBefore(
426 | srcBlock: BlockEntity,
427 | blocks: IBatchBlock | IBatchBlock[],
428 | opts?: Partial<{
429 | keepUUID: boolean;
430 | }>
431 | ) {
432 | // logseq bug: two space cut off from 2, 3, ... lines of all inserting blocks
433 | // so add fake two spaces to every line
434 | // issue: https://github.com/logseq/logseq/issues/10730
435 | let tree = blocks
436 | if (Array.isArray(blocks))
437 | tree = {content: '', children: blocks}
438 | walkBlockTree(tree as IBatchBlock, (b, level) => {
439 | b.content = b.content.trim().replaceAll(/\n^/gm, '\n ')})
440 |
441 | // first block in a page
442 | if (srcBlock.left.id === srcBlock.page.id) {
443 | // there is bug with first block in page: use pseudo block
444 | // issue: https://github.com/logseq/logseq/issues/10871
445 | const first = ( await logseq.Editor.insertBlock(
446 | srcBlock.uuid, 'ø', {before: true, sibling: true}) )!
447 | const result = await logseq.Editor.insertBatchBlock(
448 | first.uuid, blocks, {before: false, sibling: true, ...opts})
449 | await logseq.Editor.removeBlock(first.uuid)
450 | return result
451 | }
452 |
453 | const prev = await logseq.Editor.getPreviousSiblingBlock(srcBlock.uuid)
454 | if (prev) {
455 | // special handling for numbering
456 | // issue: https://github.com/logseq/logseq/issues/10729
457 | let numbering = undefined
458 | let properties = {}
459 | if (prev.properties) {
460 | numbering = prev.properties[PropertiesUtils.numberingProperty]
461 | delete prev.properties[PropertiesUtils.numberingProperty]
462 | properties = PropertiesUtils.fromCamelCaseAll(prev.properties)
463 | }
464 | if (numbering)
465 | await logseq.Editor.removeBlockProperty(prev.uuid, PropertiesUtils.numberingProperty_)
466 |
467 | const inserted = await logseq.Editor.insertBatchBlock(
468 | prev.uuid, blocks, {before: false, sibling: true, ...opts})
469 |
470 | if (numbering)
471 | await logseq.Editor.upsertBlockProperty(prev.uuid, PropertiesUtils.numberingProperty_, numbering)
472 |
473 | return inserted
474 | }
475 |
476 | // first block for parent
477 | const parent = ( await logseq.Editor.getBlock(srcBlock.parent.id) )!
478 |
479 | // special handling for numbering
480 | // issue: https://github.com/logseq/logseq/issues/10729
481 | let numbering = undefined
482 | let properties = {}
483 | if (parent.properties) {
484 | numbering = parent.properties[PropertiesUtils.numberingProperty]
485 | delete parent.properties[PropertiesUtils.numberingProperty]
486 | properties = PropertiesUtils.fromCamelCaseAll(parent.properties)
487 | }
488 | if (numbering)
489 | await logseq.Editor.removeBlockProperty(parent.uuid, PropertiesUtils.numberingProperty_)
490 |
491 | const inserted = await logseq.Editor.insertBatchBlock(
492 | parent.uuid, blocks, {before: true, sibling: false, ...opts})
493 |
494 | if (numbering)
495 | await logseq.Editor.upsertBlockProperty(parent.uuid, PropertiesUtils.numberingProperty_, numbering)
496 |
497 | return inserted
498 | }
499 |
500 | export function setNativeValue(element, value, needToDispatch) {
501 | const valueSetter = Object.getOwnPropertyDescriptor(element, 'value')!.set!
502 | const prototype = Object.getPrototypeOf(element)
503 | const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')!.set!
504 |
505 | if (valueSetter && valueSetter !== prototypeValueSetter)
506 | prototypeValueSetter.call(element, value)
507 | else
508 | valueSetter.call(element, value)
509 |
510 | if (needToDispatch)
511 | element.dispatchEvent(new Event('input', {bubbles: true}))
512 | }
513 |
514 | export function provideStyle(key: string, style: string = '') {
515 | const emptyStyle = '/**/'
516 | if (!style)
517 | style = emptyStyle
518 |
519 | logseq.provideStyle({key, style})
520 | return () => {logseq.provideStyle({key, style: emptyStyle})}
521 | }
522 |
--------------------------------------------------------------------------------
/src/utils/other.ts:
--------------------------------------------------------------------------------
1 | import { logseq as packageInfo } from '../../package.json'
2 |
3 |
4 | export const isMacOS = navigator.userAgent.toUpperCase().indexOf('MAC') >= 0
5 | export const isWindows = navigator.userAgent.toUpperCase().indexOf('WIN') >= 0
6 |
7 |
8 | /**
9 | * Tagged template printing function
10 | * @usage console.log(p`Hello, Logseq!`)
11 | * @usage console.debug(p``, {var})
12 | **/
13 | export function p(strings: any, ...values: any[]): string {
14 | const raw = String.raw({raw: strings}, ...values)
15 | const space = raw ? ' ' : ''
16 | return `#${packageInfo.id}:${space}${raw}`
17 | }
18 |
19 | /**
20 | * Format-string
21 | * @usage f`Hello, ${'name'}!`({name: 'Logseq'})
22 | * @usage
23 | * const format = f`Hello, ${'name'}!`
24 | * format({name: 'Logseq'}) // => 'Hello, Logseq!'
25 | **/
26 | export function f(strings: any, ...values: any[]): Function {
27 | return (format: {[i: string]: any}) => String.raw({raw: strings}, ...values.map(v => format[v]))
28 | }
29 |
30 | export function sleep(ms: number) {
31 | return new Promise(resolve => setTimeout(resolve, ms))
32 | }
33 |
34 | /**
35 | * Count substrings in string
36 | */
37 | export function countOf(string: string, substring: string): number {
38 | if (substring.length === 0)
39 | return 0
40 |
41 | const matchedCount = string.length - string.replaceAll(substring, '').length
42 | return matchedCount / substring.length
43 | }
44 |
45 | /**
46 | * Find index of Nth substring in string
47 | */
48 | export function indexOfNth(string: string, substring: string, count: number = 1): number | null {
49 | if (count <= 0)
50 | throw new Error('count param should be positive')
51 |
52 | const realCount = countOf(string, substring)
53 | if (count > realCount)
54 | return null
55 |
56 | return string.split(substring, count).join(substring).length
57 | }
58 |
59 | /**
60 | * Remove duplicates
61 | */
62 | export function unique(items: Array, keyFunction: (item: X) => any) {
63 | return items.filter((b, i, r) => {
64 | if (i === 0)
65 | return true
66 | return keyFunction(r[i - 1]) !== keyFunction(b)
67 | })
68 | }
69 |
70 |
71 | export function reduceTextWithLength(text: string, length: number, suffix = '...') {
72 | if (text.length <= length)
73 | return text
74 | return text.substring(0, length).trimEnd() + suffix
75 | }
76 |
77 | export function escapeForRegExp(str: string) {
78 | const specials = [
79 | '^', '$',
80 | '/', '.', '*', '+', '?', '|',
81 | '(', ')', '[', ']', '{', '}', '\\',
82 | ]
83 |
84 | const replacer = new RegExp('(\\' + specials.join('|\\') + ')', 'g')
85 | return str.replaceAll(replacer, '\\$1')
86 | }
87 |
88 |
89 | /**
90 | * source: https://stackoverflow.com/a/75643566
91 | */
92 | export function numberToLetters(x: number) {
93 | if (x <= 0)
94 | return ''
95 | const letters = numberToLetters(Math.floor((x - 1) / 26)) + String.fromCharCode((x - 1) % 26 + 65)
96 | return letters.toLowerCase()
97 | }
98 | export function lettersToNumber(letters: string) {
99 | return letters.toLowerCase().split('').reduce((acc, val) => acc * 26 + val.charCodeAt(0) - 64, 0)
100 | }
101 |
102 | /**
103 | * source: https://stackoverflow.com/a/70844631
104 | */
105 | const romanValues = {
106 | M: 1000, CM: 900, D: 500, CD: 400,
107 | C: 100, XC: 90, L: 50, XL: 40,
108 | X: 10, IX: 9, V: 5, IV: 4,
109 | I: 1,
110 | }
111 | export function numberToRoman(x: number = 0) {
112 | return Object.keys(romanValues).reduce((acc, key) => {
113 | while (x >= romanValues[key]) {
114 | acc += key
115 | x -= romanValues[key]
116 | }
117 | return acc
118 | }, '')
119 | }
120 | export function fromRoman(letters: string = '') {
121 | return Object.keys(romanValues).reduce((acc, key) => {
122 | while (letters.indexOf(key) === 0) {
123 | acc += romanValues[key];
124 | letters = letters.substr(key.length);
125 | }
126 | return acc;
127 | }, 0);
128 | }
129 |
--------------------------------------------------------------------------------
/src/views.ts:
--------------------------------------------------------------------------------
1 | import { provideStyle } from './utils'
2 |
3 | import tabularViewStyle from './css/tabular_view.css?inline'
4 | import borderViewStyle from './css/border_view.css?inline'
5 | import columnsViewStyle from './css/columns_view.css?inline'
6 | import galleryViewStyle from './css/gallery_view.css?inline'
7 |
8 | import hideDotRefs_WrapStyle from './css/hide_dot_refs__wrap.css?inline'
9 | import hideDotRefs_HoverStyle from './css/hide_dot_refs__hover.css?inline'
10 |
11 |
12 | /**
13 | * CSS: Hide references started with «.»
14 | */
15 | export function hideDotRefs(wrapToDots: boolean, hideWhenNotHovered?: boolean) {
16 | const key = 'dot-refs'
17 | if (!wrapToDots)
18 | provideStyle(key)
19 | else if (hideWhenNotHovered)
20 | provideStyle(key, hideDotRefs_WrapStyle + '\n' + hideDotRefs_HoverStyle)
21 | else
22 | provideStyle(key, hideDotRefs_WrapStyle)
23 | }
24 |
25 | /**
26 | * CSS: Tabular view
27 | */
28 | export function tabularView(toggle: boolean) {
29 | const key = 'tabular-view'
30 | if (toggle)
31 | provideStyle(key, tabularViewStyle)
32 | else
33 | provideStyle(key)
34 | }
35 |
36 | /**
37 | * CSS: Box view
38 | */
39 | export function borderView(toggle: boolean) {
40 | const key = 'border-view'
41 | if (toggle)
42 | provideStyle(key, borderViewStyle)
43 | else
44 | provideStyle(key)
45 | }
46 |
47 | /**
48 | * CSS: Columns view
49 | */
50 | export function columnsView(toggle: boolean) {
51 | const key = 'columns-view'
52 | if (toggle)
53 | provideStyle(key, columnsViewStyle)
54 | else
55 | provideStyle(key)
56 | }
57 |
58 | /**
59 | * CSS: Gallery view
60 | */
61 | export function galleryView(toggle: boolean) {
62 | const key = 'gallery-view'
63 | if (toggle)
64 | provideStyle(key, galleryViewStyle)
65 | else
66 | provideStyle(key)
67 | }
68 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "./src",
4 | ],
5 | "exclude": [
6 | "dist",
7 | "node_modules"
8 | ],
9 | "compilerOptions": {
10 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
11 |
12 | /* Projects */
13 | // "incremental": true, /* Enable incremental compilation */
14 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
15 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
16 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
17 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
18 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
19 |
20 | /* Language and Environment */
21 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
22 | "lib": ["DOM", "DOM.Iterable", "ESNext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
23 | // "jsx": "react-jsx", /* Specify what JSX code is generated. */
24 | "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
25 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
26 | // "jsxFactory": "h", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
27 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
28 | // "jsxImportSource": "preact", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
29 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
30 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
31 | "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
32 |
33 | /* Modules */
34 | "module": "ESNext", /* Specify what module code is generated. */
35 | // "rootDir": "./", /* Specify the root folder within your source files. */
36 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
37 | "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
38 | "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
39 | "@src/*": ["src/*"],
40 | },
41 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
42 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
43 | "types": ["vite/client", "node"], /* Specify type package names to be included without being referenced in a source file. */
44 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
45 | "resolveJsonModule": true, /* Enable importing .json files */
46 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */
47 |
48 | /* JavaScript Support */
49 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
50 | "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
52 |
53 | /* Emit */
54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
58 | // "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. */
59 | // "outDir": "./", /* Specify an output folder for all emitted files. */
60 | // "removeComments": true, /* Disable emitting comments. */
61 | "noEmit": true, /* Disable emitting files from a compilation. */
62 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
63 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
64 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
65 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
66 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
67 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
68 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
69 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
70 | // "newLine": "crlf", /* Set the newline character for emitting files. */
71 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
72 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
73 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
74 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
75 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
76 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
77 |
78 | /* Interop Constraints */
79 | "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
80 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
81 | "esModuleInterop": false, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
82 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
83 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
84 |
85 | /* Type Checking */
86 | "strict": true, /* Enable all strict type-checking options. */
87 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
88 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
89 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
90 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
91 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
92 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
93 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
94 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
95 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
96 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
97 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
98 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
99 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
100 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
101 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
102 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
103 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
104 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
105 |
106 | /* Completeness */
107 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
108 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/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 | }
19 | })
20 |
--------------------------------------------------------------------------------