47 |
appId:
48 |
49 |
${app?.appId}
50 |
51 |
52 |
API demo:
53 |
54 |
55 | System current time: {time}
56 |
57 |
58 |
59 |
Protyle demo: id = {blockID}
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | export function stringToSet(str: string): Set
{
2 | if (!str) {
3 | return new Set();
4 | }
5 | return new Set(
6 | str
7 | .split(/[,,]/)
8 | .map((item) => item.trim()) // remove space
9 | .filter((item) => item.length > 0) // remove empty string
10 | );
11 | }
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zxkmm/siyuan_doctree_fake_subfolder/84953de5462183529abe70d782e9281e2ccef591/src/index.scss
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, getFrontend, getBackend, showMessage } from "siyuan";
2 | import "@/index.scss";
3 | import { request, sql } from "./api";
4 | import { SettingUtils } from "./libs/setting-utils";
5 |
6 | import { stringToSet } from "./helpers";
7 |
8 | const STORAGE_NAME = "menu-config";
9 |
10 | enum DocTreeFakeSubfolderMode {
11 | Normal = "normal",
12 | Capture = "capture", // click to add item into list
13 | Reveal = "reveal", // click to view the actual document
14 | }
15 |
16 | export default class SiyuanDoctreeFakeSubfolder extends Plugin {
17 | private settingUtils: SettingUtils;
18 | private treatAsSubfolderIdSet: Set;
19 | private treatAsSubfolderEmojiSet: Set;
20 | private mode: DocTreeFakeSubfolderMode = DocTreeFakeSubfolderMode.Normal;
21 | private to_normal_mode_count = 0;
22 | //^ this is because when user enter the app, it not should display the "go to -ed normal mode noti",
23 | //thus count and only display for 2nd times whatsoever
24 | private frontend: string;
25 | private backend: string;
26 | private isDesktop: boolean;
27 | private isPhone: boolean;
28 | private isTablet: boolean;
29 |
30 |
31 | /*
32 | * @description: if toggle button has fn__hidden class, it means there is no sub document
33 | * @return: has subfolder: true, no dubfolder: false
34 | */
35 | private async isProvidedIdHasSubDocument(element: HTMLElement): Promise {
36 | const toggleElement = element.querySelector('.b3-list-item__toggle');
37 | if (!toggleElement) {
38 | return false;
39 | }
40 |
41 | return !toggleElement.classList.contains('fn__hidden');
42 | }
43 |
44 |
45 | /*
46 | * @description: return if the document is empty
47 | * @return: empty: true, not empty: false
48 | *
49 | * this APi were found by wilsons
50 | * Thanks!
51 | */
52 | private async isProvidedIdIsEmptyDocument(id: string): Promise {
53 | let data = {
54 | id: id
55 | };
56 | let url = '/api/block/getTreeStat';
57 | const res = await request(url, data);
58 | console.log(res, "res");
59 | return res.stat.runeCount === 0;
60 | }
61 |
62 | // unit test
63 | private async example() {
64 | const docId = "20250110144712-on18jor";
65 | const isEmpty = await this.isProvidedIdIsEmptyDocument(docId);
66 | if (isEmpty) {
67 | console.log("empty doc");
68 | } else {
69 | console.log("not empty doc");
70 | }
71 | }
72 |
73 | ifProvidedIdInTreatAsSubfolderSet(id: string) {
74 | return this.treatAsSubfolderIdSet.has(id);
75 | }
76 |
77 | ifProvidedLiAreUsingUserDefinedIdentifyIcon(li: HTMLElement) {
78 | const iconElement = li.querySelector(".b3-list-item__icon");
79 | if (!iconElement) {
80 | return false;
81 | }
82 |
83 | const iconText = iconElement.textContent;
84 | if (!iconText) {
85 | return false;
86 | }
87 |
88 | return this.treatAsSubfolderEmojiSet.has(iconText);
89 | }
90 |
91 | appendIdToTreatAsSubfolderSet(id: string) {
92 | this.treatAsSubfolderIdSet.add(id);
93 | }
94 |
95 | removeIdFromTreatAsSubfolderSet(id: string) {
96 | this.treatAsSubfolderIdSet.delete(id);
97 | }
98 |
99 | onClickDoctreeNode(nodeId: string) {
100 | // dom
101 | const element = document.querySelector(`li[data-node-id="${nodeId}"]`);
102 | if (!element) {
103 | console.warn(
104 | "did not found element, probably caused by theme or something"
105 | );
106 | return;
107 | }
108 |
109 | // path
110 | const id = element.getAttribute("data-node-id");
111 | if (!id) {
112 | console.warn(
113 | "node missing id attribute, probably caused by theme or something"
114 | );
115 | return;
116 | }
117 |
118 | // // debug hint
119 | // if (this.if_provided_id_in_treat_as_subfolder_set(id)) {
120 | // console.log(`forbid open: ${id} (node id: ${nodeId})`);
121 | // } else {
122 | // console.log(`allow open: ${id} (node id: ${nodeId})`);
123 | // }
124 | }
125 |
126 | captureToSetUnsetTreatAsSubfolderSetting(nodeId: string) {
127 | // fetch setting
128 | const idsStr = this.settingUtils.get(
129 | "ids_that_should_be_treated_as_subfolder"
130 | ) as string;
131 |
132 | // into temp set
133 | const tempSet = stringToSet(idsStr);
134 |
135 | // worker
136 | if (tempSet.has(nodeId)) {
137 | // delete
138 | tempSet.delete(nodeId);
139 | showMessage(
140 | `${this.i18n.recoveredThisDocumentFromSubfolder} ${nodeId}`,
141 | 2000,
142 | "error"
143 | ); //not err, just prettier with this style
144 | } else {
145 | // add
146 | tempSet.add(nodeId);
147 | showMessage(
148 | `${this.i18n.consideredThisDocumentAsSubfolder} ${nodeId}`,
149 | 2000
150 | );
151 | }
152 |
153 | // convery back
154 | const newIdsStr = Array.from(tempSet).join(",");
155 | this.settingUtils.set("ids_that_should_be_treated_as_subfolder", newIdsStr);
156 | this.settingUtils.save();
157 |
158 | // only need to update local var cuz when next boot it will load from settings anyway
159 | this.treatAsSubfolderIdSet = tempSet;
160 | }
161 |
162 | private initListener() {
163 | console.log("init_listener");
164 | // 等待 DOM
165 | setTimeout(() => {
166 | const elements = document.querySelectorAll(".b3-list--background");
167 | if (elements.length === 0) {
168 | console.warn(
169 | "not found .b3-list--background element, probably caused by theme or something"
170 | );
171 | return;
172 | }
173 |
174 | // NB: this lambda is aysnc
175 | const handleEvent = async (e: MouseEvent | TouchEvent) => {
176 | // this ev were added in later code and this is for checking
177 | if ((e as any).sf_openDoc) {
178 | return;
179 | }
180 |
181 | if (!e.target || !(e.target instanceof Element)) {
182 | console.warn(
183 | "event target is invalid, probably caused by theme or something"
184 | );
185 | return;
186 | }
187 |
188 | const listItem = e.target.closest(
189 | 'li[data-type="navigation-file"]'
190 | ) as HTMLElement | null;
191 | if (!listItem || e.target.closest(".b3-list-item__action")) {
192 | return; // handle allow clicked emoji/more/etc
193 | }
194 |
195 | const nodeId = listItem.getAttribute("data-node-id");
196 | const path = listItem.getAttribute("data-path");
197 |
198 | try {
199 | const clickedToggle = e.target.closest(".b3-list-item__toggle");
200 | const clickedIcon = e.target.closest(".b3-list-item__icon");
201 | // TODO: this probably already not needed anymore,
202 | //cuz toggle were already protected previously and emoji also protected earlier,
203 | //but leave as is for now
204 | const isSpecialClick = !!(clickedToggle || clickedIcon);
205 | /* ^ cast to bool */
206 |
207 | if (!nodeId || !this.mode) {
208 | return;
209 | }
210 |
211 | switch (this.mode) {
212 | case DocTreeFakeSubfolderMode.Normal:
213 | if (!isSpecialClick) {
214 | // cache settings in case if more chaotic
215 | const enableEmoji = this.settingUtils.get(
216 | "enable_using_emoji_as_subfolder_identify"
217 | );
218 | const enableId = this.settingUtils.get(
219 | "enable_using_id_as_subfolder_identify"
220 | );
221 | const enableAuto = this.settingUtils.get("enable_auto_mode");
222 |
223 | // emoji and id in list
224 | const isByEmoji =
225 | enableEmoji &&
226 | this.ifProvidedLiAreUsingUserDefinedIdentifyIcon(listItem);
227 | const isById =
228 | enableId && this.ifProvidedIdInTreatAsSubfolderSet(nodeId);
229 |
230 | if (isByEmoji || isById) {
231 | // Treat as folder
232 | e.preventDefault();
233 | e.stopPropagation();
234 | this.expandSubfolder(listItem);
235 | return false; // shouldn't waiste it of gone here
236 | } else {
237 | // empty check here
238 | e.preventDefault();
239 | e.stopPropagation();
240 |
241 |
242 | const isEmpty = await this.isProvidedIdIsEmptyDocument(
243 | nodeId
244 | );
245 | const hasSubDocument = await this.isProvidedIdHasSubDocument(
246 | listItem
247 | );
248 | console.log(isEmpty, hasSubDocument, "isEmpty, hasSubDocument");
249 | //TODO: it still look up db table even if auto mode disabled. Currently need it and it's not that lagging. will fix it later
250 | if (isEmpty && hasSubDocument && enableAuto) {
251 | // empty
252 | this.expandSubfolder(listItem);
253 | return false;
254 | } else {
255 | // not empty
256 | const newEvent = new MouseEvent("click", {
257 | bubbles: true,
258 | cancelable: true,
259 | });
260 | Object.defineProperty(newEvent, "sf_openDoc", {
261 | // add trigger ev to indicate if its a manual trigger
262 | value: true,
263 | });
264 | listItem.dispatchEvent(newEvent);
265 | return false;
266 | }
267 | }
268 | }
269 | // toggle click: always fallthrough is good enough
270 | break;
271 |
272 | case DocTreeFakeSubfolderMode.Capture:
273 | if (!isSpecialClick) {
274 | // capture worker
275 | this.captureToSetUnsetTreatAsSubfolderSetting(nodeId);
276 | }
277 | break;
278 |
279 | case DocTreeFakeSubfolderMode.Reveal:
280 | break;
281 | }
282 |
283 | // fallback
284 | this.onClickDoctreeNode(nodeId);
285 | } catch (err) {
286 | console.error("error when handle document tree node click:", err);
287 | }
288 | };
289 |
290 | let already_shown_the_incompatible_device_message = false;
291 |
292 | // TODO: this part were written by chatGPT, need to go back and check what exactly changed, but worked anyway
293 | // 监听事件时,不使用事件捕获阶段(第三个参数为 false 或省略)
294 | // 这样可以让思源自身的展开折叠逻辑正常执行
295 | elements.forEach((element) => {
296 | if (this.isDesktop) {
297 | element.addEventListener("click", handleEvent);
298 | element.addEventListener("touchend", handleEvent);
299 | } else if (this.isPhone || this.isTablet) {
300 | element.addEventListener("click", handleEvent);
301 | } else {
302 | if (!already_shown_the_incompatible_device_message) {
303 | showMessage(
304 | "文档树子文件夹插件:开发者没有为您的设备做准备,清将如下信息和你的设备型号反馈给开发者:" +
305 | this.frontend +
306 | " " +
307 | this.backend
308 | );
309 | showMessage(
310 | "Document Tree Subfolder Plugin: Developer did not prepare for your device, please feedback the following information to the developer: " +
311 | this.frontend +
312 | " " +
313 | this.backend
314 | );
315 | already_shown_the_incompatible_device_message = true;
316 | }
317 | }
318 | });
319 | }, 100);
320 | }
321 |
322 | expandSubfolder(item: HTMLElement) {
323 | // console.log(item, "expand_subfolder");
324 | if (!item) {
325 | console.warn("not found li item, probably caused by theme or something");
326 | return;
327 | }
328 |
329 | // the toggle btn
330 | const toggleButton = item.querySelector(".b3-list-item__toggle");
331 | if (!toggleButton) {
332 | console.warn(
333 | "arrow button missing. probably caused by theme or something"
334 | );
335 | return;
336 | }
337 |
338 | // simulate click
339 | const clickEvent = new MouseEvent("click", {
340 | view: window,
341 | bubbles: true,
342 | cancelable: true,
343 | });
344 |
345 | toggleButton.dispatchEvent(clickEvent);
346 | }
347 |
348 | async onload() {
349 | this.treatAsSubfolderIdSet = new Set();
350 | this.treatAsSubfolderEmojiSet = new Set();
351 |
352 | this.data[STORAGE_NAME] = { readonlyText: "Readonly" };
353 |
354 | this.settingUtils = new SettingUtils({
355 | plugin: this,
356 | name: STORAGE_NAME,
357 | });
358 | this.settingUtils.addItem({
359 | key: "begging",
360 | value: "",
361 | type: "hint",
362 | title: this.i18n.beggingTitle,
363 | description: this.i18n.beggingDesc,
364 | });
365 | this.settingUtils.addItem({
366 | key: "enable_auto_mode",
367 | value: true,
368 | type: "checkbox",
369 | title: this.i18n.enableAutoMode,
370 | description: this.i18n.enableAutoModeDesc,
371 | });
372 | this.settingUtils.addItem({
373 | key: "enable_using_emoji_as_subfolder_identify",
374 | value: true,
375 | type: "checkbox",
376 | title: this.i18n.enableUsingEmojiAsSubfolderIdentify,
377 | description: this.i18n.enableUsingEmojiAsSubfolderIdentifyDesc,
378 | });
379 | this.settingUtils.addItem({
380 | key: "emojies_that_should_be_treated_as_subfolder",
381 | value: "🗃️,📂,📁",
382 | type: "textarea",
383 | title: this.i18n.emojisThatShouldBeTreatedAsSubfolder,
384 | description: this.i18n.emojisThatShouldBeTreatedAsSubfolderDesc,
385 | });
386 | this.settingUtils.addItem({
387 | key: "enable_using_id_as_subfolder_identify",
388 | value: true,
389 | type: "checkbox",
390 | title: this.i18n.enableUsingIdAsSubfolderIdentify,
391 | description: this.i18n.enableUsingIdAsSubfolderIdentifyDesc,
392 | });
393 | this.settingUtils.addItem({
394 | key: "ids_that_should_be_treated_as_subfolder",
395 | value: "",
396 | type: "textarea",
397 | title: this.i18n.idsThatShouldBeTreatedAsSubfolder,
398 | description: this.i18n.idsThatShouldBeTreatedAsSubfolderDesc,
399 | });
400 | this.settingUtils.addItem({
401 | key: "enable_mode_switch_buttons",
402 | value: true,
403 | type: "checkbox",
404 | title: this.i18n.enableModeSwitchButtons,
405 | description: this.i18n.enableModeSwitchButtonsDesc,
406 | });
407 | this.settingUtils.addItem({
408 | key: "Hint",
409 | value: "",
410 | type: "hint",
411 | title: this.i18n.hintTitle,
412 | description: this.i18n.hintDesc,
413 | });
414 |
415 | try {
416 | this.settingUtils.load();
417 | } catch (error) {
418 | console.error(
419 | "Error loading settings storage, probably empty config json:",
420 | error
421 | );
422 | }
423 |
424 | this.addIcons(`
425 |
426 |
427 |
428 | `);
429 |
430 | this.addIcons(`
431 |
432 |
433 |
434 | `);
435 |
436 | this.addIcons(`
437 |
438 |
439 |
440 | `);
441 |
442 | this.frontend = getFrontend();
443 | this.backend = getBackend();
444 | this.isPhone =
445 | this.frontend === "mobile" || this.frontend === "browser-mobile";
446 | this.isTablet =
447 | ((this.frontend === "desktop" || this.frontend === "browser-desktop") &&
448 | this.backend === "ios") ||
449 | ((this.frontend === "desktop" || this.frontend === "browser-desktop") &&
450 | this.backend === "android") ||
451 | ((this.frontend === "desktop" || this.frontend === "browser-desktop") &&
452 | this.backend === "docker");
453 | this.isDesktop =
454 | (this.frontend === "desktop" || this.frontend === "browser-desktop") &&
455 | this.backend != "ios" &&
456 | this.backend != "android" &&
457 | this.backend != "docker";
458 | }
459 |
460 | private updateTopBarButtonStyles(
461 | activeMode: DocTreeFakeSubfolderMode,
462 | buttons: {
463 | normal: HTMLElement;
464 | capture: HTMLElement;
465 | reveal: HTMLElement;
466 | }
467 | ) {
468 | const setButtonStyle = (button: HTMLElement, isActive: boolean) => {
469 | button.style.backgroundColor = isActive
470 | ? "var(--b3-toolbar-color)"
471 | : "var(--b3-toolbar-background)";
472 | button.style.color = isActive
473 | ? "var(--b3-toolbar-background)"
474 | : "var(--b3-toolbar-color)";
475 | };
476 |
477 | setButtonStyle(
478 | buttons.normal,
479 | activeMode === DocTreeFakeSubfolderMode.Normal
480 | );
481 | setButtonStyle(
482 | buttons.capture,
483 | activeMode === DocTreeFakeSubfolderMode.Capture
484 | );
485 | setButtonStyle(
486 | buttons.reveal,
487 | activeMode === DocTreeFakeSubfolderMode.Reveal
488 | );
489 | }
490 |
491 | private switchMode(
492 | mode: DocTreeFakeSubfolderMode,
493 | buttons: {
494 | normal: HTMLElement;
495 | capture: HTMLElement;
496 | reveal: HTMLElement;
497 | }
498 | ) {
499 | this.to_normal_mode_count < 2 ? this.to_normal_mode_count++ : null;
500 | this.mode = mode;
501 | this.updateTopBarButtonStyles(mode, buttons);
502 |
503 | const messages = {
504 | [DocTreeFakeSubfolderMode.Normal]: {
505 | text: this.i18n.enterNormalMode,
506 | duration: 2000,
507 | },
508 | [DocTreeFakeSubfolderMode.Capture]: {
509 | text: this.i18n.enterCaptureMode,
510 | duration: 8000,
511 | },
512 | [DocTreeFakeSubfolderMode.Reveal]: {
513 | text: this.i18n.enterRevealMode,
514 | duration: 8000,
515 | },
516 | };
517 |
518 | const { text, duration } = messages[mode];
519 | if (this.to_normal_mode_count >= 2) {
520 | showMessage(text, duration);
521 | }
522 | }
523 |
524 | onLayoutReady() {
525 | console.log(this.frontend, this.backend);
526 | console.log(this.isPhone, this.isTablet, this.isDesktop);
527 | this.initListener();
528 | this.settingUtils.load();
529 |
530 | // load emoji setting
531 | const emojisStr = this.settingUtils.get(
532 | "emojies_that_should_be_treated_as_subfolder"
533 | ) as string;
534 | this.treatAsSubfolderEmojiSet = stringToSet(emojisStr);
535 |
536 | // id
537 | const idsStr = this.settingUtils.get(
538 | "ids_that_should_be_treated_as_subfolder"
539 | ) as string;
540 | this.treatAsSubfolderIdSet = stringToSet(idsStr);
541 |
542 | if (this.settingUtils.get("enable_mode_switch_buttons")) {
543 | const buttons = {
544 | normal: this.addTopBar({
545 | icon: "iconDoctreeFakeSubfolderNormalMode",
546 | title: this.i18n.normalMode,
547 | position: "left",
548 | callback: () =>
549 | this.switchMode(DocTreeFakeSubfolderMode.Normal, buttons),
550 | }),
551 | capture: this.addTopBar({
552 | icon: "iconDoctreeFakeSubfolderCaptureMode",
553 | title: this.i18n.captureMode,
554 | position: "left",
555 | callback: () =>
556 | this.switchMode(DocTreeFakeSubfolderMode.Capture, buttons),
557 | }),
558 | reveal: this.addTopBar({
559 | icon: "iconDoctreeFakeSubfolderRevealMode",
560 | title: this.i18n.revealMode,
561 | position: "left",
562 | callback: () =>
563 | this.switchMode(DocTreeFakeSubfolderMode.Reveal, buttons),
564 | }),
565 | };
566 |
567 | const ifShowCaptureModeButton = this.settingUtils.get("enable_auto_mode") &&
568 | !this.settingUtils.get("enable_using_id_as_subfolder_identify");
569 |
570 | if(ifShowCaptureModeButton){
571 | buttons.capture.style.display = "none";
572 | }
573 |
574 | // default to normal mode
575 | this.switchMode(DocTreeFakeSubfolderMode.Normal, buttons);
576 | }
577 | }
578 |
579 | async onunload() {}
580 |
581 | uninstall() {}
582 | }
583 |
--------------------------------------------------------------------------------
/src/libs/components/Form/form-input.svelte:
--------------------------------------------------------------------------------
1 |
33 |
34 | {#if type === "checkbox"}
35 |
36 |
44 | {:else if type === "textinput"}
45 |
46 |
56 | {:else if type === "textarea"}
57 |
63 | {:else if type === "number"}
64 |
74 | {:else if type === "button"}
75 |
76 |
87 | {:else if type === "select"}
88 |
89 |
102 | {:else if type == "slider"}
103 |
104 |
105 |
117 |
118 | {/if}
119 |
--------------------------------------------------------------------------------
/src/libs/components/Form/form-wrap.svelte:
--------------------------------------------------------------------------------
1 |
9 |
14 |
15 | {#if direction === "row"}
16 |
17 |
18 |
{title}
19 |
{@html description}
20 |
21 |
22 |
23 |
24 |
25 |
26 | {:else}
27 |
28 |
29 |
{title}
30 |
31 | {@html description}
32 |
33 |
34 |
35 |
36 |
37 | {/if}
38 |
39 |
54 |
--------------------------------------------------------------------------------
/src/libs/components/Form/index.ts:
--------------------------------------------------------------------------------
1 | import FormInput from './form-input.svelte';
2 | import FormWrap from './form-wrap.svelte';
3 |
4 | const Form = { Wrap: FormWrap, Input: FormInput };
5 | export default Form;
6 | export { FormInput, FormWrap };
7 |
--------------------------------------------------------------------------------
/src/libs/components/b3-typography.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/libs/components/setting-panel.svelte:
--------------------------------------------------------------------------------
1 |
9 |
29 |
30 |
31 |
32 | {#each settingItems as item (item.key)}
33 |
38 |
49 |
50 | {/each}
51 |
--------------------------------------------------------------------------------
/src/libs/const.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024 by frostime. All Rights Reserved.
3 | * @Author : frostime
4 | * @Date : 2024-06-08 20:36:30
5 | * @FilePath : /src/libs/const.ts
6 | * @LastEditTime : 2024-06-08 20:48:06
7 | * @Description :
8 | */
9 |
10 |
11 | export const BlockType2NodeType: {[key in BlockType]: string} = {
12 | d: 'NodeDocument',
13 | p: 'NodeParagraph',
14 | query_embed: 'NodeBlockQueryEmbed',
15 | l: 'NodeList',
16 | i: 'NodeListItem',
17 | h: 'NodeHeading',
18 | iframe: 'NodeIFrame',
19 | tb: 'NodeThematicBreak',
20 | b: 'NodeBlockquote',
21 | s: 'NodeSuperBlock',
22 | c: 'NodeCodeBlock',
23 | widget: 'NodeWidget',
24 | t: 'NodeTable',
25 | html: 'NodeHTMLBlock',
26 | m: 'NodeMathBlock',
27 | av: 'NodeAttributeView',
28 | audio: 'NodeAudio'
29 | }
30 |
31 |
32 | export const NodeIcons = {
33 | NodeAttributeView: {
34 | icon: "iconDatabase"
35 | },
36 | NodeAudio: {
37 | icon: "iconRecord"
38 | },
39 | NodeBlockQueryEmbed: {
40 | icon: "iconSQL"
41 | },
42 | NodeBlockquote: {
43 | icon: "iconQuote"
44 | },
45 | NodeCodeBlock: {
46 | icon: "iconCode"
47 | },
48 | NodeDocument: {
49 | icon: "iconFile"
50 | },
51 | NodeHTMLBlock: {
52 | icon: "iconHTML5"
53 | },
54 | NodeHeading: {
55 | icon: "iconHeadings",
56 | subtypes: {
57 | h1: { icon: "iconH1" },
58 | h2: { icon: "iconH2" },
59 | h3: { icon: "iconH3" },
60 | h4: { icon: "iconH4" },
61 | h5: { icon: "iconH5" },
62 | h6: { icon: "iconH6" }
63 | }
64 | },
65 | NodeIFrame: {
66 | icon: "iconLanguage"
67 | },
68 | NodeList: {
69 | subtypes: {
70 | o: { icon: "iconOrderedList" },
71 | t: { icon: "iconCheck" },
72 | u: { icon: "iconList" }
73 | }
74 | },
75 | NodeListItem: {
76 | icon: "iconListItem"
77 | },
78 | NodeMathBlock: {
79 | icon: "iconMath"
80 | },
81 | NodeParagraph: {
82 | icon: "iconParagraph"
83 | },
84 | NodeSuperBlock: {
85 | icon: "iconSuper"
86 | },
87 | NodeTable: {
88 | icon: "iconTable"
89 | },
90 | NodeThematicBreak: {
91 | icon: "iconLine"
92 | },
93 | NodeVideo: {
94 | icon: "iconVideo"
95 | },
96 | NodeWidget: {
97 | icon: "iconBoth"
98 | }
99 | };
100 |
--------------------------------------------------------------------------------
/src/libs/dialog.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024 by frostime. All Rights Reserved.
3 | * @Author : frostime
4 | * @Date : 2024-03-23 21:37:33
5 | * @FilePath : /src/libs/dialog.ts
6 | * @LastEditTime : 2024-10-16 14:31:04
7 | * @Description : Kits about dialogs
8 | */
9 | import { Dialog } from "siyuan";
10 | import { type SvelteComponent } from "svelte";
11 |
12 | export const inputDialog = (args: {
13 | title: string, placeholder?: string, defaultText?: string,
14 | confirm?: (text: string) => void, cancel?: () => void,
15 | width?: string, height?: string
16 | }) => {
17 | const dialog = new Dialog({
18 | title: args.title,
19 | content: `
22 |
23 |
24 |
25 |
`,
26 | width: args.width ?? "520px",
27 | height: args.height
28 | });
29 | const target: HTMLTextAreaElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword>textarea");
30 | const btnsElement = dialog.element.querySelectorAll(".b3-button");
31 | btnsElement[0].addEventListener("click", () => {
32 | if (args?.cancel) {
33 | args.cancel();
34 | }
35 | dialog.destroy();
36 | });
37 | btnsElement[1].addEventListener("click", () => {
38 | if (args?.confirm) {
39 | args.confirm(target.value);
40 | }
41 | dialog.destroy();
42 | });
43 | };
44 |
45 | export const inputDialogSync = async (args: {
46 | title: string, placeholder?: string, defaultText?: string,
47 | width?: string, height?: string
48 | }) => {
49 | return new Promise((resolve) => {
50 | let newargs = {
51 | ...args, confirm: (text) => {
52 | resolve(text);
53 | }, cancel: () => {
54 | resolve(null);
55 | }
56 | };
57 | inputDialog(newargs);
58 | });
59 | }
60 |
61 |
62 | interface IConfirmDialogArgs {
63 | title: string;
64 | content: string | HTMLElement;
65 | confirm?: (ele?: HTMLElement) => void;
66 | cancel?: (ele?: HTMLElement) => void;
67 | width?: string;
68 | height?: string;
69 | }
70 |
71 | export const confirmDialog = (args: IConfirmDialogArgs) => {
72 | const { title, content, confirm, cancel, width, height } = args;
73 |
74 | const dialog = new Dialog({
75 | title,
76 | content: `
80 |
81 |
82 |
83 |
`,
84 | width: width,
85 | height: height
86 | });
87 |
88 | const target: HTMLElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword");
89 | if (typeof content === "string") {
90 | target.innerHTML = content;
91 | } else {
92 | target.appendChild(content);
93 | }
94 |
95 | const btnsElement = dialog.element.querySelectorAll(".b3-button");
96 | btnsElement[0].addEventListener("click", () => {
97 | if (cancel) {
98 | cancel(target);
99 | }
100 | dialog.destroy();
101 | });
102 | btnsElement[1].addEventListener("click", () => {
103 | if (confirm) {
104 | confirm(target);
105 | }
106 | dialog.destroy();
107 | });
108 | };
109 |
110 |
111 | export const confirmDialogSync = async (args: IConfirmDialogArgs) => {
112 | return new Promise((resolve) => {
113 | let newargs = {
114 | ...args, confirm: (ele: HTMLElement) => {
115 | resolve(ele);
116 | }, cancel: (ele: HTMLElement) => {
117 | resolve(ele);
118 | }
119 | };
120 | confirmDialog(newargs);
121 | });
122 | };
123 |
124 |
125 | export const simpleDialog = (args: {
126 | title: string, ele: HTMLElement | DocumentFragment,
127 | width?: string, height?: string,
128 | callback?: () => void;
129 | }) => {
130 | const dialog = new Dialog({
131 | title: args.title,
132 | content: ``,
133 | width: args.width,
134 | height: args.height,
135 | destroyCallback: args.callback
136 | });
137 | dialog.element.querySelector(".dialog-content").appendChild(args.ele);
138 | return {
139 | dialog,
140 | close: dialog.destroy.bind(dialog)
141 | };
142 | }
143 |
144 |
145 | export const svelteDialog = (args: {
146 | title: string, constructor: (container: HTMLElement) => SvelteComponent,
147 | width?: string, height?: string,
148 | callback?: () => void;
149 | }) => {
150 | let container = document.createElement('div')
151 | container.style.display = 'contents';
152 | let component = args.constructor(container);
153 | const { dialog, close } = simpleDialog({
154 | ...args, ele: container, callback: () => {
155 | component.$destroy();
156 | if (args.callback) args.callback();
157 | }
158 | });
159 | return {
160 | component,
161 | dialog,
162 | close
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/libs/index.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024 by frostime. All Rights Reserved.
3 | * @Author : frostime
4 | * @Date : 2024-04-19 18:30:12
5 | * @FilePath : /src/libs/index.d.ts
6 | * @LastEditTime : 2024-04-30 16:39:54
7 | * @Description :
8 | */
9 | type TSettingItemType = "checkbox" | "select" | "textinput" | "textarea" | "number" | "slider" | "button" | "hint" | "custom";
10 |
11 | interface ISettingItemCore {
12 | type: TSettingItemType;
13 | key: string;
14 | value: any;
15 | placeholder?: string;
16 | slider?: {
17 | min: number;
18 | max: number;
19 | step: number;
20 | };
21 | options?: { [key: string | number]: string };
22 | button?: {
23 | label: string;
24 | callback: () => void;
25 | }
26 | }
27 |
28 | interface ISettingItem extends ISettingItemCore {
29 | title: string;
30 | description: string;
31 | direction?: "row" | "column";
32 | }
33 |
34 |
35 | //Interface for setting-utils
36 | interface ISettingUtilsItem extends ISettingItem {
37 | action?: {
38 | callback: () => void;
39 | }
40 | createElement?: (currentVal: any) => HTMLElement;
41 | getEleVal?: (ele: HTMLElement) => any;
42 | setEleVal?: (ele: HTMLElement, val: any) => void;
43 | }
44 |
--------------------------------------------------------------------------------
/src/libs/promise-pool.ts:
--------------------------------------------------------------------------------
1 | export default class PromiseLimitPool {
2 | private maxConcurrent: number;
3 | private currentRunning = 0;
4 | private queue: (() => void)[] = [];
5 | private promises: Promise[] = [];
6 |
7 | constructor(maxConcurrent: number) {
8 | this.maxConcurrent = maxConcurrent;
9 | }
10 |
11 | add(fn: () => Promise): void {
12 | const promise = new Promise((resolve, reject) => {
13 | const run = async () => {
14 | try {
15 | this.currentRunning++;
16 | const result = await fn();
17 | resolve(result);
18 | } catch (error) {
19 | reject(error);
20 | } finally {
21 | this.currentRunning--;
22 | this.next();
23 | }
24 | };
25 |
26 | if (this.currentRunning < this.maxConcurrent) {
27 | run();
28 | } else {
29 | this.queue.push(run);
30 | }
31 | });
32 | this.promises.push(promise);
33 | }
34 |
35 | async awaitAll(): Promise {
36 | return Promise.all(this.promises);
37 | }
38 |
39 | /**
40 | * Handles the next task in the queue.
41 | */
42 | private next(): void {
43 | if (this.queue.length > 0 && this.currentRunning < this.maxConcurrent) {
44 | const nextRun = this.queue.shift()!;
45 | nextRun();
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/libs/setting-utils.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2023 by frostime. All Rights Reserved.
3 | * @Author : frostime
4 | * @Date : 2023-12-17 18:28:19
5 | * @FilePath : /src/libs/setting-utils.ts
6 | * @LastEditTime : 2024-05-01 17:44:16
7 | * @Description :
8 | */
9 |
10 | import { Plugin, Setting } from 'siyuan';
11 |
12 |
13 | /**
14 | * The default function to get the value of the element
15 | * @param type
16 | * @returns
17 | */
18 | const createDefaultGetter = (type: TSettingItemType) => {
19 | let getter: (ele: HTMLElement) => any;
20 | switch (type) {
21 | case 'checkbox':
22 | getter = (ele: HTMLInputElement) => {
23 | return ele.checked;
24 | };
25 | break;
26 | case 'select':
27 | case 'slider':
28 | case 'textinput':
29 | case 'textarea':
30 | getter = (ele: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => {
31 | return ele.value;
32 | };
33 | break;
34 | case 'number':
35 | getter = (ele: HTMLInputElement) => {
36 | return parseInt(ele.value);
37 | }
38 | break;
39 | default:
40 | getter = () => null;
41 | break;
42 | }
43 | return getter;
44 | }
45 |
46 |
47 | /**
48 | * The default function to set the value of the element
49 | * @param type
50 | * @returns
51 | */
52 | const createDefaultSetter = (type: TSettingItemType) => {
53 | let setter: (ele: HTMLElement, value: any) => void;
54 | switch (type) {
55 | case 'checkbox':
56 | setter = (ele: HTMLInputElement, value: any) => {
57 | ele.checked = value;
58 | };
59 | break;
60 | case 'select':
61 | case 'slider':
62 | case 'textinput':
63 | case 'textarea':
64 | case 'number':
65 | setter = (ele: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, value: any) => {
66 | ele.value = value;
67 | };
68 | break;
69 | default:
70 | setter = () => {};
71 | break;
72 | }
73 | return setter;
74 |
75 | }
76 |
77 |
78 | export class SettingUtils {
79 | plugin: Plugin;
80 | name: string;
81 | file: string;
82 |
83 | settings: Map = new Map();
84 | elements: Map = new Map();
85 |
86 | constructor(args: {
87 | plugin: Plugin,
88 | name?: string,
89 | callback?: (data: any) => void,
90 | width?: string,
91 | height?: string
92 | }) {
93 | this.name = args.name ?? 'settings';
94 | this.plugin = args.plugin;
95 | this.file = this.name.endsWith('.json') ? this.name : `${this.name}.json`;
96 | this.plugin.setting = new Setting({
97 | width: args.width,
98 | height: args.height,
99 | confirmCallback: () => {
100 | for (let key of this.settings.keys()) {
101 | this.updateValueFromElement(key);
102 | }
103 | let data = this.dump();
104 | if (args.callback !== undefined) {
105 | args.callback(data);
106 | }
107 | this.plugin.data[this.name] = data;
108 | this.save(data);
109 |
110 | window.location.reload();
111 | },
112 | destroyCallback: () => {
113 | //Restore the original value
114 | for (let key of this.settings.keys()) {
115 | this.updateElementFromValue(key);
116 | }
117 | }
118 | });
119 | }
120 |
121 | async load() {
122 | let data = await this.plugin.loadData(this.file);
123 | console.debug('Load config:', data);
124 | if (data) {
125 | for (let [key, item] of this.settings) {
126 | item.value = data?.[key] ?? item.value;
127 | }
128 | }
129 | this.plugin.data[this.name] = this.dump();
130 | return data;
131 | }
132 |
133 | async save(data?: any) {
134 | data = data ?? this.dump();
135 | await this.plugin.saveData(this.file, this.dump());
136 | console.debug('Save config:', data);
137 | return data;
138 | }
139 |
140 | /**
141 | * read the data after saving
142 | * @param key key name
143 | * @returns setting item value
144 | */
145 | get(key: string) {
146 | return this.settings.get(key)?.value;
147 | }
148 |
149 | /**
150 | * Set data to this.settings,
151 | * but do not save it to the configuration file
152 | * @param key key name
153 | * @param value value
154 | */
155 | set(key: string, value: any) {
156 | let item = this.settings.get(key);
157 | if (item) {
158 | item.value = value;
159 | this.updateElementFromValue(key);
160 | }
161 | }
162 |
163 | /**
164 | * Set and save setting item value
165 | * If you want to set and save immediately you can use this method
166 | * @param key key name
167 | * @param value value
168 | */
169 | async setAndSave(key: string, value: any) {
170 | let item = this.settings.get(key);
171 | if (item) {
172 | item.value = value;
173 | this.updateElementFromValue(key);
174 | await this.save();
175 | }
176 | }
177 |
178 | /**
179 | * Read in the value of element instead of setting obj in real time
180 | * @param key key name
181 | * @param apply whether to apply the value to the setting object
182 | * if true, the value will be applied to the setting object
183 | * @returns value in html
184 | */
185 | take(key: string, apply: boolean = false) {
186 | let item = this.settings.get(key);
187 | let element = this.elements.get(key) as any;
188 | if (!element) {
189 | return
190 | }
191 | if (apply) {
192 | this.updateValueFromElement(key);
193 | }
194 | return item.getEleVal(element);
195 | }
196 |
197 | /**
198 | * Read data from html and save it
199 | * @param key key name
200 | * @param value value
201 | * @return value in html
202 | */
203 | async takeAndSave(key: string) {
204 | let value = this.take(key, true);
205 | await this.save();
206 | return value;
207 | }
208 |
209 | /**
210 | * Disable setting item
211 | * @param key key name
212 | */
213 | disable(key: string) {
214 | let element = this.elements.get(key) as any;
215 | if (element) {
216 | element.disabled = true;
217 | }
218 | }
219 |
220 | /**
221 | * Enable setting item
222 | * @param key key name
223 | */
224 | enable(key: string) {
225 | let element = this.elements.get(key) as any;
226 | if (element) {
227 | element.disabled = false;
228 | }
229 | }
230 |
231 | /**
232 | * 将设置项目导出为 JSON 对象
233 | * @returns object
234 | */
235 | dump(): Object {
236 | let data: any = {};
237 | for (let [key, item] of this.settings) {
238 | if (item.type === 'button') continue;
239 | data[key] = item.value;
240 | }
241 | return data;
242 | }
243 |
244 | addItem(item: ISettingUtilsItem) {
245 | this.settings.set(item.key, item);
246 | const IsCustom = item.type === 'custom';
247 | let error = IsCustom && (item.createElement === undefined || item.getEleVal === undefined || item.setEleVal === undefined);
248 | if (error) {
249 | console.error('The custom setting item must have createElement, getEleVal and setEleVal methods');
250 | return;
251 | }
252 |
253 | if (item.getEleVal === undefined) {
254 | item.getEleVal = createDefaultGetter(item.type);
255 | }
256 | if (item.setEleVal === undefined) {
257 | item.setEleVal = createDefaultSetter(item.type);
258 | }
259 |
260 | if (item.createElement === undefined) {
261 | let itemElement = this.createDefaultElement(item);
262 | this.elements.set(item.key, itemElement);
263 | this.plugin.setting.addItem({
264 | title: item.title,
265 | description: item?.description,
266 | direction: item?.direction,
267 | createActionElement: () => {
268 | this.updateElementFromValue(item.key);
269 | let element = this.getElement(item.key);
270 | return element;
271 | }
272 | });
273 | } else {
274 | this.plugin.setting.addItem({
275 | title: item.title,
276 | description: item?.description,
277 | direction: item?.direction,
278 | createActionElement: () => {
279 | let val = this.get(item.key);
280 | let element = item.createElement(val);
281 | this.elements.set(item.key, element);
282 | return element;
283 | }
284 | });
285 | }
286 | }
287 |
288 | createDefaultElement(item: ISettingUtilsItem) {
289 | let itemElement: HTMLElement;
290 | //阻止思源内置的回车键确认
291 | const preventEnterConfirm = (e) => {
292 | if (e.key === 'Enter') {
293 | e.preventDefault();
294 | e.stopImmediatePropagation();
295 | }
296 | }
297 | switch (item.type) {
298 | case 'checkbox':
299 | let element: HTMLInputElement = document.createElement('input');
300 | element.type = 'checkbox';
301 | element.checked = item.value;
302 | element.className = "b3-switch fn__flex-center";
303 | itemElement = element;
304 | element.onchange = item.action?.callback ?? (() => { });
305 | break;
306 | case 'select':
307 | let selectElement: HTMLSelectElement = document.createElement('select');
308 | selectElement.className = "b3-select fn__flex-center fn__size200";
309 | let options = item?.options ?? {};
310 | for (let val in options) {
311 | let optionElement = document.createElement('option');
312 | let text = options[val];
313 | optionElement.value = val;
314 | optionElement.text = text;
315 | selectElement.appendChild(optionElement);
316 | }
317 | selectElement.value = item.value;
318 | selectElement.onchange = item.action?.callback ?? (() => { });
319 | itemElement = selectElement;
320 | break;
321 | case 'slider':
322 | let sliderElement: HTMLInputElement = document.createElement('input');
323 | sliderElement.type = 'range';
324 | sliderElement.className = 'b3-slider fn__size200 b3-tooltips b3-tooltips__n';
325 | sliderElement.ariaLabel = item.value;
326 | sliderElement.min = item.slider?.min.toString() ?? '0';
327 | sliderElement.max = item.slider?.max.toString() ?? '100';
328 | sliderElement.step = item.slider?.step.toString() ?? '1';
329 | sliderElement.value = item.value;
330 | sliderElement.onchange = () => {
331 | sliderElement.ariaLabel = sliderElement.value;
332 | item.action?.callback();
333 | }
334 | itemElement = sliderElement;
335 | break;
336 | case 'textinput':
337 | let textInputElement: HTMLInputElement = document.createElement('input');
338 | textInputElement.className = 'b3-text-field fn__flex-center fn__size200';
339 | textInputElement.value = item.value;
340 | textInputElement.onchange = item.action?.callback ?? (() => { });
341 | itemElement = textInputElement;
342 | textInputElement.addEventListener('keydown', preventEnterConfirm);
343 | break;
344 | case 'textarea':
345 | let textareaElement: HTMLTextAreaElement = document.createElement('textarea');
346 | textareaElement.className = "b3-text-field fn__block";
347 | textareaElement.value = item.value;
348 | textareaElement.onchange = item.action?.callback ?? (() => { });
349 | itemElement = textareaElement;
350 | break;
351 | case 'number':
352 | let numberElement: HTMLInputElement = document.createElement('input');
353 | numberElement.type = 'number';
354 | numberElement.className = 'b3-text-field fn__flex-center fn__size200';
355 | numberElement.value = item.value;
356 | itemElement = numberElement;
357 | numberElement.addEventListener('keydown', preventEnterConfirm);
358 | break;
359 | case 'button':
360 | let buttonElement: HTMLButtonElement = document.createElement('button');
361 | buttonElement.className = "b3-button b3-button--outline fn__flex-center fn__size200";
362 | buttonElement.innerText = item.button?.label ?? 'Button';
363 | buttonElement.onclick = item.button?.callback ?? (() => { });
364 | itemElement = buttonElement;
365 | break;
366 | case 'hint':
367 | let hintElement: HTMLElement = document.createElement('div');
368 | hintElement.className = 'b3-label fn__flex-center';
369 | itemElement = hintElement;
370 | break;
371 | }
372 | return itemElement;
373 | }
374 |
375 | /**
376 | * return the setting element
377 | * @param key key name
378 | * @returns element
379 | */
380 | getElement(key: string) {
381 | // let item = this.settings.get(key);
382 | let element = this.elements.get(key) as any;
383 | return element;
384 | }
385 |
386 | private updateValueFromElement(key: string) {
387 | let item = this.settings.get(key);
388 | if (item.type === 'button') return;
389 | let element = this.elements.get(key) as any;
390 | item.value = item.getEleVal(element);
391 | }
392 |
393 | private updateElementFromValue(key: string) {
394 | let item = this.settings.get(key);
395 | if (item.type === 'button') return;
396 | let element = this.elements.get(key) as any;
397 | item.setEleVal(element, item.value);
398 | }
399 | }
--------------------------------------------------------------------------------
/src/setting-example.svelte:
--------------------------------------------------------------------------------
1 |
90 |
91 |
92 |
93 | {#each groups as group}
94 |
95 | - {
100 | focusGroup = group;
101 | }}
102 | on:keydown={() => {}}
103 | >
104 | {group}
105 |
106 | {/each}
107 |
108 |
109 |
{ console.debug("Click:", detail.key); }}
115 | >
116 |
117 | 💡 This is our default settings.
118 |
119 |
120 |
{ console.debug("Click:", detail.key); }}
126 | >
127 |
128 |
129 |
130 |
131 |
139 |
140 |
--------------------------------------------------------------------------------
/src/types/api.d.ts:
--------------------------------------------------------------------------------
1 | interface IResGetNotebookConf {
2 | box: string;
3 | conf: NotebookConf;
4 | name: string;
5 | }
6 |
7 | interface IReslsNotebooks {
8 | notebooks: Notebook[];
9 | }
10 |
11 | interface IResUpload {
12 | errFiles: string[];
13 | succMap: { [key: string]: string };
14 | }
15 |
16 | interface IResdoOperations {
17 | doOperations: doOperation[];
18 | undoOperations: doOperation[] | null;
19 | }
20 |
21 | interface IResGetBlockKramdown {
22 | id: BlockId;
23 | kramdown: string;
24 | }
25 |
26 | interface IResGetChildBlock {
27 | id: BlockId;
28 | type: BlockType;
29 | subtype?: BlockSubType;
30 | }
31 |
32 | interface IResGetTemplates {
33 | content: string;
34 | path: string;
35 | }
36 |
37 | interface IResReadDir {
38 | isDir: boolean;
39 | isSymlink: boolean;
40 | name: string;
41 | }
42 |
43 | interface IResExportMdContent {
44 | hPath: string;
45 | content: string;
46 | }
47 |
48 | interface IResBootProgress {
49 | progress: number;
50 | details: string;
51 | }
52 |
53 | interface IResForwardProxy {
54 | body: string;
55 | contentType: string;
56 | elapsed: number;
57 | headers: { [key: string]: string };
58 | status: number;
59 | url: string;
60 | }
61 |
62 | interface IResExportResources {
63 | path: string;
64 | }
65 |
66 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024 by frostime. All Rights Reserved.
3 | * @Author : frostime
4 | * @Date : 2023-08-15 10:28:10
5 | * @FilePath : /src/types/index.d.ts
6 | * @LastEditTime : 2024-06-08 20:50:53
7 | * @Description : Frequently used data structures in SiYuan
8 | */
9 |
10 |
11 | type DocumentId = string;
12 | type BlockId = string;
13 | type NotebookId = string;
14 | type PreviousID = BlockId;
15 | type ParentID = BlockId | DocumentId;
16 |
17 | type Notebook = {
18 | id: NotebookId;
19 | name: string;
20 | icon: string;
21 | sort: number;
22 | closed: boolean;
23 | }
24 |
25 | type NotebookConf = {
26 | name: string;
27 | closed: boolean;
28 | refCreateSavePath: string;
29 | createDocNameTemplate: string;
30 | dailyNoteSavePath: string;
31 | dailyNoteTemplatePath: string;
32 | }
33 |
34 | type BlockType =
35 | | 'd'
36 | | 'p'
37 | | 'query_embed'
38 | | 'l'
39 | | 'i'
40 | | 'h'
41 | | 'iframe'
42 | | 'tb'
43 | | 'b'
44 | | 's'
45 | | 'c'
46 | | 'widget'
47 | | 't'
48 | | 'html'
49 | | 'm'
50 | | 'av'
51 | | 'audio';
52 |
53 |
54 | type BlockSubType = "d1" | "d2" | "s1" | "s2" | "s3" | "t1" | "t2" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "table" | "task" | "toggle" | "latex" | "quote" | "html" | "code" | "footnote" | "cite" | "collection" | "bookmark" | "attachment" | "comment" | "mindmap" | "spreadsheet" | "calendar" | "image" | "audio" | "video" | "other";
55 |
56 | type Block = {
57 | id: BlockId;
58 | parent_id?: BlockId;
59 | root_id: DocumentId;
60 | hash: string;
61 | box: string;
62 | path: string;
63 | hpath: string;
64 | name: string;
65 | alias: string;
66 | memo: string;
67 | tag: string;
68 | content: string;
69 | fcontent?: string;
70 | markdown: string;
71 | length: number;
72 | type: BlockType;
73 | subtype: BlockSubType;
74 | /** string of { [key: string]: string }
75 | * For instance: "{: custom-type=\"query-code\" id=\"20230613234017-zkw3pr0\" updated=\"20230613234509\"}"
76 | */
77 | ial?: string;
78 | sort: number;
79 | created: string;
80 | updated: string;
81 | }
82 |
83 | type doOperation = {
84 | action: string;
85 | data: string;
86 | id: BlockId;
87 | parentID: BlockId | DocumentId;
88 | previousID: BlockId;
89 | retData: null;
90 | }
91 |
92 | interface Window {
93 | siyuan: {
94 | config: any;
95 | notebooks: any;
96 | menus: any;
97 | dialogs: any;
98 | blockPanels: any;
99 | storage: any;
100 | user: any;
101 | ws: any;
102 | languages: any;
103 | emojis: any;
104 | };
105 | Lute: any;
106 | }
107 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024 by frostime. All Rights Reserved.
3 | * @Author : frostime
4 | * @Date : 2023-05-19 19:49:13
5 | * @FilePath : /svelte.config.js
6 | * @LastEditTime : 2024-04-19 19:01:55
7 | * @Description :
8 | */
9 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"
10 |
11 | const NoWarns = new Set([
12 | "a11y-click-events-have-key-events",
13 | "a11y-no-static-element-interactions",
14 | "a11y-no-noninteractive-element-interactions"
15 | ]);
16 |
17 | export default {
18 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
19 | // for more information about preprocessors
20 | preprocess: vitePreprocess(),
21 | onwarn: (warning, handler) => {
22 | // suppress warnings on `vite dev` and `vite build`; but even without this, things still work
23 | if (NoWarns.has(warning.code)) return;
24 | handler(warning);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": [
7 | "ES2020",
8 | "DOM",
9 | "DOM.Iterable"
10 | ],
11 | "skipLibCheck": true,
12 | /* Bundler mode */
13 | "moduleResolution": "Node",
14 | // "allowImportingTsExtensions": true,
15 | "allowSyntheticDefaultImports": true,
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "noEmit": true,
19 | "jsx": "preserve",
20 | /* Linting */
21 | "strict": false,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noFallthroughCasesInSwitch": true,
25 | /* Svelte */
26 | /**
27 | * Typecheck JS in `.svelte` and `.js` files by default.
28 | * Disable checkJs if you'd like to use dynamic types in JS.
29 | * Note that setting allowJs false does not prevent the use
30 | * of JS in `.svelte` files.
31 | */
32 | "allowJs": true,
33 | "checkJs": true,
34 | "types": [
35 | "node",
36 | "vite/client",
37 | "svelte"
38 | ],
39 | // "baseUrl": "./src",
40 | "paths": {
41 | "@/*": ["./src/*"],
42 | "@/libs/*": ["./src/libs/*"],
43 | }
44 | },
45 | "include": [
46 | "tools/**/*.ts",
47 | "src/**/*.ts",
48 | "src/**/*.d.ts",
49 | "src/**/*.tsx",
50 | "src/**/*.vue",
51 | "src/**/*.svelte"
52 | ],
53 | "references": [
54 | {
55 | "path": "./tsconfig.node.json"
56 | }
57 | ],
58 | "root": "."
59 | }
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": [
10 | "vite.config.ts"
11 | ]
12 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "path"
2 | import { defineConfig, loadEnv } from "vite"
3 | import { viteStaticCopy } from "vite-plugin-static-copy"
4 | import livereload from "rollup-plugin-livereload"
5 | import { svelte } from "@sveltejs/vite-plugin-svelte"
6 | import zipPack from "vite-plugin-zip-pack";
7 | import fg from 'fast-glob';
8 |
9 | import vitePluginYamlI18n from './yaml-plugin';
10 |
11 | const env = process.env;
12 | const isSrcmap = env.VITE_SOURCEMAP === 'inline';
13 | const isDev = env.NODE_ENV === 'development';
14 |
15 | const outputDir = isDev ? "dev" : "dist";
16 |
17 | console.log("isDev=>", isDev);
18 | console.log("isSrcmap=>", isSrcmap);
19 | console.log("outputDir=>", outputDir);
20 |
21 | export default defineConfig({
22 | resolve: {
23 | alias: {
24 | "@": resolve(__dirname, "src"),
25 | }
26 | },
27 |
28 | plugins: [
29 | svelte(),
30 |
31 | vitePluginYamlI18n({
32 | inDir: 'public/i18n',
33 | outDir: `${outputDir}/i18n`
34 | }),
35 |
36 | viteStaticCopy({
37 | targets: [
38 | { src: "./README*.md", dest: "./" },
39 | { src: "./plugin.json", dest: "./" },
40 | { src: "./preview.png", dest: "./" },
41 | { src: "./icon.png", dest: "./" }
42 | ],
43 | }),
44 |
45 | ],
46 |
47 | define: {
48 | "process.env.DEV_MODE": JSON.stringify(isDev),
49 | "process.env.NODE_ENV": JSON.stringify(env.NODE_ENV)
50 | },
51 |
52 | build: {
53 | outDir: outputDir,
54 | emptyOutDir: false,
55 | minify: true,
56 | sourcemap: isSrcmap ? 'inline' : false,
57 |
58 | lib: {
59 | entry: resolve(__dirname, "src/index.ts"),
60 | fileName: "index",
61 | formats: ["cjs"],
62 | },
63 | rollupOptions: {
64 | plugins: [
65 | ...(isDev ? [
66 | livereload(outputDir),
67 | {
68 | name: 'watch-external',
69 | async buildStart() {
70 | const files = await fg([
71 | 'public/i18n/**',
72 | './README*.md',
73 | './plugin.json'
74 | ]);
75 | for (let file of files) {
76 | this.addWatchFile(file);
77 | }
78 | }
79 | }
80 | ] : [
81 | // Clean up unnecessary files under dist dir
82 | cleanupDistFiles({
83 | patterns: ['i18n/*.yaml', 'i18n/*.md'],
84 | distDir: outputDir
85 | }),
86 | zipPack({
87 | inDir: './dist',
88 | outDir: './',
89 | outFileName: 'package.zip'
90 | })
91 | ])
92 | ],
93 |
94 | external: ["siyuan", "process"],
95 |
96 | output: {
97 | entryFileNames: "[name].js",
98 | assetFileNames: (assetInfo) => {
99 | if (assetInfo.name === "style.css") {
100 | return "index.css"
101 | }
102 | return assetInfo.name
103 | },
104 | },
105 | },
106 | }
107 | });
108 |
109 |
110 | /**
111 | * Clean up some dist files after compiled
112 | * @author frostime
113 | * @param options:
114 | * @returns
115 | */
116 | function cleanupDistFiles(options: { patterns: string[], distDir: string }) {
117 | const {
118 | patterns,
119 | distDir
120 | } = options;
121 |
122 | return {
123 | name: 'rollup-plugin-cleanup',
124 | enforce: 'post',
125 | writeBundle: {
126 | sequential: true,
127 | order: 'post' as 'post',
128 | async handler() {
129 | const fg = await import('fast-glob');
130 | const fs = await import('fs');
131 | // const path = await import('path');
132 |
133 | // 使用 glob 语法,确保能匹配到文件
134 | const distPatterns = patterns.map(pat => `${distDir}/${pat}`);
135 | console.debug('Cleanup searching patterns:', distPatterns);
136 |
137 | const files = await fg.default(distPatterns, {
138 | dot: true,
139 | absolute: true,
140 | onlyFiles: false
141 | });
142 |
143 | // console.info('Files to be cleaned up:', files);
144 |
145 | for (const file of files) {
146 | try {
147 | if (fs.default.existsSync(file)) {
148 | const stat = fs.default.statSync(file);
149 | if (stat.isDirectory()) {
150 | fs.default.rmSync(file, { recursive: true });
151 | } else {
152 | fs.default.unlinkSync(file);
153 | }
154 | console.log(`Cleaned up: ${file}`);
155 | }
156 | } catch (error) {
157 | console.error(`Failed to clean up ${file}:`, error);
158 | }
159 | }
160 | }
161 | }
162 | };
163 | }
164 |
--------------------------------------------------------------------------------
/yaml-plugin.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2024 by frostime. All Rights Reserved.
3 | * @Author : frostime
4 | * @Date : 2024-04-05 21:27:55
5 | * @FilePath : /yaml-plugin.js
6 | * @LastEditTime : 2024-04-05 22:53:34
7 | * @Description : 去妮玛的 json 格式,我就是要用 yaml 写 i18n
8 | */
9 | // plugins/vite-plugin-parse-yaml.js
10 | import fs from 'fs';
11 | import yaml from 'js-yaml';
12 | import { resolve } from 'path';
13 |
14 | export default function vitePluginYamlI18n(options = {}) {
15 | // Default options with a fallback
16 | const DefaultOptions = {
17 | inDir: 'src/i18n',
18 | outDir: 'dist/i18n',
19 | };
20 |
21 | const finalOptions = { ...DefaultOptions, ...options };
22 |
23 | return {
24 | name: 'vite-plugin-yaml-i18n',
25 | buildStart() {
26 | console.log('🌈 Parse I18n: YAML to JSON..');
27 | const inDir = finalOptions.inDir;
28 | const outDir = finalOptions.outDir
29 |
30 | if (!fs.existsSync(outDir)) {
31 | fs.mkdirSync(outDir, { recursive: true });
32 | }
33 |
34 | //Parse yaml file, output to json
35 | const files = fs.readdirSync(inDir);
36 | for (const file of files) {
37 | if (file.endsWith('.yaml') || file.endsWith('.yml')) {
38 | console.log(`-- Parsing ${file}`)
39 | //检查是否有同名的json文件
40 | const jsonFile = file.replace(/\.(yaml|yml)$/, '.json');
41 | if (files.includes(jsonFile)) {
42 | console.log(`---- File ${jsonFile} already exists, skipping...`);
43 | continue;
44 | }
45 | try {
46 | const filePath = resolve(inDir, file);
47 | const fileContents = fs.readFileSync(filePath, 'utf8');
48 | const parsed = yaml.load(fileContents);
49 | const jsonContent = JSON.stringify(parsed, null, 2);
50 | const outputFilePath = resolve(outDir, file.replace(/\.(yaml|yml)$/, '.json'));
51 | console.log(`---- Writing to ${outputFilePath}`);
52 | fs.writeFileSync(outputFilePath, jsonContent);
53 | } catch (error) {
54 | this.error(`---- Error parsing YAML file ${file}: ${error.message}`);
55 | }
56 | }
57 | }
58 | },
59 | };
60 | }
61 |
--------------------------------------------------------------------------------